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
64 changes: 64 additions & 0 deletions contracts/claims-processor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,35 @@ impl ClaimsProcessor {
claim_id
}

/// Submit multiple claims in a single transaction up to MAX_BATCH_SIZE.
pub fn batch_submit_claims(env: Env, claimant: Address, policy_ids: Vec<u128>) -> Vec<u128> {
claimant.require_auth();
Self::require_not_paused(&env);

let mut claim_ids = Vec::new(&env);
let count = if policy_ids.len() > MAX_BATCH_SIZE {
MAX_BATCH_SIZE
} else {
policy_ids.len()
};

for i in 0..count {
let pid = policy_ids.get_unchecked(i);
let cid = Self::submit_claim(env.clone(), claimant.clone(), pid);
claim_ids.push_back(cid);
}

env.events().publish(
(Symbol::new(&env, "batch_claims_submitted"),),
BatchClaimsSubmitted {
claimant,
count,
},
);

claim_ids
}

/// Process an existing pending claim. Reads oracle data and pays out or rejects.
///
/// `partial_payout_bps` is an optional payout ratio in basis points (0-10000).
Expand Down Expand Up @@ -389,6 +418,41 @@ impl ClaimsProcessor {
Self::evaluate_and_settle(&env, &mut claim, &policy, partial_payout_bps)
}

/// Process multiple existing claims in a single transaction up to MAX_BATCH_SIZE.
pub fn batch_process_claims(
env: Env,
keeper: Address,
claim_ids: Vec<u128>,
partial_payout_bps: Option<u32>,
) -> Vec<(u128, ClaimResult)> {
Self::require_keeper(&env, &keeper);
Self::require_not_paused(&env);

let mut results = Vec::new(&env);
let count = if claim_ids.len() > MAX_BATCH_SIZE {
MAX_BATCH_SIZE
} else {
claim_ids.len()
};

for i in 0..count {
let cid = claim_ids.get_unchecked(i);
let res = Self::process_claim(env.clone(), keeper.clone(), cid, partial_payout_bps);
results.push_back((cid, res));
}

env.events().publish(
(Symbol::new(&env, "batch_claims_processed"),),
BatchClaimsProcessed {
keeper,
count,
},
);

results
}


/// Keeper-triggered automatic processing — no prior `submit_claim` needed.
/// This is the primary flow for parametric insurance.
/// Returns AlreadyClaimed / Expired idempotently if policy is already settled.
Expand Down
33 changes: 33 additions & 0 deletions contracts/claims-processor/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1278,3 +1278,36 @@ fn test_resolve_dispute_paid_claim_fails() {

cp.resolve_dispute(&w.admin, &claim_id);
}

// ── Batch Claim Processing (Issue #427) ──────────────────────────────────────

#[test]
fn test_batch_submit_and_process_claims() {
let w = deploy();
let pid = create_crop_product(&w);
let buyer = Address::generate(&w.env);
let pol_id1 = buy_crop_policy(&w, &buyer, pid);
let pol_id2 = buy_crop_policy(&w, &buyer, pid);

submit_rainfall(&w, 20_000_000); // 20mm < 50mm threshold

let cp = ClaimsProcessorClient::new(&w.env, &w.claims_id);

let mut policy_ids = soroban_sdk::Vec::new(&w.env);
policy_ids.push_back(pol_id1);
policy_ids.push_back(pol_id2);

let claim_ids = cp.batch_submit_claims(&buyer, &policy_ids);
assert_eq!(claim_ids.len(), 2);

let pending = cp.get_pending_claims();
assert_eq!(pending.len(), 2);

let results = cp.batch_process_claims(&w.keeper, &claim_ids, &None);
assert_eq!(results.len(), 2);
assert_eq!(results.get_unchecked(0).1, ClaimResult::Paid);
assert_eq!(results.get_unchecked(1).1, ClaimResult::Paid);

assert_eq!(cp.get_pending_claims().len(), 0);
}

15 changes: 15 additions & 0 deletions contracts/claims-processor/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,21 @@ pub struct ClaimSubmitted {
pub coverage_amount: i128,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BatchClaimsSubmitted {
pub claimant: Address,
pub count: u32,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BatchClaimsProcessed {
pub keeper: Address,
pub count: u32,
}


#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClaimProcessed {
Expand Down
34 changes: 33 additions & 1 deletion contracts/governance-dao/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,11 @@ enum StorageKey {
TemplateList,
/// Risk pool contract address for querying LP vote delegation.
RiskPool,
/// On-chain audit trail record for executed proposal — proposal_id -> ExecutionAuditRecord.
ExecutionAudit(u64),
}


#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
Expand Down Expand Up @@ -1099,12 +1102,41 @@ impl GovernanceDao {
.persistent()
.set(&StorageKey::Proposal(proposal_id), &proposal);

let executed_at = env.ledger().timestamp();
let audit = ExecutionAuditRecord {
proposal_id,
executor: proposal.proposer.clone(),
target: proposal.target.clone(),
function: proposal.function.clone(),
executed_at,
votes_for: proposal.votes_for,
votes_against: proposal.votes_against,
};
let audit_key = StorageKey::ExecutionAudit(proposal_id);
env.storage().persistent().set(&audit_key, &audit);
env.storage().persistent().extend_ttl(&audit_key, TTL_THRESHOLD, TTL_EXTEND_TO);

env.events().publish(
(Symbol::new(&env, "proposal_executed"),),
ProposalExecuted { proposal_id },
ProposalExecuted {
proposal_id,
executor: proposal.proposer,
target: proposal.target,
function: proposal.function,
executed_at,
},
);
}

/// Return the execution audit trail record for an executed proposal.
pub fn get_execution_audit(env: Env, proposal_id: u64) -> ExecutionAuditRecord {
env.storage()
.persistent()
.get(&StorageKey::ExecutionAudit(proposal_id))
.unwrap_or_else(|| panic_with_error!(&env, Error::ProposalNotFound))
}


/// Admin-only: cancel an Active proposal before voting closes.
///
/// Refunds the proposer's deposit (the exact amount locked at
Expand Down
46 changes: 46 additions & 0 deletions contracts/governance-dao/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1293,3 +1293,49 @@ fn deactivated_template_cannot_be_used() {
);
assert!(result.is_err());
}

// ── Governance Execution Audit Trail (Issue #428) ─────────────────────────────

#[test]
fn test_get_execution_audit_records_data() {
let (env, dao, _, voter1, voter2, target) = setup();
let args: Vec<Val> = Vec::new(&env);
let pid = dao.create_proposal(
&voter1,
&Bytes::from_slice(&env, b"Execute and audit me"),
&target,
&Symbol::new(&env, "update"),
&args,
);
dao.vote(&voter1, &pid, &VoteChoice::For);
dao.vote(&voter2, &pid, &VoteChoice::For);

env.ledger()
.with_mut(|l| l.timestamp += VOTING_PERIOD + (24 * 3600) + 1);

dao.finalize(&pid);
dao.execute(&pid);

let audit = dao.get_execution_audit(&pid);
assert_eq!(audit.proposal_id, pid);
assert_eq!(audit.target, target);
assert_eq!(audit.function, Symbol::new(&env, "update"));
assert_eq!(audit.executed_at, env.ledger().timestamp());
assert!(audit.votes_for > 0);
}

#[test]
#[should_panic(expected = "Error(Contract, #5)")]
fn test_get_execution_audit_unexecuted_panics() {
let (env, dao, _, voter1, _, target) = setup();
let args: Vec<Val> = Vec::new(&env);
let pid = dao.create_proposal(
&voter1,
&Bytes::from_slice(&env, b"Unexecuted"),
&target,
&Symbol::new(&env, "update"),
&args,
);
dao.get_execution_audit(&pid);
}

17 changes: 17 additions & 0 deletions contracts/governance-dao/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,29 @@ pub struct ProposalFinalized {
pub status: ProposalStatus,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExecutionAuditRecord {
pub proposal_id: u64,
pub executor: Address,
pub target: Address,
pub function: Symbol,
pub executed_at: u64,
pub votes_for: i128,
pub votes_against: i128,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProposalExecuted {
pub proposal_id: u64,
pub executor: Address,
pub target: Address,
pub function: Symbol,
pub executed_at: u64,
}


#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProposalCancelled {
Expand Down
Loading