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
14 changes: 1 addition & 13 deletions gas_results.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
[
{"contract":"bill_payments","method":"get_total_unpaid","scenario":"100_bills_50_cancelled","cpu":1077221,"mem":235460},
{"contract":"savings_goals","method":"get_all_goals","scenario":"100_goals_single_owner","cpu":2661552,"mem":480721},
{"contract":"insurance","method":"get_total_monthly_premium","scenario":"100_active_policies","cpu":2373104,"mem":427575},
{"contract":"family_wallet","method":"configure_multisig","scenario":"9_signers_threshold_all","cpu":342677,"mem":69106},
{"contract":"remittance_split","method":"distribute_usdc","scenario":"4_recipients_all_nonzero","cpu":654751,"mem":86208},
{"contract":"remittance_split","method":"create_remittance_schedule","scenario":"single_recurring_schedule","cpu":145230,"mem":28456},
{"contract":"remittance_split","method":"create_remittance_schedule","scenario":"11th_schedule_with_existing","cpu":167890,"mem":32145},
{"contract":"remittance_split","method":"modify_remittance_schedule","scenario":"single_schedule_modification","cpu":134567,"mem":26789},
{"contract":"remittance_split","method":"cancel_remittance_schedule","scenario":"single_schedule_cancellation","cpu":123456,"mem":24567},
{"contract":"remittance_split","method":"get_remittance_schedules","scenario":"empty_schedules","cpu":45678,"mem":12345},
{"contract":"remittance_split","method":"get_remittance_schedules","scenario":"5_schedules_with_isolation","cpu":234567,"mem":45678},
{"contract":"remittance_split","method":"get_remittance_schedule","scenario":"single_schedule_lookup","cpu":67890,"mem":15432},
{"contract":"remittance_split","method":"get_remittance_schedules","scenario":"50_schedules_worst_case","cpu":1234567,"mem":234567}
{"contract":"family_wallet","method":"configure_multisig","scenario":"9_signers_threshold_all","cpu":343463,"mem":69170}
]
1 change: 0 additions & 1 deletion reporting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,3 @@ Returns an empty Vec when fewer than two points are supplied.
**Determinism guarantee**: identical `history` input always produces identical
output regardless of call order, ledger state, or caller identity.

## Running Tests
168 changes: 136 additions & 32 deletions reporting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub enum ReportingError {
NotInitialized = 2,
Unauthorized = 3,
AddressesNotConfigured = 4,
NotAdminProposed = 5,
}

impl From<ReportingError> for soroban_sdk::Error {
Expand All @@ -159,6 +160,10 @@ impl From<ReportingError> for soroban_sdk::Error {
soroban_sdk::xdr::ScErrorType::Contract,
soroban_sdk::xdr::ScErrorCode::MissingValue,
)),
ReportingError::NotAdminProposed => soroban_sdk::Error::from((
soroban_sdk::xdr::ScErrorType::Contract,
soroban_sdk::xdr::ScErrorCode::InvalidAction,
)),
}
}
}
Expand Down Expand Up @@ -301,25 +306,25 @@ pub struct ReportingContract;
impl ReportingContract {
/// Initialize the reporting contract with an admin address.
///
/// This function must be called only once. The provided admin address will
/// have full control over contract configuration and maintenance.
///
/// # Arguments
/// * `admin` - Address of the contract administrator (must authorize)
/// * `admin` - Address of the initial contract administrator
///
/// # Returns
/// `Ok(())` on successful initialization
///
/// # Errors
/// * `AlreadyInitialized` - If the contract has already been initialized
///
/// # Panics
/// * If `admin` does not authorize the transaction
pub fn init(env: Env, admin: Address) -> Result<(), ReportingError> {
admin.require_auth();

let existing: Option<Address> = env.storage().instance().get(&symbol_short!("ADMIN"));
if existing.is_some() {
return Err(ReportingError::AlreadyInitialized);
}

admin.require_auth();

Self::extend_instance_ttl(&env);
env.storage()
.instance()
Expand All @@ -328,6 +333,76 @@ impl ReportingContract {
Ok(())
}

/// Propose a new administrator for the contract.
///
/// This is the first step of a two-step admin rotation process. The proposed
/// admin must then call `accept_admin_rotation` to complete the transfer.
///
/// # Arguments
/// * `caller` - Current administrator (must authorize)
/// * `new_admin` - Address of the proposed successor
///
/// # Errors
/// * `NotInitialized` - If contract has not been initialized
/// * `Unauthorized` - If caller is not the current admin
pub fn propose_new_admin(
env: Env,
caller: Address,
new_admin: Address,
) -> Result<(), ReportingError> {
caller.require_auth();

let admin: Address = env
.storage()
.instance()
.get(&symbol_short!("ADMIN"))
.ok_or(ReportingError::NotInitialized)?;

if caller != admin {
return Err(ReportingError::Unauthorized);
}

Self::extend_instance_ttl(&env);
env.storage()
.instance()
.set(&symbol_short!("PEND_ADM"), &new_admin);

Ok(())
}

/// Accept the role of contract administrator.
///
/// This is the second step of a two-step admin rotation process. Only the
/// address currently proposed via `propose_new_admin` can call this.
///
/// # Arguments
/// * `caller` - The proposed administrator (must authorize)
///
/// # Errors
/// * `NotAdminProposed` - If no admin rotation is currently in progress
/// * `Unauthorized` - If caller is not the proposed admin
pub fn accept_admin_rotation(env: Env, caller: Address) -> Result<(), ReportingError> {
caller.require_auth();

let pending_admin: Address = env
.storage()
.instance()
.get(&symbol_short!("PEND_ADM"))
.ok_or(ReportingError::NotAdminProposed)?;

if caller != pending_admin {
return Err(ReportingError::Unauthorized);
}

Self::extend_instance_ttl(&env);
env.storage()
.instance()
.set(&symbol_short!("ADMIN"), &pending_admin);
env.storage().instance().remove(&symbol_short!("PEND_ADM"));

Ok(())
}

/// Configure addresses for all related contracts (admin only).
///
/// # Arguments
Expand Down Expand Up @@ -390,7 +465,9 @@ impl ReportingContract {
Ok(())
}

/// Generate remittance summary report
/// Generate remittance summary report.
///
/// Fetches split configuration and calculates amounts for a specific period.
pub fn get_remittance_summary(
env: Env,
user: Address,
Expand Down Expand Up @@ -443,7 +520,9 @@ impl ReportingContract {
}
}

/// Generate savings progress report
/// Generate savings progress report.
///
/// Aggregates all goals for a user and calculates overall completion progress.
pub fn get_savings_report(
env: Env,
user: Address,
Expand Down Expand Up @@ -499,7 +578,9 @@ impl ReportingContract {
}
}

/// Generate bill payment compliance report
/// Generate bill payment compliance report.
///
/// Analyzes bill statuses and payment deadlines for a specific period.
pub fn get_bill_compliance_report(
env: Env,
user: Address,
Expand Down Expand Up @@ -577,7 +658,9 @@ impl ReportingContract {
}
}

/// Generate insurance coverage report
/// Generate insurance coverage report.
///
/// Summarizes active policies, coverage amounts, and premium ratios.
pub fn get_insurance_report(
env: Env,
user: Address,
Expand Down Expand Up @@ -699,7 +782,9 @@ impl ReportingContract {
}
}

/// Generate comprehensive financial health report
/// Generate comprehensive financial health report combining all metrics.
///
/// This is the primary reporting entry point for users.
pub fn get_financial_health_report(
env: Env,
user: Address,
Expand Down Expand Up @@ -736,7 +821,7 @@ impl ReportingContract {
}
}

/// Generate trend analysis comparing two periods
/// Generate trend analysis comparing two data points.
pub fn get_trend_analysis(
_env: Env,
_user: Address,
Expand Down Expand Up @@ -805,7 +890,7 @@ impl ReportingContract {
result
}

/// Store a financial health report for a user
/// Store a financial health report for a user (must authorize).
pub fn store_report(
env: Env,
user: Address,
Expand Down Expand Up @@ -837,7 +922,7 @@ impl ReportingContract {
true
}

/// Retrieve a stored report
/// Retrieve a previously stored report.
pub fn get_stored_report(
env: Env,
user: Address,
Expand All @@ -853,35 +938,46 @@ impl ReportingContract {
reports.get((user, period_key))
}

/// Get configured contract addresses
/// Get configured contract addresses.
pub fn get_addresses(env: Env) -> Option<ContractAddresses> {
env.storage().instance().get(&symbol_short!("ADDRS"))
}

/// Get admin address
/// Get current administrator address.
pub fn get_admin(env: Env) -> Option<Address> {
env.storage().instance().get(&symbol_short!("ADMIN"))
}

/// Archive old reports before the specified timestamp
/// Archive old reports before the specified timestamp (admin only).
///
/// Moves report data from the primary `REPORTS` storage to the `ARCH_RPT`
/// storage, potentially reducing gas costs for active users.
///
/// # Arguments
/// * `caller` - Address of the caller (must be admin)
/// * `before_timestamp` - Archive reports generated before this timestamp
/// * `caller` - Address of the administrator (must authorize)
/// * `before_timestamp` - Archive reports generated before this ledger timestamp
///
/// # Returns
/// Number of reports archived
pub fn archive_old_reports(env: Env, caller: Address, before_timestamp: u64) -> u32 {
/// `Ok(u32)` containing the number of reports archived
///
/// # Errors
/// * `NotInitialized` - If contract has not been initialized
/// * `Unauthorized` - If caller is not the admin
pub fn archive_old_reports(
env: Env,
caller: Address,
before_timestamp: u64,
) -> Result<u32, ReportingError> {
caller.require_auth();

let admin: Address = env
.storage()
.instance()
.get(&symbol_short!("ADMIN"))
.unwrap_or_else(|| panic!("Contract not initialized"));
.ok_or(ReportingError::NotInitialized)?;

if caller != admin {
panic!("Only admin can archive reports");
return Err(ReportingError::Unauthorized);
}

Self::extend_instance_ttl(&env);
Expand Down Expand Up @@ -938,7 +1034,7 @@ impl ReportingContract {
(archived_count, caller),
);

archived_count
Ok(archived_count)
}

/// Get archived reports for a user
Expand All @@ -965,25 +1061,33 @@ impl ReportingContract {
result
}

/// Permanently delete old archives before specified timestamp
/// Permanently delete old archives before specified timestamp (admin only).
///
/// # Arguments
/// * `caller` - Address of the caller (must be admin)
/// * `before_timestamp` - Delete archives created before this timestamp
/// * `caller` - Address of the administrator (must authorize)
/// * `before_timestamp` - Delete archives created before this ledger timestamp
///
/// # Returns
/// Number of archives deleted
pub fn cleanup_old_reports(env: Env, caller: Address, before_timestamp: u64) -> u32 {
/// `Ok(u32)` containing the number of archives deleted
///
/// # Errors
/// * `NotInitialized` - If contract has not been initialized
/// * `Unauthorized` - If caller is not the admin
pub fn cleanup_old_reports(
env: Env,
caller: Address,
before_timestamp: u64,
) -> Result<u32, ReportingError> {
caller.require_auth();

let admin: Address = env
.storage()
.instance()
.get(&symbol_short!("ADMIN"))
.unwrap_or_else(|| panic!("Contract not initialized"));
.ok_or(ReportingError::NotInitialized)?;

if caller != admin {
panic!("Only admin can cleanup reports");
return Err(ReportingError::Unauthorized);
}

Self::extend_instance_ttl(&env);
Expand Down Expand Up @@ -1021,7 +1125,7 @@ impl ReportingContract {
(deleted_count, caller),
);

deleted_count
Ok(deleted_count)
}

/// Returns aggregate counts of active and archived reports for observability.
Expand Down
Loading
Loading