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
85 changes: 65 additions & 20 deletions contracts/marketx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,17 @@ pub use types::{
ArbitersConfig, ArbitersConfiguredEvent, BatchFeesCollectedEvent, BulkEscrowCreatedEvent,
BulkEscrowRequest, BuyerContribution, CancellationProposedEvent, ContractResourceProfile,
ContractVersion, CounterEvidenceSubmittedEvent, DataKey, DeliveryVerifiedEvent,
DisputeConsensusReachedEvent, DisputeVotingRecord, Escrow, EscrowCreatedEvent,
EscrowExpiredEvent, EscrowItem, EscrowStatus, EvidenceSubmittedEvent, EvidenceWindow,
EvidenceWindowExpiredEvent, FeeCapsChangedEvent, FeeChangedEvent, FeeCollectedEvent,
FeeCollectorRotatedEvent, FeeExemptionEvent, FeesWithdrawnEvent, FundsReleasedEvent,
GlobalDisputeAnalytics, GroupBuy, GroupBuyCompletedEvent, GroupBuyFundedEvent,
MediationOpenedEvent, MediationPhase, MediationProposedEvent, MediationSettledEvent,
MetadataVisibility, Milestone, MilestoneCompletedEvent, PendingOracleRelease, PendingUpgrade,
RefundHistoryEntry, RefundReason, RefundRequest, RefundRequestedEvent, RefundStatus,
StatusChangeEvent, StorageRentEstimate, TimeLock, TimeLockReleasedEvent,
TokenCircuitBreakerEvent, UpgradeCancelledEvent, UpgradeExecutedEvent, UpgradeProposedEvent,
APPEAL_WINDOW_LEDGERS, CONTRACT_VERSION, CURRENT_SCHEMA_VERSION,
DisputeConsensusReachedEvent, DisputeVotingRecord, Escrow, EscrowCancelledEvent,
EscrowCreatedEvent, EscrowExpiredEvent, EscrowItem, EscrowStatus, EvidenceSubmittedEvent,
EvidenceWindow, EvidenceWindowExpiredEvent, FeeCapsChangedEvent, FeeChangedEvent,
FeeCollectedEvent, FeeCollectorRotatedEvent, FeeExemptionEvent, FeesWithdrawnEvent,
FundsReleasedEvent, GlobalDisputeAnalytics, GroupBuy, GroupBuyCompletedEvent,
GroupBuyFundedEvent, MediationOpenedEvent, MediationPhase, MediationProposedEvent,
MediationSettledEvent, MetadataVisibility, Milestone, MilestoneCompletedEvent,
PendingOracleRelease, PendingUpgrade, RefundHistoryEntry, RefundReason, RefundRequest,
RefundRequestedEvent, RefundStatus, StatusChangeEvent, StorageRentEstimate, TimeLock,
TimeLockReleasedEvent, TokenCircuitBreakerEvent, UpgradeCancelledEvent, UpgradeExecutedEvent,
UpgradeProposedEvent, APPEAL_WINDOW_LEDGERS, CONTRACT_VERSION, CURRENT_SCHEMA_VERSION,
DEFAULT_ARBITER_QUORUM_PERCENTAGE, DEFAULT_EVIDENCE_WINDOW_LEDGERS,
DEFAULT_MAX_ARBITERS_PER_ESCROW, DEFAULT_MEDIATION_WINDOW_LEDGERS,
DEFAULT_MIN_ARBITERS_REQUIRED, DEFAULT_ORACLE_CHALLENGE_WINDOW_LEDGERS, MAX_DESCRIPTION_SIZE,
Expand Down Expand Up @@ -330,18 +330,31 @@ impl Contract {
env.storage().persistent().set(&key, &(current + amount));
}

fn refund_buyer(env: &Env, escrow: &mut Escrow) {
fn settle_to_buyer(env: &Env, escrow: &mut Escrow, status: EscrowStatus) {
let token_client = soroban_sdk::token::Client::new(env, &escrow.token);
token_client.transfer(
&env.current_contract_address(),
&escrow.buyer,
&escrow.amount,
);

escrow.status = EscrowStatus::Refunded;
escrow.status = status.clone();
escrow.cancellation_proposer = None;
Self::add_i128(env, DataKey::TotalRefundedAmount, escrow.amount);
Self::add_u32(env, DataKey::TotalRefundedCount);
if status == EscrowStatus::Cancelled {
Self::add_i128(env, DataKey::TotalCancelledAmount, escrow.amount);
Self::add_u32(env, DataKey::TotalCancelledCount);
} else {
Self::add_i128(env, DataKey::TotalRefundedAmount, escrow.amount);
Self::add_u32(env, DataKey::TotalRefundedCount);
}
}

fn refund_buyer(env: &Env, escrow: &mut Escrow) {
Self::settle_to_buyer(env, escrow, EscrowStatus::Refunded);
}

fn cancel_to_buyer(env: &Env, escrow: &mut Escrow) {
Self::settle_to_buyer(env, escrow, EscrowStatus::Cancelled);
}

fn validate_metadata(metadata: &Option<Bytes>) -> Result<(), ContractError> {
Expand Down Expand Up @@ -484,6 +497,9 @@ impl Contract {
env.storage()
.persistent()
.set(&DataKey::TotalRefundedAmount, &0i128);
env.storage()
.persistent()
.set(&DataKey::TotalReleasedAmount, &0i128);
env.storage()
.persistent()
.set(&DataKey::TotalDisputedCount, &0u32);
Expand All @@ -496,6 +512,9 @@ impl Contract {
env.storage()
.persistent()
.set(&DataKey::TotalCancelledCount, &0u32);
env.storage()
.persistent()
.set(&DataKey::TotalCancelledAmount, &0i128);
env.storage()
.persistent()
.set(&DataKey::TotalFeesCollected, &0i128);
Expand Down Expand Up @@ -880,6 +899,13 @@ impl Contract {
.unwrap_or(0)
}

pub fn get_total_cancelled_amount(env: Env) -> i128 {
env.storage()
.persistent()
.get(&DataKey::TotalCancelledAmount)
.unwrap_or(0)
}

/// Returns a structured summary containing comprehensive contract state metrics.
pub fn analytics_summary(env: Env) -> GlobalDisputeAnalytics {
let total_escrows = Self::get_total_escrows(env.clone());
Expand Down Expand Up @@ -1592,11 +1618,19 @@ impl Contract {

// If the other party already proposed, auto-accept the cancellation
let from_status = escrow.status.clone();
Self::refund_buyer(&env, &mut escrow);
Self::cancel_to_buyer(&env, &mut escrow);
env.storage()
.persistent()
.set(&DataKey::Escrow(escrow_id), &escrow);
Self::emit_status_change(&env, escrow_id, from_status, escrow.status.clone(), actor);
EscrowCancelledEvent {
escrow_id,
buyer: escrow.buyer.clone(),
seller: escrow.seller.clone(),
amount: escrow.amount,
was_funded: true,
}
.publish(&env);
return Ok(());
}

Expand Down Expand Up @@ -1642,11 +1676,19 @@ impl Contract {
}

let from_status = escrow.status.clone();
Self::refund_buyer(&env, &mut escrow);
Self::cancel_to_buyer(&env, &mut escrow);
env.storage()
.persistent()
.set(&DataKey::Escrow(escrow_id), &escrow);
Self::emit_status_change(&env, escrow_id, from_status, escrow.status.clone(), actor);
EscrowCancelledEvent {
escrow_id,
buyer: escrow.buyer.clone(),
seller: escrow.seller.clone(),
amount: escrow.amount,
was_funded: true,
}
.publish(&env);

Ok(())
}
Expand Down Expand Up @@ -1809,10 +1851,13 @@ impl Contract {
.persistent()
.remove(&DataKey::EscrowHash(hash));

EscrowExpiredEvent {
Self::add_u32(&env, DataKey::TotalCancelledCount);
EscrowCancelledEvent {
escrow_id,
buyer: escrow.buyer,
seller: escrow.seller,
buyer: escrow.buyer.clone(),
seller: escrow.seller.clone(),
amount: escrow.amount,
was_funded: false,
}
.publish(&env);

Expand Down
19 changes: 14 additions & 5 deletions contracts/marketx/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#[cfg(test)]
extern crate std;

use soroban_sdk::testutils::Events;
use soroban_sdk::{
testutils::{storage::Persistent as _, Address as _, Ledger as _, MockAuth, MockAuthInvoke},
testutils::{
storage::Persistent as _, Address as _, Events as _, Ledger as _, MockAuth, MockAuthInvoke,
},
Address, Bytes, Env, Event, IntoVal, Vec,
};

Expand Down Expand Up @@ -899,7 +900,7 @@ fn fund_fails_if_buyer_has_insufficient_balance() {
}

#[test]
fn seller_can_accept_buyer_cancellation_and_refund_immediately() {
fn seller_can_accept_buyer_cancellation_and_record_cancellation() {
let (env, client) = setup();
let admin = Address::generate(&env);
let buyer = Address::generate(&env);
Expand Down Expand Up @@ -930,10 +931,13 @@ fn seller_can_accept_buyer_cancellation_and_refund_immediately() {

assert_eq!(token.balance(&buyer), 1000);
assert_eq!(token.balance(&client.address), 0);
assert_eq!(client.get_total_refunded_amount(), 1000);
assert_eq!(client.get_total_refunded_amount(), 0);
assert_eq!(client.get_total_refunded_count(), 0);
assert_eq!(client.get_total_cancelled_count(), 1);
assert_eq!(client.get_total_cancelled_amount(), 1000);

let escrow = client.get_escrow(&escrow_id).unwrap();
assert_eq!(escrow.status, crate::types::EscrowStatus::Refunded);
assert_eq!(escrow.status, crate::types::EscrowStatus::Cancelled);
assert_eq!(escrow.cancellation_proposer, None);
}

Expand Down Expand Up @@ -1279,6 +1283,9 @@ fn test_arbiter_can_refund_buyer_on_dispute() {
assert_eq!(token.balance(&buyer), 1000);
let escrow = client.get_escrow(&escrow_id).unwrap();
assert_eq!(escrow.status, crate::types::EscrowStatus::Refunded);
assert_eq!(client.get_total_refunded_count(), 1);
assert_eq!(client.get_total_refunded_amount(), 1000);
assert_eq!(client.get_total_cancelled_count(), 0);
}

#[test]
Expand Down Expand Up @@ -2148,6 +2155,8 @@ fn test_cancel_unfunded_removes_escrow_after_expiry() {

// Escrow should be gone
assert!(client.get_escrow(&escrow_id).is_none());
assert_eq!(client.get_total_cancelled_count(), 1);
assert_eq!(client.get_total_cancelled_amount(), 0);
}

#[test]
Expand Down
12 changes: 12 additions & 0 deletions contracts/marketx/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub enum DataKey {
TotalReleasedCount,
TotalRefundedCount,
TotalCancelledCount,
TotalCancelledAmount,
TotalFeesCollected,
EscrowIds,

Expand Down Expand Up @@ -344,6 +345,17 @@ pub struct CancellationProposedEvent {
pub actor: Address,
}

#[contractevent(topics = ["escrow_cancelled"], data_format = "vec")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EscrowCancelledEvent {
#[topic]
pub escrow_id: u64,
pub buyer: Address,
pub seller: Address,
pub amount: i128,
pub was_funded: bool,
}

#[contractevent(topics = ["fee_changed"], data_format = "vec")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FeeChangedEvent {
Expand Down
Loading