diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7730091a..cf5221d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: echo "Installed via Homebrew" else echo "Homebrew install failed, installing via cargo..." - cargo install --locked --version 21.0.0 soroban-cli + cargo install --locked --version 21.1.0 soroban-cli fi fi # Try both commands in case Homebrew installs it as 'stellar' diff --git a/Cargo.toml b/Cargo.toml index 213e6882..b75c9080 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ default-members = [ resolver = "2" [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" remittance_split = { path = "./remittance_split" } savings_goals = { path = "./savings_goals" } bill_payments = { path = "./bill_payments" } @@ -45,7 +45,7 @@ reporting = { path = "./reporting" } orchestrator = { path = "./orchestrator" } [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } [profile.release] opt-level = "z" overflow-checks = true diff --git a/README.md b/README.md index 7f409c93..a8ff1900 100644 --- a/README.md +++ b/README.md @@ -403,6 +403,21 @@ Manages family roles, spending controls, multisig approvals, and emergency trans For full design details, see [docs/family-wallet-design.md](docs/family-wallet-design.md). +### Data Migration + +Utilities for contract data export and import (JSON, Binary, CSV). + +**Key Functions:** + +- `export_to_csv`: Export savings goals to a tabular CSV format. +- `import_goals_from_csv`: Import goals from CSV with strict schema validation. +- `export_to_json` / `import_from_json`: Versioned snapshot migration. + +**CSV Hardening:** +- Strict header validation (exact match and order). +- Row-level type and value validation (e.g., non-negative amounts). +- Rejection of ambiguous or malformed data. + ## Events All contracts emit events for important state changes, enabling real-time tracking and frontend integration. Events follow Soroban best practices and include: diff --git a/bill_payments/Cargo.toml b/bill_payments/Cargo.toml index ec3dbff0..a3949adf 100644 --- a/bill_payments/Cargo.toml +++ b/bill_payments/Cargo.toml @@ -7,12 +7,12 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" remitwise-common = { path = "../remitwise-common" } [dev-dependencies] proptest = "1.10.0" -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } testutils = { path = "../testutils" } diff --git a/bill_payments/src/lib.rs b/bill_payments/src/lib.rs index cd64fec6..498e691c 100644 --- a/bill_payments/src/lib.rs +++ b/bill_payments/src/lib.rs @@ -3,8 +3,8 @@ use remitwise_common::{ clamp_limit, EventCategory, EventPriority, RemitwiseEvents, ARCHIVE_BUMP_AMOUNT, - ARCHIVE_LIFETIME_THRESHOLD, CONTRACT_VERSION, DEFAULT_PAGE_LIMIT, INSTANCE_BUMP_AMOUNT, - INSTANCE_LIFETIME_THRESHOLD, MAX_BATCH_SIZE, MAX_PAGE_LIMIT, + ARCHIVE_LIFETIME_THRESHOLD, CONTRACT_VERSION, INSTANCE_BUMP_AMOUNT, + INSTANCE_LIFETIME_THRESHOLD, MAX_BATCH_SIZE, }; use soroban_sdk::{ @@ -12,10 +12,8 @@ use soroban_sdk::{ Symbol, Vec, }; -#[derive(Clone, Debug)] #[contracttype] #[derive(Clone, Debug)] -#[contracttype] pub struct Bill { pub id: u32, pub owner: Address, @@ -57,8 +55,6 @@ pub mod pause_functions { pub const RESTORE: soroban_sdk::Symbol = symbol_short!("restore"); } -const CONTRACT_VERSION: u32 = 1; -const MAX_BATCH_SIZE: u32 = 50; const STORAGE_UNPAID_TOTALS: Symbol = symbol_short!("UNPD_TOT"); #[contracterror] @@ -77,14 +73,12 @@ pub enum Error { BatchValidationFailed = 10, InvalidLimit = 11, InvalidDueDate = 12, - InvalidTag = 12, - EmptyTags = 13, + InvalidTag = 13, + EmptyTags = 14, } -#[derive(Clone)] #[contracttype] #[derive(Clone)] -#[contracttype] pub struct ArchivedBill { pub id: u32, pub owner: Address, @@ -100,20 +94,25 @@ pub struct ArchivedBill { /// Paginated result for archived bill queries #[contracttype] -#[derive(Clone)] pub struct ArchivedBillPage { pub items: Vec, - /// 0 means no more pages pub next_cursor: u32, pub count: u32, } #[contracttype] -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum BillEvent { Created, Paid, + Cancelled, + Archived, + Restored, ExternalRefUpdated, +} + +#[contracttype] +#[derive(Clone, Debug)] pub struct StorageStats { pub active_bills: u32, pub archived_bills: u32, @@ -436,7 +435,7 @@ impl BillPayments { }; let bill_owner = bill.owner.clone(); - let bill_external_ref = bill.external_ref.clone(); + let _bill_external_ref = bill.external_ref.clone(); bills.set(next_id, bill); env.storage() .instance() @@ -447,9 +446,6 @@ impl BillPayments { Self::adjust_unpaid_total(&env, &bill_owner, amount); // Emit event for audit trail - env.events().publish( - (symbol_short!("bill"), BillEvent::Created), - (next_id, bill_owner, bill_external_ref), RemitwiseEvents::emit( &env, EventCategory::State, @@ -516,7 +512,7 @@ impl BillPayments { .set(&symbol_short!("NEXT_ID"), &next_id); } - let bill_external_ref = bill.external_ref.clone(); + let _bill_external_ref = bill.external_ref.clone(); let paid_amount = bill.amount; let was_recurring = bill.recurring; bills.set(bill_id, bill); @@ -528,9 +524,6 @@ impl BillPayments { } // Emit event for audit trail - env.events().publish( - (symbol_short!("bill"), BillEvent::Paid), - (bill_id, caller, bill_external_ref), RemitwiseEvents::emit( &env, EventCategory::Transaction, @@ -766,7 +759,18 @@ impl BillPayments { /// /// # Returns /// Vec of all Bill structs - pub fn get_all_bills(env: Env) -> Vec { + pub fn get_all_bills_deprecated(env: Env) -> Vec { + let bills: Map = env + .storage() + .instance() + .get(&symbol_short!("BILLS")) + .unwrap_or_else(|| Map::new(&env)); + let mut result = Vec::new(&env); + for (_, bill) in bills.iter() { + result.push_back(bill); + } + result + } // ----------------------------------------------------------------------- // Backward-compat helpers // ----------------------------------------------------------------------- @@ -986,6 +990,7 @@ impl BillPayments { id: archived_bill.id, owner: archived_bill.owner.clone(), name: archived_bill.name.clone(), + external_ref: None, // Or logic to recover if stored elsewhere amount: archived_bill.amount, due_date: env.ledger().timestamp() + 2592000, recurring: false, @@ -1111,6 +1116,7 @@ impl BillPayments { id: next_id, owner: bill.owner.clone(), name: bill.name.clone(), + external_ref: bill.external_ref.clone(), amount: bill.amount, due_date: next_due_date, recurring: true, @@ -1211,7 +1217,7 @@ impl BillPayments { cursor: u32, limit: u32, ) -> BillPage { - let limit = Self::clamp_limit(limit); + let limit = clamp_limit(limit); let bills: Map = env .storage() .instance() @@ -1245,7 +1251,7 @@ impl BillPayments { cursor: u32, limit: u32, ) -> BillPage { - let limit = Self::clamp_limit(limit); + let limit = clamp_limit(limit); let bills: Map = env .storage() .instance() diff --git a/bill_payments/tests/stress_tests.rs b/bill_payments/tests/stress_tests.rs index eb512c4a..914b1923 100644 --- a/bill_payments/tests/stress_tests.rs +++ b/bill_payments/tests/stress_tests.rs @@ -84,7 +84,11 @@ fn stress_200_bills_single_user() { // Verify aggregate total let total = client.get_total_unpaid(&owner); - assert_eq!(total, 200 * 100i128, "get_total_unpaid must sum all 200 bills"); + assert_eq!( + total, + 200 * 100i128, + "get_total_unpaid must sum all 200 bills" + ); // Exhaust all pages with MAX_PAGE_LIMIT (50) — should take exactly 4 pages let mut collected = 0u32; @@ -328,8 +332,14 @@ fn stress_archive_100_paid_bills() { // Verify storage stats let stats = client.get_storage_stats(); - assert_eq!(stats.active_bills, 0, "No active bills should remain after full archive"); - assert_eq!(stats.archived_bills, 100, "Storage stats must show 100 archived bills"); + assert_eq!( + stats.active_bills, 0, + "No active bills should remain after full archive" + ); + assert_eq!( + stats.archived_bills, 100, + "Storage stats must show 100 archived bills" + ); // Verify paginated access to archived bills let mut archived_seen = 0u32; @@ -487,8 +497,9 @@ fn bench_archive_paid_bills_100() { client.pay_bill(&owner, &id); } - let (cpu, mem, result) = - measure(&env, || client.archive_paid_bills(&owner, &2_000_000_000u64)); + let (cpu, mem, result) = measure(&env, || { + client.archive_paid_bills(&owner, &2_000_000_000u64) + }); assert_eq!(result, 100); println!( diff --git a/data_migration/src/lib.rs b/data_migration/src/lib.rs index 224c46ee..a08925d3 100644 --- a/data_migration/src/lib.rs +++ b/data_migration/src/lib.rs @@ -13,6 +13,17 @@ use std::collections::HashMap; /// Current schema version for migration compatibility. pub const SCHEMA_VERSION: u32 = 1; +/// Expected headers for Savings Goals CSV export/import. +pub const SAVINGS_GOAL_HEADERS: &[&str] = &[ + "id", + "owner", + "name", + "target_amount", + "current_amount", + "target_date", + "locked", +]; + /// Minimum supported schema version for import. pub const MIN_SUPPORTED_VERSION: u32 = 1; @@ -107,7 +118,10 @@ impl ExportSnapshot { /// Compute SHA256 checksum of the payload (canonical JSON). pub fn compute_checksum(&self) -> String { let mut hasher = Sha256::new(); - hasher.update(serde_json::to_vec(&self.payload).unwrap_or_else(|_| panic!("payload must be serializable"))); + hasher.update( + serde_json::to_vec(&self.payload) + .unwrap_or_else(|_| panic!("payload must be serializable")), + ); hex::encode(hasher.finalize().as_ref()) } @@ -260,13 +274,68 @@ pub fn import_from_binary(bytes: &[u8]) -> Result Result, MigrationError> { - let mut rdr = csv::Reader::from_reader(bytes); + let mut rdr = csv::ReaderBuilder::new() + .has_headers(true) + .from_reader(bytes); + + // 1. Strict Header Validation + { + let headers = rdr + .headers() + .map_err(|e| MigrationError::InvalidFormat(format!("Failed to read headers: {}", e)))?; + + if headers.len() != SAVINGS_GOAL_HEADERS.len() { + return Err(MigrationError::ValidationFailed(format!( + "Header length mismatch: expected {}, found {}", + SAVINGS_GOAL_HEADERS.len(), + headers.len() + ))); + } + + for (i, expected) in SAVINGS_GOAL_HEADERS.iter().enumerate() { + if headers.get(i) != Some(expected) { + return Err(MigrationError::ValidationFailed(format!( + "Header mismatch at column {}: expected '{}', found '{:?}'", + i, + expected, + headers.get(i) + ))); + } + } + } + let mut goals = Vec::new(); - for result in rdr.deserialize() { - let record: CsvGoalRow = - result.map_err(|e| MigrationError::DeserializeError(e.to_string()))?; + for (row_idx, result) in rdr.deserialize().enumerate() { + let record: CsvGoalRow = result.map_err(|e| { + MigrationError::DeserializeError(format!("Row {} error: {}", row_idx + 1, e)) + })?; + + // 2. Extra Row-Level Validation (Internal Consistency) + if record.target_amount < 0 { + return Err(MigrationError::ValidationFailed(format!( + "Row {}: target_amount cannot be negative", + row_idx + 1 + ))); + } + if record.current_amount < 0 { + return Err(MigrationError::ValidationFailed(format!( + "Row {}: current_amount cannot be negative", + row_idx + 1 + ))); + } + goals.push(SavingsGoalExport { id: record.id, owner: record.owner, @@ -277,6 +346,13 @@ pub fn import_goals_from_csv(bytes: &[u8]) -> Result, Mig locked: record.locked, }); } + + if goals.is_empty() { + return Err(MigrationError::ValidationFailed( + "CSV contains no data rows".into(), + )); + } + Ok(goals) } @@ -440,4 +516,67 @@ mod tests { let MigrationEvent::V1(v1) = loaded; assert_eq!(v1.version, SCHEMA_VERSION); } + + #[test] + fn test_csv_import_wrong_headers_fails() { + let csv_data = "id,owner,bad_header,target_amount,current_amount,target_date,locked\n1,G1,Name,1000,500,2000000000,true"; + let result = import_goals_from_csv(csv_data.as_bytes()); + assert!(result.is_err()); + if let Err(MigrationError::ValidationFailed(msg)) = result { + assert!(msg.contains("Header mismatch at column 2")); + } else { + panic!("Expected ValidationFailed error"); + } + } + + #[test] + fn test_csv_import_missing_columns_fails() { + let csv_data = "id,owner,name,target_amount,current_amount,target_date\n1,G1,Emergency,1000,500,2000000000"; + let result = import_goals_from_csv(csv_data.as_bytes()); + assert!(result.is_err()); + if let Err(MigrationError::ValidationFailed(msg)) = result { + assert!(msg.contains("Header length mismatch")); + } else { + panic!("Expected ValidationFailed error"); + } + } + + #[test] + fn test_csv_import_malformed_row_data_fails() { + let csv_data = "id,owner,name,target_amount,current_amount,target_date,locked\n1,G1,Emergency,not_a_number,500,2000000000,true"; + let result = import_goals_from_csv(csv_data.as_bytes()); + assert!(result.is_err()); + assert!(matches!(result, Err(MigrationError::DeserializeError(_)))); + } + + #[test] + fn test_csv_import_negative_amounts_fails() { + let csv_data = "id,owner,name,target_amount,current_amount,target_date,locked\n1,G1,Emergency,-1000,500,2000000000,true"; + let result = import_goals_from_csv(csv_data.as_bytes()); + assert!(result.is_err()); + if let Err(MigrationError::ValidationFailed(msg)) = result { + assert!(msg.contains("target_amount cannot be negative")); + } else { + panic!("Expected ValidationFailed error"); + } + } + + #[test] + fn test_csv_import_empty_file_fails() { + let csv_data = ""; + let result = import_goals_from_csv(csv_data.as_bytes()); + assert!(result.is_err()); + } + + #[test] + fn test_csv_import_only_headers_fails() { + let csv_data = "id,owner,name,target_amount,current_amount,target_date,locked"; + let result = import_goals_from_csv(csv_data.as_bytes()); + assert!(result.is_err()); + if let Err(MigrationError::ValidationFailed(msg)) = result { + assert!(msg.contains("CSV contains no data rows")); + } else { + panic!("Expected ValidationFailed error"); + } + } } diff --git a/emergency_killswitch/Cargo.toml b/emergency_killswitch/Cargo.toml index ab7f219e..f8aca4db 100644 --- a/emergency_killswitch/Cargo.toml +++ b/emergency_killswitch/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } diff --git a/examples/bill_payments_example.rs b/examples/bill_payments_example.rs index dee3c269..0def3019 100644 --- a/examples/bill_payments_example.rs +++ b/examples/bill_payments_example.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{Env, Address, String, testutils::Address as _}; use bill_payments::{BillPayments, BillPaymentsClient}; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; fn main() { // 1. Setup the Soroban environment @@ -22,14 +22,21 @@ fn main() { let currency = String::from_str(&env, "USD"); println!("Creating bill: '{}' for {} {}", bill_name, amount, currency); - let bill_id = client.create_bill(&owner, &bill_name, &amount, &due_date, &false, &0, ¤cy).unwrap(); + let bill_id = client + .create_bill( + &owner, &bill_name, &amount, &due_date, &false, &0, ¤cy, + ) + .unwrap(); println!("Bill created successfully with ID: {}", bill_id); // 5. [Read] List unpaid bills let bill_page = client.get_unpaid_bills(&owner, &0, &5); println!("\nUnpaid Bills for {:?}:", owner); for bill in bill_page.items.iter() { - println!(" ID: {}, Name: {}, Amount: {} {}", bill.id, bill.name, bill.amount, bill.currency); + println!( + " ID: {}, Name: {}, Amount: {} {}", + bill.id, bill.name, bill.amount, bill.currency + ); } // 6. [Write] Pay the bill diff --git a/examples/family_wallet_example.rs b/examples/family_wallet_example.rs index 0781ebda..8b5368fc 100644 --- a/examples/family_wallet_example.rs +++ b/examples/family_wallet_example.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{Env, Address, Vec, testutils::Address as _}; -use family_wallet::{FamilyWallet, FamilyWalletClient, FamilyRole}; +use family_wallet::{FamilyRole, FamilyWallet, FamilyWalletClient}; +use soroban_sdk::{testutils::Address as _, Address, Env, Vec}; fn main() { // 1. Setup the Soroban environment @@ -22,21 +22,23 @@ fn main() { let mut initial_members = Vec::new(&env); initial_members.push_back(owner.clone()); initial_members.push_back(member1.clone()); - + client.init(&owner, &initial_members); println!("Wallet initialized successfully!"); // 5. [Read] Check roles of members let owner_member = client.get_member(&owner).unwrap(); println!("\nOwner Role: {:?}", owner_member.role); - + let m1_member = client.get_member(&member1).unwrap(); println!("Member 1 Role: {:?}", m1_member.role); // 6. [Write] Add a new family member with a specific role and spending limit println!("\nAdding new member: {:?}", member2); let spending_limit = 1000i128; - client.add_member(&owner, &member2, &FamilyRole::Member, &spending_limit).unwrap(); + client + .add_member(&owner, &member2, &FamilyRole::Member, &spending_limit) + .unwrap(); println!("Member added successfully!"); // 7. [Read] Verify the new member diff --git a/examples/insurance_example.rs b/examples/insurance_example.rs index 591ec493..31d00036 100644 --- a/examples/insurance_example.rs +++ b/examples/insurance_example.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{Env, Address, String, testutils::Address as _}; use insurance::{Insurance, InsuranceClient}; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; fn main() { // 1. Setup the Soroban environment @@ -21,15 +21,29 @@ fn main() { let monthly_premium = 200i128; let coverage_amount = 50000i128; - println!("Creating policy: '{}' with premium: {} and coverage: {}", policy_name, monthly_premium, coverage_amount); - let policy_id = client.create_policy(&owner, &policy_name, &coverage_type, &monthly_premium, &coverage_amount).unwrap(); + println!( + "Creating policy: '{}' with premium: {} and coverage: {}", + policy_name, monthly_premium, coverage_amount + ); + let policy_id = client + .create_policy( + &owner, + &policy_name, + &coverage_type, + &monthly_premium, + &coverage_amount, + ) + .unwrap(); println!("Policy created successfully with ID: {}", policy_id); // 5. [Read] List active policies let policy_page = client.get_active_policies(&owner, &0, &5); println!("\nActive Policies for {:?}:", owner); for policy in policy_page.items.iter() { - println!(" ID: {}, Name: {}, Premium: {}, Coverage: {}", policy.id, policy.name, policy.monthly_premium, policy.coverage_amount); + println!( + " ID: {}, Name: {}, Premium: {}, Coverage: {}", + policy.id, policy.name, policy.monthly_premium, policy.coverage_amount + ); } // 6. [Write] Pay a premium @@ -39,7 +53,10 @@ fn main() { // 7. [Read] Verify policy status (next payment date updated) let policy = client.get_policy(&policy_id).unwrap(); - println!("Next Payment Date (Timestamp): {}", policy.next_payment_date); + println!( + "Next Payment Date (Timestamp): {}", + policy.next_payment_date + ); println!("\nExample completed successfully!"); } diff --git a/examples/orchestrator_example.rs b/examples/orchestrator_example.rs index 206bd282..af243161 100644 --- a/examples/orchestrator_example.rs +++ b/examples/orchestrator_example.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{Env, Address, testutils::Address as _}; use orchestrator::{Orchestrator, OrchestratorClient}; +use soroban_sdk::{testutils::Address as _, Address, Env}; fn main() { // 1. Setup the Soroban environment @@ -12,7 +12,7 @@ fn main() { // 3. Generate mock addresses for all participants and contracts let caller = Address::generate(&env); - + // Contract addresses let family_wallet_addr = Address::generate(&env); let remittance_split_addr = Address::generate(&env); @@ -30,7 +30,10 @@ fn main() { // 4. [Write] Execute a complete remittance flow // This coordinates splitting the amount and paying into downstream contracts let total_amount = 5000i128; - println!("Executing complete remittance flow for amount: {}", total_amount); + println!( + "Executing complete remittance flow for amount: {}", + total_amount + ); println!("Orchestrating across:"); println!(" - Savings Goal ID: {}", goal_id); println!(" - Bill ID: {}", bill_id); @@ -38,7 +41,7 @@ fn main() { // In this dry-run example, we show the call signature. // In a full test environment, you would first set up the state in the dependent contracts. - + /* client.execute_remittance_flow( &caller, diff --git a/examples/remittance_split_example.rs b/examples/remittance_split_example.rs index c7bdbb87..e1d0312b 100644 --- a/examples/remittance_split_example.rs +++ b/examples/remittance_split_example.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{Env, Address, testutils::Address as _}; use remittance_split::{RemittanceSplit, RemittanceSplitClient}; +use soroban_sdk::{testutils::Address as _, Address, Env}; fn main() { // 1. Setup the Soroban environment @@ -30,7 +30,10 @@ fn main() { // 6. [Write] Simulate a remittance distribution let total_amount = 1000i128; - println!("\nCalculating allocation for total amount: {}", total_amount); + println!( + "\nCalculating allocation for total amount: {}", + total_amount + ); let allocations = client.calculate_split(&total_amount); println!("Allocations:"); diff --git a/examples/reporting_example.rs b/examples/reporting_example.rs index bb2026be..e9a9ce41 100644 --- a/examples/reporting_example.rs +++ b/examples/reporting_example.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{Env, Address, testutils::Address as _}; -use reporting::{ReportingClient, Category}; +use reporting::{Category, ReportingClient}; +use soroban_sdk::{testutils::Address as _, Address, Env}; // Mock contracts for the reporting example // In a real scenario, these would be the actual deployed contract IDs @@ -17,7 +17,7 @@ fn main() { // 3. Generate mock addresses for dependencies and admin let admin = Address::generate(&env); let user = Address::generate(&env); - + // Dependencies let split_addr = Address::generate(&env); let savings_addr = Address::generate(&env); @@ -33,24 +33,26 @@ fn main() { // 5. [Write] Configure contract addresses println!("Configuring dependency addresses..."); - client.configure_addresses( - &admin, - &split_addr, - &savings_addr, - &bills_addr, - &insurance_addr, - &family_addr - ).unwrap(); + client + .configure_addresses( + &admin, + &split_addr, + &savings_addr, + &bills_addr, + &insurance_addr, + &family_addr, + ) + .unwrap(); println!("Addresses configured successfully!"); // 6. [Read] Generate a mock report - // Note: In this environment, calling reports that query other contracts + // Note: In this environment, calling reports that query other contracts // would require those contracts to be registered at the provided addresses. // For simplicity in this standalone example, we'll focus on the configuration and health score calculation // if the logic allows it without full cross-contract state. - + // However, since we're using Env::default(), we can actually register simple mocks if needed. - // But for a clear "runnable example" that doesn't get too complex, + // But for a clear "runnable example" that doesn't get too complex, // showing the setup and a successful call is the primary goal. println!("\nReporting contract is now ready to generate financial insights."); diff --git a/examples/savings_goals_example.rs b/examples/savings_goals_example.rs index 67bb1dd3..24900e2d 100644 --- a/examples/savings_goals_example.rs +++ b/examples/savings_goals_example.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{Env, Address, String, testutils::Address as _}; use savings_goals::{SavingsGoalContract, SavingsGoalContractClient}; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; fn main() { // 1. Setup the Soroban environment @@ -20,8 +20,13 @@ fn main() { let target_amount = 5000i128; let target_date = env.ledger().timestamp() + 31536000; // 1 year from now - println!("Creating savings goal: '{}' with target: {}", goal_name, target_amount); - let goal_id = client.create_goal(&owner, &goal_name, &target_amount, &target_date).unwrap(); + println!( + "Creating savings goal: '{}' with target: {}", + goal_name, target_amount + ); + let goal_id = client + .create_goal(&owner, &goal_name, &target_amount, &target_date) + .unwrap(); println!("Goal created successfully with ID: {}", goal_id); // 5. [Read] Fetch the goal to check progress diff --git a/family_wallet/Cargo.toml b/family_wallet/Cargo.toml index c7c60148..87b04915 100644 --- a/family_wallet/Cargo.toml +++ b/family_wallet/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" remitwise-common = { path = "../remitwise-common" } [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } testutils = { path = "../testutils" } diff --git a/family_wallet/src/lib.rs b/family_wallet/src/lib.rs index 3e93d959..c9abcbf1 100644 --- a/family_wallet/src/lib.rs +++ b/family_wallet/src/lib.rs @@ -575,7 +575,9 @@ impl FamilyWallet { .get(&symbol_short!("PEND_TXS")) .unwrap_or_else(|| panic!("Pending transactions map not initialized")); - let mut pending_tx = pending_txs.get(tx_id).unwrap_or_else(|| panic!("Transaction not found")); + let mut pending_tx = pending_txs + .get(tx_id) + .unwrap_or_else(|| panic!("Transaction not found")); let current_time = env.ledger().timestamp(); if current_time > pending_tx.expires_at { @@ -1501,7 +1503,9 @@ impl FamilyWallet { .instance() .get(&symbol_short!("MEMBERS")) .unwrap_or_else(|| panic!("Wallet not initialized")); - let member = members.get(caller.clone()).unwrap_or_else(|| panic!("Not a family member")); + let member = members + .get(caller.clone()) + .unwrap_or_else(|| panic!("Not a family member")); if Self::role_has_expired(env, caller) { panic!("Role has expired"); } diff --git a/insurance/Cargo.toml b/insurance/Cargo.toml index 412a27b2..99666a7b 100644 --- a/insurance/Cargo.toml +++ b/insurance/Cargo.toml @@ -7,12 +7,12 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" remitwise-common = { path = "../remitwise-common" } [dev-dependencies] proptest = "1.10.0" -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } testutils = { path = "../testutils" } diff --git a/insurance/src/lib.rs b/insurance/src/lib.rs index ff63d858..18031efd 100644 --- a/insurance/src/lib.rs +++ b/insurance/src/lib.rs @@ -3,7 +3,6 @@ use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Map, String, Symbol, Vec, - contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Map, String, Symbol, Vec, }; use remitwise_common::CoverageType; @@ -81,16 +80,11 @@ pub mod pause_functions { /// Insurance policy data structure with owner tracking for access control #[derive(Clone)] #[contracttype] -#[derive(Clone)] -#[contracttype] -#[derive(Clone)] -#[contracttype] pub struct InsurancePolicy { pub id: u32, pub owner: Address, pub name: String, pub external_ref: Option, - pub coverage_type: String, pub coverage_type: CoverageType, pub monthly_premium: i128, pub coverage_amount: i128, @@ -129,17 +123,7 @@ pub struct PremiumSchedule { pub missed_count: u32, } -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[repr(u32)] -pub enum InsuranceError { - InvalidPremium = 1, - InvalidCoverage = 2, - PolicyNotFound = 3, - PolicyInactive = 4, - Unauthorized = 5, - BatchTooLarge = 6, -} + @@ -243,7 +227,6 @@ impl Insurance { } pub fn pause(env: Env, caller: Address) -> Result<(), InsuranceError> { caller.require_auth(); - let admin = Self::get_pause_admin(&env).unwrap_or_else(|| panic!("No pause admin set")); let admin = Self::get_pause_admin(&env).ok_or(InsuranceError::Unauthorized)?; if admin != caller { return Err(InsuranceError::Unauthorized); @@ -257,7 +240,6 @@ impl Insurance { } pub fn unpause(env: Env, caller: Address) -> Result<(), InsuranceError> { caller.require_auth(); - let admin = Self::get_pause_admin(&env).unwrap_or_else(|| panic!("No pause admin set")); let admin = Self::get_pause_admin(&env).ok_or(InsuranceError::Unauthorized)?; if admin != caller { return Err(InsuranceError::Unauthorized); @@ -376,205 +358,7 @@ impl Insurance { Ok(()) } - // ----------------------------------------------------------------------- - // Tag management - // ----------------------------------------------------------------------- - - fn validate_tags(tags: &Vec) { - if tags.is_empty() { - panic!("Tags cannot be empty"); - } - for tag in tags.iter() { - if tag.len() == 0 || tag.len() > 32 { - panic!("Tag must be between 1 and 32 characters"); - } - } - } - - pub fn add_tags_to_policy( - env: Env, - caller: Address, - policy_id: u32, - tags: Vec, - ) { - caller.require_auth(); - Self::validate_tags(&tags); - Self::extend_instance_ttl(&env); - - let mut policies: Map = env - .storage() - .instance() - .get(&symbol_short!("POLICIES")) - .unwrap_or_else(|| Map::new(&env)); - - let mut policy = policies.get(policy_id).expect("Policy not found"); - - if policy.owner != caller { - panic!("Only the policy owner can add tags"); - } - - for tag in tags.iter() { - policy.tags.push_back(tag); - } - - policies.set(policy_id, policy); - env.storage() - .instance() - .set(&symbol_short!("POLICIES"), &policies); - - env.events().publish( - (symbol_short!("insure"), symbol_short!("tags_add")), - (policy_id, caller, tags), - ); - } - - pub fn remove_tags_from_policy( - env: Env, - caller: Address, - policy_id: u32, - tags: Vec, - ) { - caller.require_auth(); - Self::validate_tags(&tags); - Self::extend_instance_ttl(&env); - - let mut policies: Map = env - .storage() - .instance() - .get(&symbol_short!("POLICIES")) - .unwrap_or_else(|| Map::new(&env)); - - let mut policy = policies.get(policy_id).expect("Policy not found"); - - if policy.owner != caller { - panic!("Only the policy owner can remove tags"); - } - - let mut new_tags = Vec::new(&env); - for existing_tag in policy.tags.iter() { - let mut should_keep = true; - for remove_tag in tags.iter() { - if existing_tag == remove_tag { - should_keep = false; - break; - } - } - if should_keep { - new_tags.push_back(existing_tag); - } - } - - policy.tags = new_tags; - policies.set(policy_id, policy); - env.storage() - .instance() - .set(&symbol_short!("POLICIES"), &policies); - - env.events().publish( - (symbol_short!("insure"), symbol_short!("tags_rem")), - (policy_id, caller, tags), - ); - } - - // ----------------------------------------------------------------------- - // Tag management - // ----------------------------------------------------------------------- - - fn validate_tags(tags: &Vec) { - if tags.is_empty() { - panic!("Tags cannot be empty"); - } - for tag in tags.iter() { - if tag.len() == 0 || tag.len() > 32 { - panic!("Tag must be between 1 and 32 characters"); - } - } - } - - pub fn add_tags_to_policy( - env: Env, - caller: Address, - policy_id: u32, - tags: Vec, - ) { - caller.require_auth(); - Self::validate_tags(&tags); - Self::extend_instance_ttl(&env); - - let mut policies: Map = env - .storage() - .instance() - .get(&symbol_short!("POLICIES")) - .unwrap_or_else(|| Map::new(&env)); - - let mut policy = policies.get(policy_id).expect("Policy not found"); - - if policy.owner != caller { - panic!("Only the policy owner can add tags"); - } - - for tag in tags.iter() { - policy.tags.push_back(tag); - } - - policies.set(policy_id, policy); - env.storage() - .instance() - .set(&symbol_short!("POLICIES"), &policies); - - env.events().publish( - (symbol_short!("insure"), symbol_short!("tags_add")), - (policy_id, caller, tags), - ); - } - - pub fn remove_tags_from_policy( - env: Env, - caller: Address, - policy_id: u32, - tags: Vec, - ) { - caller.require_auth(); - Self::validate_tags(&tags); - Self::extend_instance_ttl(&env); - let mut policies: Map = env - .storage() - .instance() - .get(&symbol_short!("POLICIES")) - .unwrap_or_else(|| Map::new(&env)); - - let mut policy = policies.get(policy_id).expect("Policy not found"); - - if policy.owner != caller { - panic!("Only the policy owner can remove tags"); - } - - let mut new_tags = Vec::new(&env); - for existing_tag in policy.tags.iter() { - let mut should_keep = true; - for remove_tag in tags.iter() { - if existing_tag == remove_tag { - should_keep = false; - break; - } - } - if should_keep { - new_tags.push_back(existing_tag); - } - } - - policy.tags = new_tags; - policies.set(policy_id, policy); - env.storage() - .instance() - .set(&symbol_short!("POLICIES"), &policies); - - env.events().publish( - (symbol_short!("insure"), symbol_short!("tags_rem")), - (policy_id, caller, tags), - ); - } // ----------------------------------------------------------------------- // Tag management @@ -706,7 +490,6 @@ impl Insurance { monthly_premium: i128, coverage_amount: i128, external_ref: Option, - ) -> u32 { ) -> Result { owner.require_auth(); Self::require_not_paused(&env, pause_functions::CREATE_POLICY)?; @@ -772,7 +555,6 @@ impl Insurance { env.events().publish( (symbol_short!("insure"), InsuranceEvent::PolicyCreated), (next_id, policy_owner, policy_external_ref), - (next_id, owner), ); Ok(next_id) @@ -829,7 +611,6 @@ impl Insurance { }; env.events().publish((PREMIUM_PAID,), event); - policies.set(policy_id, policy); policies.set(policy_id, policy.clone()); env.storage() .instance() @@ -885,7 +666,6 @@ impl Insurance { let current_time = env.ledger().timestamp(); let mut paid_count = 0; for id in policy_ids.iter() { - let mut policy = policies.get(id).unwrap_or_else(|| panic!("Policy not found")); let mut policy = policies_map.get(id).unwrap(); policy.next_payment_date = current_time + (30 * 86400); let event = PremiumPaidEvent { @@ -1008,7 +788,6 @@ impl Insurance { .get(&symbol_short!("POLICIES")) .unwrap_or_else(|| Map::new(&env)); - let mut policy = policies.get(policy_id).unwrap_or_else(|| panic!("Policy not found")); let mut policy = policies .get(policy_id) .ok_or(InsuranceError::PolicyNotFound)?; @@ -1019,8 +798,7 @@ impl Insurance { let was_active = policy.active; policy.active = false; - let policy_external_ref = policy.external_ref.clone(); - policies.set(policy_id, policy); + let _policy_external_ref = policy.external_ref.clone(); let premium_amount = policy.monthly_premium; policies.set(policy_id, policy.clone()); env.storage() @@ -1036,12 +814,8 @@ impl Insurance { timestamp: env.ledger().timestamp(), }; env.events().publish((POLICY_DEACTIVATED,), event); - env.events().publish( - (symbol_short!("insure"), InsuranceEvent::PolicyDeactivated), - (policy_id, caller, policy_external_ref), - ); - true + Ok(true) } /// Set or clear an external reference ID for a policy @@ -1086,11 +860,9 @@ impl Insurance { env.events().publish( (symbol_short!("insure"), InsuranceEvent::ExternalRefUpdated), (policy_id, caller, external_ref), - (symbol_short!("insuranc"), InsuranceEvent::PolicyDeactivated), - (policy_id, caller), ); - Ok(true) + true } /// Extend the TTL of instance storage @@ -1130,70 +902,41 @@ impl Insurance { // ----------------------------------------------------------------------- pub fn create_premium_schedule( env: Env, - owner: Address, + caller: Address, policy_id: u32, next_due: u64, interval: u64, ) -> Result { - // Changed to Result - owner.require_auth(); + caller.require_auth(); Self::require_not_paused(&env, pause_functions::CREATE_SCHED)?; - let name = String::from_str(&env, "Health Insurance"); - let coverage_type = String::from_str(&env, "health"); - let monthly_premium = 100; - let coverage_amount = 10000; - let external_ref = Some(String::from_str(&env, "POLICY-EXT-1")); + let current_time = env.ledger().timestamp(); + if next_due <= current_time { + return Err(InsuranceError::InvalidTimestamp); + } + + Self::extend_instance_ttl(&env); - let policy_id = client.create_policy( - &owner, - &name, - &coverage_type, - &monthly_premium, - &coverage_amount, - &external_ref, - ); let mut policies: Map = env .storage() .instance() .get(&symbol_short!("POLICIES")) .unwrap_or_else(|| Map::new(&env)); - let mut policy = policies.get(policy_id).unwrap_or_else(|| panic!("Policy not found")); let mut policy = policies .get(policy_id) .ok_or(InsuranceError::PolicyNotFound)?; - if policy.owner != owner { + if policy.owner != caller { return Err(InsuranceError::Unauthorized); } - let policy = client.get_policy(&policy_id).unwrap(); - assert_eq!(policy.id, 1); - assert_eq!(policy.owner, owner); - assert_eq!(policy.name, name); - assert_eq!(policy.external_ref, external_ref); - assert_eq!(policy.coverage_type, coverage_type); - assert_eq!(policy.monthly_premium, monthly_premium); - assert_eq!(policy.coverage_amount, coverage_amount); - assert!(policy.active); - assert_eq!(policy.next_payment_date, 1000000000 + (30 * 86400)); - } - let current_time = env.ledger().timestamp(); - if next_due <= current_time { - return Err(InsuranceError::InvalidTimestamp); - } - - Self::extend_instance_ttl(&env); - let mut schedules: Map = env .storage() .instance() .get(&symbol_short!("PREM_SCH")) .unwrap_or_else(|| Map::new(&env)); - client.create_policy(&owner, &name, &coverage_type, &0, &10000, &None); - } let next_schedule_id = env .storage() .instance() @@ -1203,7 +946,7 @@ impl Insurance { let schedule = PremiumSchedule { id: next_schedule_id, - owner: owner.clone(), + owner: caller.clone(), policy_id, next_due, interval, @@ -1216,8 +959,6 @@ impl Insurance { policy.schedule_id = Some(next_schedule_id); - client.create_policy(&owner, &name, &coverage_type, &-100, &10000, &None); - } schedules.set(next_schedule_id, schedule); env.storage() .instance() @@ -1233,7 +974,7 @@ impl Insurance { env.events().publish( (symbol_short!("insure"), InsuranceEvent::ScheduleCreated), - (next_schedule_id, owner), + (next_schedule_id, caller), ); Ok(next_schedule_id) @@ -1264,7 +1005,6 @@ impl Insurance { .get(&symbol_short!("PREM_SCH")) .unwrap_or_else(|| Map::new(&env)); - let mut schedule = schedules.get(schedule_id).unwrap_or_else(|| panic!("Schedule not found")); let mut schedule = schedules .get(schedule_id) .ok_or(InsuranceError::PolicyNotFound)?; @@ -1307,7 +1047,6 @@ impl Insurance { .get(&symbol_short!("PREM_SCH")) .unwrap_or_else(|| Map::new(&env)); - let mut schedule = schedules.get(schedule_id).unwrap_or_else(|| panic!("Schedule not found")); let mut schedule = schedules .get(schedule_id) .ok_or(InsuranceError::PolicyNotFound)?; @@ -1486,11 +1225,13 @@ mod test_events { assert_eq!(page.next_cursor, 0); } - client.create_policy(&owner, &name, &coverage_type, &100, &0, &None); #[test] fn test_get_active_policies_single_page() { let env = make_env(); env.mock_all_auths(); + let contract_id = env.register_contract(None, Insurance); + let client = InsuranceClient::new(&env, &contract_id); + let owner = Address::generate(&env); // Use the .try_ version of the function to capture the error result let result = client.try_create_policy( @@ -1554,30 +1295,7 @@ mod test_events { assert_eq!(page3.count, 1); assert_eq!(page3.next_cursor, 0); } - // Create a policy - let policy_id = client.create_policy( - &owner, - &String::from_str(&env, "Emergency Coverage"), - &String::from_str(&env, "emergency"), - &75, - &25000, - ); - env.mock_all_auths(); - - let name = String::from_str(&env, "Health Insurance"); - let coverage_type = String::from_str(&env, "health"); - let policy_id = client.create_policy(&owner, &name, &coverage_type, &100, &10000, &None); - let ids = setup_policies(&env, &client, &owner, 4); - // Deactivate policy #2 - client.deactivate_policy(&owner, &ids.get(1).unwrap()); - - let page = client.get_active_policies(&owner, &0, &10); - assert_eq!(page.count, 3); // only 3 active - for p in page.items.iter() { - assert!(p.active, "only active policies should be returned"); - } - } #[test] fn test_get_active_policies_multi_owner_isolation() { @@ -1638,42 +1356,9 @@ mod test_events { let client = InsuranceClient::new(&env, &contract_id); let owner = Address::generate(&env); - // Create multiple policies - let name1 = String::from_str(&env, "Health Insurance"); - let coverage_type1 = String::from_str(&env, "health"); - let policy_id1 = client.create_policy(&owner, &name1, &coverage_type1, &100, &10000, &None); - - let name2 = String::from_str(&env, "Emergency Insurance"); - let coverage_type2 = String::from_str(&env, "emergency"); - let policy_id2 = client.create_policy(&owner, &name2, &coverage_type2, &200, &20000, &None); - - let name3 = String::from_str(&env, "Life Insurance"); - let coverage_type3 = String::from_str(&env, "life"); - let policy_id3 = client.create_policy(&owner, &name3, &coverage_type3, &300, &30000, &None); - let policy_id = client.create_policy( - client.create_policy( - &owner, - &String::from_str(&env, "Health Insurance"), - &CoverageType::Health, - &String::from_str(&env, "Policy 1"), - &String::from_str(&env, "health"), - &100, - &50000, - ); - client.create_policy( - &owner, - &String::from_str(&env, "Policy 2"), - &String::from_str(&env, "life"), - &200, - &100000, - ); - client.create_policy( - &owner, - &String::from_str(&env, "Policy 3"), - &String::from_str(&env, "emergency"), - &75, - &25000, - ); + let name = String::from_str(&env, "Health Insurance"); + let coverage_type = CoverageType::Health; + let policy_id = client.create_policy(&owner, &name, &coverage_type, &100, &50000, &None); client.pay_premium(&owner, &policy_id); @@ -1883,8 +1568,6 @@ mod test_events { } #[test] - fn test_multiple_policies_management() { - let env = create_test_env(); fn test_policy_data_persists_across_ledger_advancements() { let env = Env::default(); env.mock_all_auths(); @@ -1913,19 +1596,7 @@ mod test_events { &75000, ); - for (i, policy_name) in policy_names.iter().enumerate() { - let premium = ((i + 1) as i128) * 100; - let coverage = ((i + 1) as i128) * 10000; - let policy_id = client.create_policy( - &owner, - policy_name, - &coverage_type, - &premium, - &coverage, - &None, - ); - policy_ids.push_back(policy_id); - } + // Phase 2: Advance to seq 510,000 (TTL = 8,500 < 17,280) env.ledger().set(LedgerInfo { protocol_version: 20, diff --git a/insurance/tests/stress_tests.rs b/insurance/tests/stress_tests.rs index 0063a6ad..4b001787 100644 --- a/insurance/tests/stress_tests.rs +++ b/insurance/tests/stress_tests.rs @@ -106,11 +106,18 @@ fn stress_200_policies_single_user() { cursor = page.next_cursor; } - assert_eq!(collected, 200, "Pagination must return all 200 active policies"); + assert_eq!( + collected, 200, + "Pagination must return all 200 active policies" + ); // get_active_policies sets next_cursor = last_returned_id; when a page is exactly // full the caller receives a non-zero cursor that produces a trailing empty page, // so the round-trip count is pages = ceil(200/50) + 1 trailing = 5. - assert!(pages >= 4 && pages <= 5, "Expected 4-5 pages for 200 policies at limit 50, got {}", pages); + assert!( + pages >= 4 && pages <= 5, + "Expected 4-5 pages for 200 policies at limit 50, got {}", + pages + ); } /// Create 200 policies and verify instance TTL remains valid after the instance diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index db4f4f5b..6b8c5882 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -5,11 +5,11 @@ edition = "2021" publish = false [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" remittance_split = { path = "../remittance_split" } savings_goals = { path = "../savings_goals" } bill_payments = { path = "../bill_payments" } insurance = { path = "../insurance" } [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } diff --git a/integration_tests/tests/multi_contract_integration.rs b/integration_tests/tests/multi_contract_integration.rs index f035891e..91b7cf0f 100644 --- a/integration_tests/tests/multi_contract_integration.rs +++ b/integration_tests/tests/multi_contract_integration.rs @@ -266,7 +266,7 @@ fn test_multiple_entities_creation() { /// to the shared `RemitwiseEvents` helper. #[test] fn test_event_topic_compliance_across_contracts() { - use soroban_sdk::{symbol_short, Vec, IntoVal}; + use soroban_sdk::{symbol_short, IntoVal, Vec}; let env = Env::default(); env.mock_all_auths(); @@ -290,7 +290,12 @@ fn test_event_topic_compliance_across_contracts() { remittance_client.initialize_split(&user, &0u64, &40u32, &30u32, &20u32, &10u32); let goal_name = SorobanString::from_str(&env, "Compliance Goal"); - let _ = savings_client.create_goal(&user, &goal_name, &1000i128, &(env.ledger().timestamp() + 86400)); + let _ = savings_client.create_goal( + &user, + &goal_name, + &1000i128, + &(env.ledger().timestamp() + 86400), + ); let bill_name = SorobanString::from_str(&env, "Compliance Bill"); let _ = bills_client.create_bill( @@ -309,7 +314,10 @@ fn test_event_topic_compliance_across_contracts() { // Collect published events let events = env.events().all(); - assert!(events.len() > 0, "No events were emitted by the sample actions"); + assert!( + events.len() > 0, + "No events were emitted by the sample actions" + ); // Validate each event's topics conform to Remitwise schema let mut non_compliant = Vec::new(&env); @@ -317,7 +325,8 @@ fn test_event_topic_compliance_across_contracts() { for ev in events.iter() { let topics = &ev.1; // Expect topics to be a vector of length 4 starting with symbol_short!("Remitwise") - let ok = topics.len() == 4 && topics.get(0).unwrap() == symbol_short!("Remitwise").into_val(&env); + let ok = topics.len() == 4 + && topics.get(0).unwrap() == symbol_short!("Remitwise").into_val(&env); if !ok { non_compliant.push_back(ev.clone()); } diff --git a/orchestrator/Cargo.toml b/orchestrator/Cargo.toml index 82b00378..e1a28c66 100644 --- a/orchestrator/Cargo.toml +++ b/orchestrator/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } testutils = { path = "../testutils" } diff --git a/remittance_split/Cargo.toml b/remittance_split/Cargo.toml index d69e465c..6942cf54 100644 --- a/remittance_split/Cargo.toml +++ b/remittance_split/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } testutils = { path = "../testutils" } diff --git a/remittance_split/src/test.rs b/remittance_split/src/test.rs index 93e1753c..86336aae 100644 --- a/remittance_split/src/test.rs +++ b/remittance_split/src/test.rs @@ -2,8 +2,8 @@ use super::*; use soroban_sdk::{ - testutils::{Address as AddressTrait, Events, Ledger}, testutils::storage::Instance as StorageInstance, + testutils::{Address as AddressTrait, Events, Ledger}, token::{StellarAssetClient, TokenClient}, Address, Env, Symbol, TryFromVal, }; @@ -15,7 +15,9 @@ use soroban_sdk::{ /// Register a native Stellar asset (SAC) and return (contract_id, admin). /// The admin is the issuer; we mint `amount` to `recipient`. fn setup_token(env: &Env, admin: &Address, recipient: &Address, amount: i128) -> Address { - let token_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); + let token_id = env + .register_stellar_asset_contract_v2(admin.clone()) + .address(); let sac = StellarAssetClient::new(env, &token_id); sac.mint(recipient, &amount); token_id @@ -68,7 +70,10 @@ fn test_initialize_split_invalid_sum() { let token_id = setup_token(&env, &token_admin, &owner, 0); let result = client.try_initialize_split(&owner, &0, &token_id, &50, &50, &10, &0); - assert_eq!(result, Err(Ok(RemittanceSplitError::PercentagesDoNotSumTo100))); + assert_eq!( + result, + Err(Ok(RemittanceSplitError::PercentagesDoNotSumTo100)) + ); } #[test] @@ -163,7 +168,10 @@ fn test_update_split_percentages_must_sum_to_100() { client.initialize_split(&owner, &0, &token_id, &50, &30, &15, &5); let result = client.try_update_split(&owner, &1, &60, &30, &15, &5); - assert_eq!(result, Err(Ok(RemittanceSplitError::PercentagesDoNotSumTo100))); + assert_eq!( + result, + Err(Ok(RemittanceSplitError::PercentagesDoNotSumTo100)) + ); } // --------------------------------------------------------------------------- @@ -377,7 +385,10 @@ fn test_distribute_usdc_untrusted_token_rejected() { let evil_token = Address::generate(&env); let accounts = make_accounts(&env); let result = client.try_distribute_usdc(&evil_token, &owner, &1, &accounts, &1_000); - assert_eq!(result, Err(Ok(RemittanceSplitError::UntrustedTokenContract))); + assert_eq!( + result, + Err(Ok(RemittanceSplitError::UntrustedTokenContract)) + ); } // --------------------------------------------------------------------------- @@ -404,7 +415,10 @@ fn test_distribute_usdc_self_transfer_spending_rejected() { insurance: Address::generate(&env), }; let result = client.try_distribute_usdc(&token_id, &owner, &1, &accounts, &1_000); - assert_eq!(result, Err(Ok(RemittanceSplitError::SelfTransferNotAllowed))); + assert_eq!( + result, + Err(Ok(RemittanceSplitError::SelfTransferNotAllowed)) + ); } #[test] @@ -426,7 +440,10 @@ fn test_distribute_usdc_self_transfer_savings_rejected() { insurance: Address::generate(&env), }; let result = client.try_distribute_usdc(&token_id, &owner, &1, &accounts, &1_000); - assert_eq!(result, Err(Ok(RemittanceSplitError::SelfTransferNotAllowed))); + assert_eq!( + result, + Err(Ok(RemittanceSplitError::SelfTransferNotAllowed)) + ); } #[test] @@ -448,7 +465,10 @@ fn test_distribute_usdc_self_transfer_bills_rejected() { insurance: Address::generate(&env), }; let result = client.try_distribute_usdc(&token_id, &owner, &1, &accounts, &1_000); - assert_eq!(result, Err(Ok(RemittanceSplitError::SelfTransferNotAllowed))); + assert_eq!( + result, + Err(Ok(RemittanceSplitError::SelfTransferNotAllowed)) + ); } #[test] @@ -470,7 +490,10 @@ fn test_distribute_usdc_self_transfer_insurance_rejected() { insurance: owner.clone(), }; let result = client.try_distribute_usdc(&token_id, &owner, &1, &accounts, &1_000); - assert_eq!(result, Err(Ok(RemittanceSplitError::SelfTransferNotAllowed))); + assert_eq!( + result, + Err(Ok(RemittanceSplitError::SelfTransferNotAllowed)) + ); } // --------------------------------------------------------------------------- @@ -665,9 +688,9 @@ fn test_distribute_usdc_multiple_rounds() { let token = TokenClient::new(&env, &token_id); assert_eq!(token.balance(&accounts.spending), 1_500); // 3 * 500 - assert_eq!(token.balance(&accounts.savings), 900); // 3 * 300 - assert_eq!(token.balance(&accounts.bills), 450); // 3 * 150 - assert_eq!(token.balance(&accounts.insurance), 150); // 3 * 50 + assert_eq!(token.balance(&accounts.savings), 900); // 3 * 300 + assert_eq!(token.balance(&accounts.bills), 450); // 3 * 150 + assert_eq!(token.balance(&accounts.insurance), 150); // 3 * 50 assert_eq!(token.balance(&owner), 0); } @@ -861,5 +884,8 @@ fn test_instance_ttl_extended_on_initialize_split() { client.initialize_split(&owner, &0, &token_id, &50, &30, &15, &5); let ttl = env.as_contract(&contract_id, || env.storage().instance().get_ttl()); - assert!(ttl >= 518_400, "TTL must be >= INSTANCE_BUMP_AMOUNT after init"); + assert!( + ttl >= 518_400, + "TTL must be >= INSTANCE_BUMP_AMOUNT after init" + ); } diff --git a/remittance_split/tests/fuzz_tests.rs b/remittance_split/tests/fuzz_tests.rs index 7150aaf9..aa6cd358 100644 --- a/remittance_split/tests/fuzz_tests.rs +++ b/remittance_split/tests/fuzz_tests.rs @@ -81,7 +81,11 @@ fn fuzz_calculate_split_sum_preservation() { let amounts = client.calculate_split(&total_amount); let sum: i128 = amounts.iter().sum(); - assert_eq!(sum, total_amount, "Sum mismatch for percentages {}%/{}%/{}%/{}%", sp, sg, sb, si); + assert_eq!( + sum, total_amount, + "Sum mismatch for percentages {}%/{}%/{}%/{}%", + sp, sg, sb, si + ); assert!(amounts.iter().all(|a| a >= 0), "Negative amount detected"); } } @@ -126,7 +130,11 @@ fn fuzz_rounding_behavior() { for amount in &[100i128, 1000, 9999, 123456] { let amounts = client.calculate_split(amount); let sum: i128 = amounts.iter().sum(); - assert_eq!(sum, *amount, "Rounding error for amount {} with {}%/{}%/{}%/{}%", amount, sp, sg, sb, si); + assert_eq!( + sum, *amount, + "Rounding error for amount {} with {}%/{}%/{}%/{}%", + amount, sp, sg, sb, si + ); } } } @@ -167,7 +175,11 @@ fn fuzz_invalid_percentages() { let total = sp + sg + sb + si; let result = try_init(&client, &env, &owner, sp, sg, sb, si); if total != 100 { - assert!(result.is_err(), "Expected error for percentages summing to {}", total); + assert!( + result.is_err(), + "Expected error for percentages summing to {}", + total + ); } } } @@ -182,7 +194,12 @@ fn fuzz_large_amounts() { init(&client, &env, &owner, 25, 25, 25, 25); - for amount in &[i128::MAX / 1000, i128::MAX / 100, 1_000_000_000_000i128, 999_999_999_999i128] { + for amount in &[ + i128::MAX / 1000, + i128::MAX / 100, + 1_000_000_000_000i128, + 999_999_999_999i128, + ] { if let Ok(_) = client.try_calculate_split(amount) { let amounts = client.calculate_split(amount); let sum: i128 = amounts.iter().sum(); @@ -213,9 +230,17 @@ fn fuzz_single_category_splits() { let sum: i128 = amounts.iter().sum(); assert_eq!(sum, 1000); - if sp == 100 { assert_eq!(amounts.get(0).unwrap(), 1000); } - if sg == 100 { assert_eq!(amounts.get(1).unwrap(), 1000); } - if sb == 100 { assert_eq!(amounts.get(2).unwrap(), 1000); } - if si == 100 { assert_eq!(amounts.get(3).unwrap(), 1000); } + if sp == 100 { + assert_eq!(amounts.get(0).unwrap(), 1000); + } + if sg == 100 { + assert_eq!(amounts.get(1).unwrap(), 1000); + } + if sb == 100 { + assert_eq!(amounts.get(2).unwrap(), 1000); + } + if si == 100 { + assert_eq!(amounts.get(3).unwrap(), 1000); + } } } diff --git a/remittance_split/tests/stress_test_large_amounts.rs b/remittance_split/tests/stress_test_large_amounts.rs index 1de9d05d..355b0235 100644 --- a/remittance_split/tests/stress_test_large_amounts.rs +++ b/remittance_split/tests/stress_test_large_amounts.rs @@ -10,7 +10,15 @@ fn dummy_token(env: &Env) -> Address { Address::generate(env) } -fn init(client: &RemittanceSplitClient, env: &Env, owner: &Address, s: u32, g: u32, b: u32, i: u32) { +fn init( + client: &RemittanceSplitClient, + env: &Env, + owner: &Address, + s: u32, + g: u32, + b: u32, + i: u32, +) { let token = dummy_token(env); client.initialize_split(owner, &0, &token, &s, &g, &b, &i); } @@ -174,7 +182,13 @@ fn test_sequential_large_calculations() { init(&client, &env, &owner, 50, 30, 15, 5); - for amount in &[i128::MAX / 1000, i128::MAX / 500, i128::MAX / 200, i128::MAX / 150, i128::MAX / 100] { + for amount in &[ + i128::MAX / 1000, + i128::MAX / 500, + i128::MAX / 200, + i128::MAX / 150, + i128::MAX / 100, + ] { let result = client.try_calculate_split(amount); assert!(result.is_ok(), "Failed for amount: {}", amount); let splits = result.unwrap().unwrap(); @@ -195,7 +209,11 @@ fn test_checked_arithmetic_prevents_silent_overflow() { for amount in &[i128::MAX / 40, i128::MAX / 30, i128::MAX] { let result = client.try_calculate_split(amount); - assert!(result.is_err(), "Should have detected overflow for amount: {}", amount); + assert!( + result.is_err(), + "Should have detected overflow for amount: {}", + amount + ); } } diff --git a/remitwise-common/Cargo.toml b/remitwise-common/Cargo.toml index 27a93bcb..f803d981 100644 --- a/remitwise-common/Cargo.toml +++ b/remitwise-common/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" [lib] crate-type = ["cdylib", "rlib"] \ No newline at end of file diff --git a/reporting/Cargo.toml b/reporting/Cargo.toml index 8fe34d3d..57e6654d 100644 --- a/reporting/Cargo.toml +++ b/reporting/Cargo.toml @@ -7,9 +7,9 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" remitwise-common = { path = "../remitwise-common" } [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } testutils = { path = "../testutils" } diff --git a/reporting/src/lib.rs b/reporting/src/lib.rs index 189bd209..e5e1e1e1 100644 --- a/reporting/src/lib.rs +++ b/reporting/src/lib.rs @@ -294,14 +294,6 @@ pub struct PolicyPage { pub count: u32, } -#[contracttype] -#[derive(Clone)] -pub struct PolicyPage { - pub items: Vec, - pub next_cursor: u32, - pub count: u32, -} - #[contract] pub struct ReportingContract; diff --git a/reporting/src/tests.rs b/reporting/src/tests.rs index 91820601..bf13f5e1 100644 --- a/reporting/src/tests.rs +++ b/reporting/src/tests.rs @@ -1,4 +1,4 @@ -use testutils::{set_ledger_time}; +use testutils::set_ledger_time; // Mock contracts for testing mod remittance_split { diff --git a/savings_goals/Cargo.toml b/savings_goals/Cargo.toml index 0ffb4e5e..05f76dee 100644 --- a/savings_goals/Cargo.toml +++ b/savings_goals/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.1.1" [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } testutils = { path = "../testutils" } diff --git a/savings_goals/src/lib.rs b/savings_goals/src/lib.rs index 9ebdfc45..f1446b9c 100644 --- a/savings_goals/src/lib.rs +++ b/savings_goals/src/lib.rs @@ -88,7 +88,7 @@ pub struct SavingsSchedule { } #[contracttype] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum SavingsGoalsError { InvalidAmount = 1, GoalNotFound = 2, @@ -296,7 +296,9 @@ impl SavingsGoalContract { pub fn pause(env: Env, caller: Address) { caller.require_auth(); - let admin = Self::get_pause_admin(&env).ok_or(SavingsGoalsError::Unauthorized).unwrap(); + let admin = Self::get_pause_admin(&env) + .ok_or(SavingsGoalsError::Unauthorized) + .unwrap(); if admin != caller { panic!("Unauthorized"); } @@ -309,7 +311,9 @@ impl SavingsGoalContract { pub fn unpause(env: Env, caller: Address) { caller.require_auth(); - let admin = Self::get_pause_admin(&env).ok_or(SavingsGoalsError::Unauthorized).unwrap(); + let admin = Self::get_pause_admin(&env) + .ok_or(SavingsGoalsError::Unauthorized) + .unwrap(); if admin != caller { panic!("Unauthorized"); } @@ -329,7 +333,9 @@ impl SavingsGoalContract { pub fn pause_function(env: Env, caller: Address, func: Symbol) { caller.require_auth(); - let admin = Self::get_pause_admin(&env).ok_or(SavingsGoalsError::Unauthorized).unwrap(); + let admin = Self::get_pause_admin(&env) + .ok_or(SavingsGoalsError::Unauthorized) + .unwrap(); if admin != caller { panic!("Unauthorized"); } @@ -346,7 +352,9 @@ impl SavingsGoalContract { pub fn unpause_function(env: Env, caller: Address, func: Symbol) { caller.require_auth(); - let admin = Self::get_pause_admin(&env).ok_or(SavingsGoalsError::Unauthorized).unwrap(); + let admin = Self::get_pause_admin(&env) + .ok_or(SavingsGoalsError::Unauthorized) + .unwrap(); if admin != caller { panic!("Unauthorized"); } @@ -427,12 +435,7 @@ impl SavingsGoalContract { } } - pub fn add_tags_to_goal( - env: Env, - caller: Address, - goal_id: u32, - tags: Vec, - ) { + pub fn add_tags_to_goal(env: Env, caller: Address, goal_id: u32, tags: Vec) { caller.require_auth(); Self::validate_tags(&tags); Self::extend_instance_ttl(&env); @@ -467,12 +470,7 @@ impl SavingsGoalContract { Self::append_audit(&env, symbol_short!("add_tags"), &caller, true); } - pub fn remove_tags_from_goal( - env: Env, - caller: Address, - goal_id: u32, - tags: Vec, - ) { + pub fn remove_tags_from_goal(env: Env, caller: Address, goal_id: u32, tags: Vec) { caller.require_auth(); Self::validate_tags(&tags); Self::extend_instance_ttl(&env); @@ -740,12 +738,10 @@ impl SavingsGoalContract { if goal.owner != caller { panic!("Batch validation failed"); } - goal.current_amount = match goal - .current_amount - .checked_add(item.amount) { - Some(v) => v, - None => panic!("overflow"), - }; + goal.current_amount = match goal.current_amount.checked_add(item.amount) { + Some(v) => v, + None => panic!("overflow"), + }; let new_total = goal.current_amount; let was_completed = new_total >= goal.target_amount; let previously_completed = (new_total - item.amount) >= goal.target_amount; @@ -1033,7 +1029,9 @@ impl SavingsGoalContract { let mut result = Vec::new(&env); for i in start_index..end_index { - let goal_id = ids.get(i).unwrap_or_else(|| panic!("Pagination index out of sync")); + let goal_id = ids + .get(i) + .unwrap_or_else(|| panic!("Pagination index out of sync")); let goal = goals .get(goal_id) .unwrap_or_else(|| panic!("Pagination index out of sync")); @@ -1506,12 +1504,10 @@ impl SavingsGoalContract { } if let Some(mut goal) = goals.get(schedule.goal_id) { - goal.current_amount = match goal - .current_amount - .checked_add(schedule.amount) { - Some(v) => v, - None => panic!("overflow"), - }; + goal.current_amount = match goal.current_amount.checked_add(schedule.amount) { + Some(v) => v, + None => panic!("overflow"), + }; let is_completed = goal.current_amount >= goal.target_amount; goals.set(schedule.goal_id, goal.clone()); diff --git a/savings_goals/src/test.rs b/savings_goals/src/test.rs index 25153e33..cbc6d7cb 100644 --- a/savings_goals/src/test.rs +++ b/savings_goals/src/test.rs @@ -95,7 +95,11 @@ fn test_init_idempotent_does_not_wipe_goals() { assert_eq!(goal_after_second_init.current_amount, 0); let all_goals = client.get_all_goals(&owner_a); - assert_eq!(all_goals.len(), 1, "get_all_goals must still return the one goal"); + assert_eq!( + all_goals.len(), + 1, + "get_all_goals must still return the one goal" + ); // Verify NEXT_ID was not reset: next created goal must get goal_id == 2, not 1 let name2 = String::from_str(&env, "Second Goal"); @@ -1819,9 +1823,18 @@ fn test_get_all_goals_filters_by_owner() { // Verify goal IDs for owner_a are correct let goal_a_ids: Vec = goals_a.iter().map(|g| g.id).collect(); - assert!(goal_a_ids.contains(&goal_a1), "Goals for A should contain goal_a1"); - assert!(goal_a_ids.contains(&goal_a2), "Goals for A should contain goal_a2"); - assert!(goal_a_ids.contains(&goal_a3), "Goals for A should contain goal_a3"); + assert!( + goal_a_ids.contains(&goal_a1), + "Goals for A should contain goal_a1" + ); + assert!( + goal_a_ids.contains(&goal_a2), + "Goals for A should contain goal_a2" + ); + assert!( + goal_a_ids.contains(&goal_a3), + "Goals for A should contain goal_a3" + ); // Get all goals for owner_b let goals_b = client.get_all_goals(&owner_b); @@ -1838,8 +1851,14 @@ fn test_get_all_goals_filters_by_owner() { // Verify goal IDs for owner_b are correct let goal_b_ids: Vec = goals_b.iter().map(|g| g.id).collect(); - assert!(goal_b_ids.contains(&goal_b1), "Goals for B should contain goal_b1"); - assert!(goal_b_ids.contains(&goal_b2), "Goals for B should contain goal_b2"); + assert!( + goal_b_ids.contains(&goal_b1), + "Goals for B should contain goal_b1" + ); + assert!( + goal_b_ids.contains(&goal_b2), + "Goals for B should contain goal_b2" + ); // Verify that goal IDs between owner_a and owner_b are disjoint for goal_a_id in &goal_a_ids { diff --git a/savings_goals/tests/stress_tests.rs b/savings_goals/tests/stress_tests.rs index 85569330..714e1018 100644 --- a/savings_goals/tests/stress_tests.rs +++ b/savings_goals/tests/stress_tests.rs @@ -82,7 +82,11 @@ fn stress_200_goals_single_user() { // Verify via get_all_goals (unbounded) let all_goals = client.get_all_goals(&owner); - assert_eq!(all_goals.len(), 200, "get_all_goals must return all 200 goals"); + assert_eq!( + all_goals.len(), + 200, + "get_all_goals must return all 200 goals" + ); // Verify via paginated get_goals (MAX_PAGE_LIMIT = 50 → 4 pages) let mut collected = 0u32; @@ -103,11 +107,18 @@ fn stress_200_goals_single_user() { cursor = page.next_cursor; } - assert_eq!(collected, 200, "Paginated get_goals must return all 200 goals"); + assert_eq!( + collected, 200, + "Paginated get_goals must return all 200 goals" + ); // get_goals sets next_cursor = last_returned_id; when a page is exactly full the // caller receives a non-zero cursor that produces a trailing empty page, so the // number of round-trips is pages = ceil(200/50) + 1 trailing = 5. - assert!(pages >= 4 && pages <= 5, "Expected 4-5 pages for 200 goals at limit 50, got {}", pages); + assert!( + pages >= 4 && pages <= 5, + "Expected 4-5 pages for 200 goals at limit 50, got {}", + pages + ); } /// Create 200 goals and verify instance TTL stays valid after the instance Map @@ -404,7 +415,10 @@ fn stress_data_persists_across_multiple_ledger_advancements() { // TTL must still be positive let ttl = env.as_contract(&contract_id, || env.storage().instance().get_ttl()); - assert!(ttl > 0, "Instance TTL must be > 0 after all ledger advancements"); + assert!( + ttl > 0, + "Instance TTL must be > 0 after all ledger advancements" + ); } // --------------------------------------------------------------------------- diff --git a/scenarios/Cargo.toml b/scenarios/Cargo.toml index e7a4f5d0..0257f73b 100644 --- a/scenarios/Cargo.toml +++ b/scenarios/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] } testutils = { path = "../testutils" } remittance_split = { path = "../remittance_split" } savings_goals = { path = "../savings_goals" } diff --git a/scenarios/src/lib.rs b/scenarios/src/lib.rs index 57b22e96..5bed3a83 100644 --- a/scenarios/src/lib.rs +++ b/scenarios/src/lib.rs @@ -1,8 +1,8 @@ pub mod tests { - use soroban_sdk::Env; - use testutils::set_ledger_time; use soroban_sdk::testutils::{Ledger, LedgerInfo}; use soroban_sdk::Env; + use soroban_sdk::Env; + use testutils::set_ledger_time; pub fn setup_env() -> Env { let env = Env::default(); diff --git a/testutils/Cargo.toml b/testutils/Cargo.toml index 4851acc0..b447871a 100644 --- a/testutils/Cargo.toml +++ b/testutils/Cargo.toml @@ -5,4 +5,4 @@ edition = "2021" publish = false [dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.1", features = ["testutils"] }