Skip to content

Commit 3f8ff65

Browse files
authored
Merge pull request #279 from abore9769/feature/arbitrator-reputation
feat: arbitrator reputation tracking, ratings, and selection
2 parents 0e09b94 + 24e143f commit 3f8ff65

2 files changed

Lines changed: 203 additions & 2 deletions

File tree

contract/src/lib.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod governance;
88
mod oracle;
99
mod privacy;
1010
mod queries;
11+
mod reputation;
1112
mod social;
1213
mod storage;
1314
mod subscription;
@@ -21,8 +22,8 @@ use soroban_sdk::{contract, contractimpl, token::TokenClient, Address, BytesN, E
2122

2223
pub use errors::ContractError;
2324
pub use types::{
24-
DisclosureGrant, DisputeResolution, Proposal, ProposalAction, ProposalStatus,
25-
Subscription, SubscriptionTier, TierConfig, TemplateTerms, TemplateVersion,
25+
ArbitratorReputation, DisclosureGrant, DisputeResolution, Proposal, ProposalAction,
26+
ProposalStatus, Subscription, SubscriptionTier, TierConfig, TemplateTerms, TemplateVersion,
2627
Trade, TradePrivacy, TradeStatus, TradeTemplate, UserTier, UserTierInfo,
2728
};
2829
pub use queries::{PageParams, SortDirection, TradeFilter, TradeSortField, TradeStats};
@@ -204,6 +205,73 @@ impl StellarEscrowContract {
204205
Ok(())
205206
}
206207

208+
// -------------------------------------------------------------------------
209+
// Arbitrator Reputation
210+
// -------------------------------------------------------------------------
211+
212+
/// Rate the arbitrator of a disputed trade (buyer or seller, once each).
213+
pub fn rate_arbitrator(
214+
env: Env,
215+
trade_id: u64,
216+
rater: Address,
217+
stars: u32,
218+
) -> Result<(), ContractError> {
219+
require_initialized(&env)?;
220+
let trade = get_trade(&env, trade_id)?;
221+
let arbitrator = match &trade.arbitrator {
222+
Some(arb) => arb.clone(),
223+
None => return Err(ContractError::NoArbitrator),
224+
};
225+
rater.require_auth();
226+
reputation::rate_arbitrator(
227+
&env,
228+
trade_id,
229+
&rater,
230+
&arbitrator,
231+
&trade.buyer,
232+
&trade.seller,
233+
&trade.status,
234+
stars,
235+
)
236+
}
237+
238+
/// Raw reputation record for an arbitrator.
239+
pub fn get_arbitrator_reputation(env: Env, arbitrator: Address) -> ArbitratorReputation {
240+
storage::get_arbitrator_reputation(&env, &arbitrator)
241+
}
242+
243+
/// Average star rating ×100 (e.g. 450 = 4.50 stars). Returns 0 if unrated.
244+
pub fn get_arbitrator_avg_rating(env: Env, arbitrator: Address) -> u32 {
245+
reputation::average_rating_x100(&storage::get_arbitrator_reputation(&env, &arbitrator))
246+
}
247+
248+
/// Resolution rate in basis points (0–10000).
249+
pub fn get_arbitrator_resolution_rate(env: Env, arbitrator: Address) -> u32 {
250+
reputation::resolution_rate_bps(&storage::get_arbitrator_reputation(&env, &arbitrator))
251+
}
252+
253+
/// Composite reputation score (0–10000).
254+
pub fn get_arbitrator_score(env: Env, arbitrator: Address) -> u32 {
255+
reputation::composite_score(&storage::get_arbitrator_reputation(&env, &arbitrator))
256+
}
257+
258+
/// From a candidate list, return the registered arbitrator with the highest score.
259+
pub fn select_best_arbitrator(
260+
env: Env,
261+
candidates: soroban_sdk::Vec<Address>,
262+
) -> Result<Address, ContractError> {
263+
require_initialized(&env)?;
264+
reputation::select_best_arbitrator(&env, &candidates)
265+
}
266+
267+
/// Reputation records for all arbitrators in the supplied list (same order).
268+
pub fn get_arbitrator_reputations(
269+
env: Env,
270+
arbitrators: soroban_sdk::Vec<Address>,
271+
) -> soroban_sdk::Vec<ArbitratorReputation> {
272+
reputation::get_reputations(&env, &arbitrators)
273+
}
274+
207275
/// Update platform fee (admin only)
208276
pub fn update_fee(env: Env, fee_bps: u32) -> Result<(), ContractError> {
209277
require_initialized(&env)?;

contract/src/reputation.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/// Arbitrator reputation: query helpers and reputation-based selection.
2+
///
3+
/// Storage (get/save_arbitrator_reputation, has_rated, mark_rated) lives in
4+
/// storage.rs. This module adds the computation layer on top.
5+
6+
use soroban_sdk::{Address, Env, Vec};
7+
8+
use crate::errors::ContractError;
9+
use crate::events;
10+
use crate::storage::{
11+
get_arbitrator_reputation, has_arbitrator, has_rated, mark_rated,
12+
save_arbitrator_reputation,
13+
};
14+
use crate::types::{ArbitratorReputation, TradeStatus};
15+
16+
// ---------------------------------------------------------------------------
17+
// Rating
18+
// ---------------------------------------------------------------------------
19+
20+
/// Submit a 1–5 star rating for the arbitrator of a disputed/resolved trade.
21+
/// Only the buyer or seller may rate, once each per trade.
22+
pub fn rate_arbitrator(
23+
env: &Env,
24+
trade_id: u64,
25+
rater: &Address,
26+
arbitrator: &Address,
27+
buyer: &Address,
28+
seller: &Address,
29+
status: &TradeStatus,
30+
stars: u32,
31+
) -> Result<(), ContractError> {
32+
if stars < 1 || stars > 5 {
33+
return Err(ContractError::InvalidRating);
34+
}
35+
if rater != buyer && rater != seller {
36+
return Err(ContractError::Unauthorized);
37+
}
38+
// Only allow rating once a dispute has been raised
39+
if *status != TradeStatus::Disputed {
40+
return Err(ContractError::InvalidStatus);
41+
}
42+
if !has_arbitrator(env, arbitrator) {
43+
return Err(ContractError::ArbitratorNotRegistered);
44+
}
45+
if has_rated(env, trade_id, rater) {
46+
return Err(ContractError::AlreadyRated);
47+
}
48+
mark_rated(env, trade_id, rater);
49+
50+
let mut rep = get_arbitrator_reputation(env, arbitrator);
51+
rep.rating_sum = rep.rating_sum.saturating_add(stars);
52+
rep.rating_count = rep.rating_count.saturating_add(1);
53+
save_arbitrator_reputation(env, arbitrator, &rep);
54+
55+
events::emit_arb_rated(env, arbitrator.clone(), trade_id, rater.clone(), stars);
56+
events::emit_arb_rep_updated(
57+
env,
58+
arbitrator.clone(),
59+
rep.resolved_count,
60+
rep.rating_sum,
61+
rep.rating_count,
62+
);
63+
Ok(())
64+
}
65+
66+
// ---------------------------------------------------------------------------
67+
// Computed statistics
68+
// ---------------------------------------------------------------------------
69+
70+
/// Average star rating scaled ×100 (e.g. 450 = 4.50 stars). Returns 0 if unrated.
71+
pub fn average_rating_x100(rep: &ArbitratorReputation) -> u32 {
72+
if rep.rating_count == 0 {
73+
return 0;
74+
}
75+
rep.rating_sum
76+
.saturating_mul(100)
77+
.checked_div(rep.rating_count)
78+
.unwrap_or(0)
79+
}
80+
81+
/// Resolution rate in basis points (0–10000). Returns 0 if no disputes assigned.
82+
pub fn resolution_rate_bps(rep: &ArbitratorReputation) -> u32 {
83+
if rep.total_disputes == 0 {
84+
return 0;
85+
}
86+
((rep.resolved_count as u64)
87+
.saturating_mul(10_000)
88+
.checked_div(rep.total_disputes as u64)
89+
.unwrap_or(0)) as u32
90+
}
91+
92+
/// Composite score (0–10000): 60 % resolution rate + 40 % normalised rating.
93+
/// Rating is normalised so 5 stars → 10000: avg_rating_x100 × 20.
94+
pub fn composite_score(rep: &ArbitratorReputation) -> u32 {
95+
let rr = resolution_rate_bps(rep) as u64;
96+
let ar = (average_rating_x100(rep) as u64).saturating_mul(20).min(10_000);
97+
((rr.saturating_mul(6) + ar.saturating_mul(4)) / 10).min(10_000) as u32
98+
}
99+
100+
// ---------------------------------------------------------------------------
101+
// Reputation-based selection
102+
// ---------------------------------------------------------------------------
103+
104+
/// Return the registered arbitrator with the highest composite score from
105+
/// `candidates`. Ties broken by order. Errors if none are registered.
106+
pub fn select_best_arbitrator(
107+
env: &Env,
108+
candidates: &Vec<Address>,
109+
) -> Result<Address, ContractError> {
110+
let mut best: Option<Address> = None;
111+
let mut best_score: u32 = 0;
112+
for i in 0..candidates.len() {
113+
let c = candidates.get(i).unwrap();
114+
if !has_arbitrator(env, &c) {
115+
continue;
116+
}
117+
let score = composite_score(&get_arbitrator_reputation(env, &c));
118+
if best.is_none() || score > best_score {
119+
best_score = score;
120+
best = Some(c);
121+
}
122+
}
123+
best.ok_or(ContractError::ArbitratorNotRegistered)
124+
}
125+
126+
/// Return reputation records for all addresses in `arbitrators` (same order).
127+
pub fn get_reputations(env: &Env, arbitrators: &Vec<Address>) -> Vec<ArbitratorReputation> {
128+
let mut out = Vec::new(env);
129+
for i in 0..arbitrators.len() {
130+
out.push_back(get_arbitrator_reputation(env, &arbitrators.get(i).unwrap()));
131+
}
132+
out
133+
}

0 commit comments

Comments
 (0)