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
21 changes: 15 additions & 6 deletions contracts/split/src/calc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use crate::types::BASIS_POINTS_TOTAL;
use soroban_sdk::{Env, Vec};

use crate::error::ContractError;

/// Distribute `total` among recipients according to their `ratios` out of
/// `denom`, using the largest-remainder method to handle rounding.
///
Expand Down Expand Up @@ -35,9 +37,13 @@ pub fn distribute_with_remainder(
total: i128,
ratios: &Vec<i128>,
denom: i128,
) -> Vec<i128> {
assert!(!ratios.is_empty(), "ratios must not be empty");
assert!(denom > 0, "denom must be positive");
) -> Result<Vec<i128>, ContractError> {
if ratios.is_empty() {
return Err(ContractError::InvalidAmount);
}
if denom <= 0 {
return Err(ContractError::InvalidAmount);
}

let n = ratios.len() as usize;

Expand All @@ -59,7 +65,9 @@ pub fn distribute_with_remainder(
// Contracts with more than 64 recipients would need a larger cap, but
// 64 is a reasonable upper bound for on-chain use.
const MAX_RECIPIENTS: usize = 64;
assert!(n <= MAX_RECIPIENTS, "too many recipients (max 64)");
if n > MAX_RECIPIENTS {
return Err(ContractError::InvalidAmount);
}

let mut indices = [0usize; MAX_RECIPIENTS];
for i in 0..n {
Expand Down Expand Up @@ -95,7 +103,7 @@ pub fn distribute_with_remainder(
shares_mut.set(idx, current + 1);
}

shares_mut
Ok(shares_mut)
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -169,7 +177,8 @@ mod tests {
/// Assert sum equals total and return shares.
fn assert_exact(env: &Env, total: i128, ratios: &[i128], denom: i128) -> Vec<i128> {
let r_vec = make_ratios(env, ratios);
let result = distribute_with_remainder(env, total, &r_vec, denom);
let result = distribute_with_remainder(env, total, &r_vec, denom)
.expect("distribute_with_remainder should not fail for valid inputs");
let sum: i128 = result.iter().sum();
assert_eq!(
sum, total,
Expand Down
28 changes: 23 additions & 5 deletions contracts/split/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1060,15 +1060,30 @@ pub fn tranche_released(env: &Env, invoice_id: u64, tranche_index: u32, amount:

/// Issue #349: Emitted when an address's reputation score is updated.
/// Topics: (split, rep_upd, address)
/// Data: score
pub fn rep_updated(env: &Env, address: &Address, score: &RepScore) {
/// Data: (score_struct, computed_score)
///
/// # Computed score formula
///
/// The `computed_score` is a single `u32` summary of the raw `RepScore`
/// counters. The formula rewards consistent on-time payment and successful
/// invoice completion while penalising late payments and refunds:
///
/// ```text
/// base = paid_on_time * 10 + invoices_released * 5
/// deductions = late_pays * 5 + invoices_refunded * 2
/// computed = base.saturating_sub(deductions)
/// ```
///
/// Indexers are encouraged to use `computed_score` directly rather than
/// re-implementing the formula off-chain.
pub fn rep_updated(env: &Env, address: &Address, score: &RepScore, computed_score: u32) {
env.events().publish(
(
symbol_short!("split"),
symbol_short!("rep_upd"),
address.clone(),
),
score.clone(),
(score.clone(), computed_score),
);
}

Expand Down Expand Up @@ -1625,7 +1640,10 @@ pub fn child_invoice_unblocked(env: &Env, child_id: u64, parent_id: u64) {
/// Emitted every time a late-payment penalty is charged.
///
/// Topics: `("late_pen", invoice_id)`
/// Data: `(payer, penalty_amount)`
/// Data: `(invoice_id, payer, penalty_amount)`
///
/// `invoice_id` is included in both the topics (for indexer filtering) and
/// the data payload (for data-only decoders that do not inspect topics).
#[allow(dead_code)]
pub fn late_payment_penalty_charged(
env: &Env,
Expand All @@ -1635,7 +1653,7 @@ pub fn late_payment_penalty_charged(
) {
env.events().publish(
(symbol_short!("late_pen"), invoice_id),
(payer.clone(), penalty_amount),
(invoice_id, payer.clone(), penalty_amount),
);
}

Expand Down
12 changes: 11 additions & 1 deletion contracts/split/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,17 @@ where
let mut score = get_rep_internal(env, address);
update_fn(&mut score);
env.storage().persistent().set(&rep_key(address), &score);
events::rep_updated(env, address, &score);
// Compute the derived score integer before emitting the event.
// Formula: (paid_on_time * 10 + invoices_released * 5)
// .saturating_sub(late_pays * 5 + invoices_refunded * 2)
let base = (score.paid_on_time as u32)
.saturating_mul(10)
.saturating_add((score.invoices_released as u32).saturating_mul(5));
let deductions = (score.late_pays as u32)
.saturating_mul(5)
.saturating_add((score.invoices_refunded as u32).saturating_mul(2));
let computed_score = base.saturating_sub(deductions);
events::rep_updated(env, address, &score, computed_score);
score
}

Expand Down
86 changes: 0 additions & 86 deletions contracts/split/src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,92 +13,6 @@ const TOTAL_VOLUME: &str = "stats_total_volume";
const TOTAL_RECIPIENTS_PAID: &str = "stats_total_recipients_paid";
const STATS_UPDATED: &str = "StatsUpdated";

pub type Stats = (u64, i128, u64);

fn invoices_key(env: &Env) -> Symbol {
Symbol::new(env, TOTAL_INVOICES)
}

fn volume_key(env: &Env) -> Symbol {
Symbol::new(env, TOTAL_VOLUME)
}

fn recipients_paid_key(env: &Env) -> Symbol {
Symbol::new(env, TOTAL_RECIPIENTS_PAID)
}

pub fn get_stats(env: &Env) -> Stats {
let invoices = env
.storage()
.instance()
.get::<Symbol, u64>(&invoices_key(env))
.unwrap_or(0);
let volume = env
.storage()
.instance()
.get::<Symbol, i128>(&volume_key(env))
.unwrap_or(0);
let recipients_paid = env
.storage()
.instance()
.get::<Symbol, u64>(&recipients_paid_key(env))
.unwrap_or(0);

(invoices, volume, recipients_paid)
}

fn publish_updated(env: &Env, stats: Stats) {
env.events().publish(
(Symbol::new(env, STATS_UPDATED),),
(
stats.0,
stats.1,
stats.2,
env.ledger().sequence(),
),
);
}

pub fn record_invoice_created(env: &Env) -> Result<(), ContractError> {
let (invoices, volume, recipients_paid) = get_stats(env);
let updated_invoices = invoices
.checked_add(1)
.ok_or(ContractError::StatsOverflow)?;

env.storage()
.instance()
.set(&invoices_key(env), &updated_invoices);

publish_updated(env, (updated_invoices, volume, recipients_paid));
Ok(())
}

pub fn record_volume(env: &Env, amount: i128) -> Result<(), ContractError> {
let (invoices, volume, recipients_paid) = get_stats(env);
let updated_volume = volume
.checked_add(amount)
.ok_or(ContractError::StatsOverflow)?;

env.storage()
.instance()
.set(&volume_key(env), &updated_volume);

publish_updated(env, (invoices, updated_volume, recipients_paid));
Ok(())
}

pub fn record_recipients_paid(env: &Env, count: u64) -> Result<(), ContractError> {
let (invoices, volume, recipients_paid) = get_stats(env);
let updated_recipients_paid = recipients_paid
.checked_add(count)
.ok_or(ContractError::StatsOverflow)?;

env.storage()
.instance()
.set(&recipients_paid_key(env), &updated_recipients_paid);

publish_updated(env, (invoices, volume, updated_recipients_paid));
Ok(())
fn total_invoices_key(env: &Env) -> Symbol {
Symbol::new(env, TOTAL_INVOICES)
}
Expand Down