Skip to content
Open
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
12 changes: 8 additions & 4 deletions contracts/invoice-escrow/src/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,8 @@ fn test_integration_duplicate_invoice_id_rejected() {
&ctx.inv_token_id,
&commitment,
&None,
);
&None,
);

let result = ctx.escrow.try_create_escrow(
&ctx.invoice_id,
Expand All @@ -649,7 +650,8 @@ fn test_integration_duplicate_invoice_id_rejected() {
&ctx.inv_token_id,
&commitment,
&None,
);
&None,
);
assert_eq!(result, Err(Ok(errors::Error::EscrowExists)));
}

Expand Down Expand Up @@ -756,7 +758,8 @@ fn test_integration_state_persistence_after_create() {
&ctx.inv_token_id,
&commitment,
&None,
);
&None,
);

let data = ctx.escrow.get_escrow(&ctx.invoice_id);
assert_eq!(data.inv_id, ctx.invoice_id);
Expand Down Expand Up @@ -985,7 +988,8 @@ fn test_integration_escrow_created_event_emitted() {
&ctx.inv_token_id,
&commitment,
&None,
);
&None,
);

let evts = env.events().all();
let evt = evts
Expand Down
31 changes: 29 additions & 2 deletions contracts/invoice-escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use types::{EmergencyApprovals, MultiSigConfig};

// EscrowStatus is re-exported publicly; Config and EscrowData are crate-private.
pub use types::EscrowStatus;
use types::{Config, EscrowData, FundingInvoice, InvoiceStatus};
use types::{CategoryFeeSchedule, Config, EscrowData, FundingInvoice, InvoiceCategory, InvoiceStatus};

use errors::Error;

Expand Down Expand Up @@ -145,6 +145,7 @@ impl InvoiceEscrow {
invoice_token: Address,
commitment: soroban_sdk::BytesN<32>,
funding_milestone: Option<i128>,
category: Option<InvoiceCategory>,
) -> Result<(), Error> {
seller.require_auth();
if face_value <= 0 || purchase_price <= 0 {
Expand Down Expand Up @@ -191,6 +192,7 @@ impl InvoiceEscrow {
}
let data = EscrowData {
inv_id: invoice_id.clone(),
category: category.unwrap_or(InvoiceCategory::Standard),
seller: seller.clone(),
debtor: debtor.clone(),
face_value,
Expand Down Expand Up @@ -521,7 +523,12 @@ impl InvoiceEscrow {
return Err(Error::InvalidAmount);
}

let fee_bps = i128::from(config.fee_bps);
let fee_bps = if let Some(cat_fee) = storage::get_category_fee(&env, data.category) {
i128::from(cat_fee.fee_bps)
} else {
i128::from(config.fee_bps)
};

// Fee is calculated on the payment amount (not face_value)
let platform_fee = amount
.checked_mul(fee_bps)
Expand Down Expand Up @@ -735,6 +742,26 @@ impl InvoiceEscrow {
Ok(())
}

/// Admin-only: set the fee schedule for a specific invoice category.
pub fn set_category_fee(
env: Env,
admin: Address,
category: InvoiceCategory,
fee_bps: u32,
) -> Result<(), Error> {
let config = storage::get_config(&env).ok_or(Error::NotInit)?;
if config.admin != admin {
return Err(Error::Unauthorized);
}
admin.require_auth();
if fee_bps > MAX_BPS {
return Err(Error::InvalidFeeBps);
}
let schedule = CategoryFeeSchedule { fee_bps };
storage::set_category_fee(&env, category, &schedule);
Ok(())
}

/// Set the payment distributor used for settlement/refund fan-out. Admin only.
pub fn set_payment_distributor(env: Env, payment_distributor: Address) -> Result<(), Error> {
let mut config = storage::get_config(&env).ok_or(Error::NotInit)?;
Expand Down
16 changes: 15 additions & 1 deletion contracts/invoice-escrow/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use soroban_sdk::{Address, Symbol};

use crate::types::{Config, EmergencyApprovals, EscrowData, MultiSigConfig, StorageKey};
use crate::types::{CategoryFeeSchedule, Config, EmergencyApprovals, EscrowData, InvoiceCategory, MultiSigConfig, StorageKey};

/// Ledgers below which a persistent entry's TTL is extended (~7 days at 5s/ledger).
const TTL_THRESHOLD: u32 = 120_960;
Expand Down Expand Up @@ -247,3 +247,17 @@ pub fn set_escrow_id_by_index(env: &soroban_sdk::Env, index: u32, invoice_id: &S
.persistent()
.set(&StorageKey::EscrowIdByIndex(index), invoice_id);
}

/// Load the category fee schedule for a given category.
pub fn get_category_fee(env: &soroban_sdk::Env, category: InvoiceCategory) -> Option<CategoryFeeSchedule> {
env.storage()
.persistent()
.get(&StorageKey::CategoryFee(category))
}

/// Save the category fee schedule for a given category.
pub fn set_category_fee(env: &soroban_sdk::Env, category: InvoiceCategory, schedule: &CategoryFeeSchedule) {
env.storage()
.persistent()
.set(&StorageKey::CategoryFee(category), schedule);
}
15 changes: 10 additions & 5 deletions contracts/invoice-escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2641,7 +2641,8 @@ fn test_create_escrow_with_commitment() {
&inv_token,
&commitment,
&None,
);
&None,
);

// Verify escrow was created with commitment
let escrow_data = escrow_client.get_escrow(&invoice_id);
Expand Down Expand Up @@ -2716,7 +2717,8 @@ fn test_commitment_included_in_created_event() {
&inv_token,
&commitment,
&None,
);
&None,
);

// Assert escrow_created event was emitted with commitment
let events = env.events().all();
Expand Down Expand Up @@ -2855,7 +2857,8 @@ fn test_commitment_persists_through_lifecycle() {
&inv_token_id,
&commitment,
&None,
);
&None,
);

// Verify commitment after creation
let escrow_data = escrow_client.get_escrow(&invoice_id);
Expand Down Expand Up @@ -5246,7 +5249,8 @@ fn test_settlement_at_exact_due_date_state_persistence() {
&inv_token_id,
&commitment,
&None,
);
&None,
);
escrow_client.fund_escrow(&invoice_id, &buyer, &purchase_price);

env.ledger().with_mut(|li| li.timestamp = due_date);
Expand Down Expand Up @@ -7785,7 +7789,8 @@ fn test_invoice_id_and_optional_metadata_event_encoding() {
&inv_token_id,
&commitment,
&None,
);
&None,
);

let events = env.events().all();
let mut decoded: Option<(Symbol, soroban_sdk::BytesN<32>, Option<i128>)> = None;
Expand Down
22 changes: 22 additions & 0 deletions contracts/invoice-escrow/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@ pub enum StorageKey {
EscrowCount,
/// Persistent: invoice_id indexed by sequential creation order.
EscrowIdByIndex(u32),
/// Persistent: fee bps by category.
CategoryFee(InvoiceCategory),
}

/// Category of commercial invoice.
#[contracttype]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum InvoiceCategory {
Standard = 0,
Factoring = 1,
Reverse = 2,
Government = 3,
}

/// Persistent fee schedule for a specific invoice category.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CategoryFeeSchedule {
pub fee_bps: u32,
}

/// Global contract configuration.
Expand Down Expand Up @@ -78,6 +98,8 @@ pub enum EscrowStatus {
pub struct EscrowData {
/// Invoice identifier (Symbol, ≀10 chars when used as key).
pub inv_id: soroban_sdk::Symbol,
/// Invoice category (determines fee schedule).
pub category: InvoiceCategory,
/// Seller (invoice owner).
pub seller: soroban_sdk::Address,
/// Debtor (authorized payer of the invoice).
Expand Down
Loading