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
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions contracts/split/src/calc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,57 @@ pub fn distribute_with_remainder(
shares_mut
}

// ---------------------------------------------------------------------------
// Issue #561: Canonical payout ordering
// ---------------------------------------------------------------------------

/// Sort recipients by their Stellar address byte representation (lexicographic
/// order over `Address`'s inner `BytesN<32>`).
///
/// This guarantees deterministic, canonical payout ordering regardless of the
/// order in which recipients were supplied at invoice creation.
///
/// # Arguments
/// * `env` – Soroban environment
/// * `recipients` – mutable list of recipient addresses to sort in-place
pub fn sort_recipients(env: &Env, recipients: &mut Vec<Address>) {
let n = recipients.len() as usize;
if n <= 1 {
return;
}

// Build byte representations for comparison.
let mut bytes_vec: Vec<(BytesN<32>, usize)> = Vec::new(env);
for i in 0..n {
let addr = recipients.get(i as u32).unwrap();
let bytes = addr.to_bytes();
bytes_vec.push_back((bytes, i));
}

// Insertion sort by byte representation (lexicographic).
for i in 1..n {
let key = bytes_vec.get(i).unwrap();
let mut j = i;
while j > 0 {
let prev = bytes_vec.get(j - 1).unwrap();
if prev.0 <= key.0 {
break;
}
bytes_vec.set(j, bytes_vec.get(j - 1).unwrap());
j -= 1;
}
bytes_vec.set(j, key.clone());
}

// Reorder recipients according to sorted indices.
let mut sorted = Vec::new(env);
for i in 0..n {
let (_, original_idx) = bytes_vec.get(i).unwrap();
sorted.push_back(recipients.get(original_idx as u32).unwrap());
}
*recipients = sorted;
}

// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------
Expand Down
8 changes: 8 additions & 0 deletions contracts/split/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ pub enum ContractError {
CircularDependency = 45,
/// Issue #453: Source contract has exceeded its call rate limit within the current window.
SourceContractRateLimited = 46,
/// Issue #559: creator fee bps + platform fee bps exceeds cap of 10000.
FeeSumExceedsCap = 47,
/// Issue #562: operation not allowed on a soft-deleted invoice.
InvoiceDeleted = 48,
/// Issue #562: invoice has unclaimed funds and cannot be soft-deleted.
FundsUnclaimed = 49,
/// Issue #562: attempted to read tombstone for an invoice that is not deleted.
NotDeleted = 50,
/// Issue #519: An invoice status transition is not permitted by the state machine.
InvalidStateTransition = 47,
/// Issue #518: A split ratio is invalid (e.g. >= denominator or sum mismatch).
Expand Down
62 changes: 62 additions & 0 deletions contracts/split/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1545,3 +1545,65 @@ pub fn batch_invoice_created(env: &Env, ids: &Vec<u64>) {
env.events()
.publish((symbol_short!("btch_crt"),), ids.clone());
}

// ---------------------------------------------------------------------------
// Issue #559: Creator Revenue Share
// ---------------------------------------------------------------------------

/// Emitted when the creator fee is deducted during invoice release.
///
/// Topics: `("creator_fee", invoice_id)`
/// Data: `(creator, fee_amount)`
pub fn creator_fee_paid(env: &Env, invoice_id: u64, creator: &Address, fee_amount: i128) {
env.events().publish(
(symbol_short!("creator_fee"), invoice_id),
(creator.clone(), fee_amount),
);
}

// ---------------------------------------------------------------------------
// Issue #560: Creator Migration
// ---------------------------------------------------------------------------

/// Emitted when a new creator is nominated for an invoice.
///
/// Topics: `("creator_nom", invoice_id)`
/// Data: `(successor)`
pub fn creator_nominated(env: &Env, invoice_id: u64, successor: &Address) {
env.events().publish(
(symbol_short!("creator_nom"), invoice_id),
successor.clone(),
);
}

/// Emitted when a creator role is accepted and the creator is migrated.
///
/// Topics: `("creator_mig", invoice_id)`
/// Data: `(new_creator)`
pub fn creator_migrated(env: &Env, invoice_id: u64, new_creator: &Address) {
env.events().publish(
(symbol_short!("creator_mig"), invoice_id),
new_creator.clone(),
);
}

// ---------------------------------------------------------------------------
// Issue #561: Payout Ordering
// ---------------------------------------------------------------------------

/// Emitted when a payout is initiated for a recipient.
///
/// Topics: `("payout_init", invoice_id, recipient_index)`
/// Data: `(recipient, amount)`
pub fn payout_initiated(
env: &Env,
invoice_id: u64,
recipient_index: u32,
recipient: &Address,
amount: i128,
) {
env.events().publish(
(symbol_short!("payout_init"), invoice_id, recipient_index),
(recipient.clone(), amount),
);
}
Loading