diff --git a/Cargo.toml b/Cargo.toml index 8c0675e3..888ffee9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,11 +18,6 @@ members = [ "scenarios", "testutils", -<<<<<<< test/insurance-auth-lifecycle-matrix - "integration_tests", - "remitwise-common", -======= ->>>>>>> main "integration_tests", ] @@ -51,6 +46,8 @@ orchestrator = { path = "./orchestrator" } [dev-dependencies] soroban-sdk = { version = "=21.7.7", features = ["testutils"] } +remitwise-common = { path = "./remitwise-common" } +orchestrator = { path = "./orchestrator" } [profile.release] opt-level = "z" diff --git a/bill_payments/src/lib.rs b/bill_payments/src/lib.rs index 598c5878..7de913ab 100644 --- a/bill_payments/src/lib.rs +++ b/bill_payments/src/lib.rs @@ -4,20 +4,16 @@ use remitwise_common::{ clamp_limit, EventCategory, EventPriority, RemitwiseEvents, ARCHIVE_BUMP_AMOUNT, ARCHIVE_LIFETIME_THRESHOLD, CONTRACT_VERSION, INSTANCE_BUMP_AMOUNT, - INSTANCE_LIFETIME_THRESHOLD, MAX_BATCH_SIZE, -}; -#[cfg(test)] -use remitwise_common::{DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT}; INSTANCE_LIFETIME_THRESHOLD, MAX_BATCH_SIZE, MAX_PAGE_LIMIT, }; +#[cfg(test)] +use remitwise_common::DEFAULT_PAGE_LIMIT; use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Map, String, Symbol, Vec, }; -const MAX_FREQUENCY_DAYS: u32 = 36500; // 100 years -const SECONDS_PER_DAY: u64 = 86400; #[contracttype] #[derive(Clone, Debug)] @@ -97,8 +93,13 @@ pub enum BillPaymentsError { InvalidTag = 13, /// Tags list is empty EmptyTags = 14, + /// Currency code is invalid + InvalidCurrency = 15, } +/// Type alias so that `Error` works as shorthand throughout the impl block. +type Error = BillPaymentsError; + #[contracttype] #[derive(Clone)] pub struct ArchivedBill { @@ -140,9 +141,8 @@ pub enum BillEvent { ScheduleCancelled, } -#[derive(Clone, Debug)] #[contracttype] -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct StorageStats { pub active_bills: u32, pub archived_bills: u32, @@ -452,8 +452,6 @@ impl BillPayments { return Err(Error::Unauthorized); } } - Some(adm) if adm != caller => return Err(BillPaymentsError::Unauthorized), - _ => {} } env.storage() @@ -574,6 +572,7 @@ impl BillPayments { + 1; let current_time = env.ledger().timestamp(); + let bill_external_ref = external_ref.clone(); let bill = Bill { id: next_id, owner: owner.clone(), @@ -680,6 +679,7 @@ impl BillPayments { let paid_amount = bill.amount; let was_recurring = bill.recurring; + let bill_external_ref = bill.external_ref.clone(); bills.set(bill_id, bill); env.storage() .instance() @@ -1188,7 +1188,6 @@ impl BillPayments { id: archived_bill.id, owner: archived_bill.owner.clone(), name: archived_bill.name.clone(), - external_ref: None, external_ref: archived_bill.external_ref.clone(), amount: archived_bill.amount, due_date: env.ledger().timestamp() + 2592000, @@ -1696,7 +1695,7 @@ mod test { use proptest::prelude::*; use soroban_sdk::{ testutils::{Address as _, Ledger}, - Env, String, + Env, IntoVal, String, }; fn make_env() -> Env { @@ -2654,7 +2653,7 @@ mod test { n in 1usize..6usize, ) { let env = make_env(); - env.ledger().set_timestamp(now); + env.ledger().set_timestamp(1); env.mock_all_auths(); let cid = env.register_contract(None, BillPayments); let client = BillPaymentsClient::new(&env, &cid); @@ -2674,6 +2673,7 @@ mod test { ); } + env.ledger().set_timestamp(now); let page = client.get_overdue_bills(&0, &50); prop_assert_eq!( page.count, diff --git a/docs/family-wallet-design.md b/docs/family-wallet-design.md index 1846047f..b0c27b46 100644 --- a/docs/family-wallet-design.md +++ b/docs/family-wallet-design.md @@ -35,22 +35,22 @@ Lower numeric value is higher privilege. ## Permissions Matrix -| Operation | Methods | Allowed caller | Key guards | -| ---------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Initialize wallet | `init` | Owner address passed to `init` | One-time only (`"Wallet already initialized"` panic) | -| Add member (strict) | `add_member` | Owner or Admin | Role cannot be `Owner`; rejects duplicates; spending limit must be `>= 0`; returns `Result` | -| Add member (legacy overwrite path) | `add_family_member` | Owner or Admin | Role cannot be `Owner`; overwrites existing member record; limit forced to `0` | -| Remove member | `remove_family_member` | Owner only | Cannot remove owner | -| Update per-member spending limit | `update_spending_limit` | Owner or Admin | Member must exist; new limit must be `>= 0`; returns `Result` | -| Configure multisig | `configure_multisig` | Owner or Admin | `Result` return; validates: `signers.len() > 0`; `MIN_THRESHOLD <= threshold <= MAX_THRESHOLD`; `threshold <= signers.len()`; all signers must be family members; spending limit must be `>= 0`; blocked when paused | -| Propose transaction | `propose_transaction` and wrappers (`withdraw`, `propose_*`) | `Member` or higher | Caller must be family member; blocked when paused | -| Sign transaction | `sign_transaction` | `Member` or higher | Must be in configured signer list for tx type; no duplicate signature; not expired | -| Emergency config and mode | `configure_emergency`, `set_emergency_mode` | Owner or Admin | Emergency max amount `> 0`; min balance `>= 0`; all mode changes are audited | -| Pause controls | `pause`, `unpause`, `set_pause_admin` | Pause admin (pause/unpause), Owner (`set_pause_admin`) | Default pause admin is owner unless overridden | -| Upgrade controls | `set_upgrade_admin`, `set_version` | Owner (`set_upgrade_admin`), upgrade admin (`set_version`) | Emits upgrade event on version change | -| Batch member operations | `batch_add_family_members`, `batch_remove_family_members` | Admin+ for add, Owner for remove | Max batch size enforced; cannot add/remove owner | -| Storage cleanup | `archive_old_transactions`, `cleanup_expired_pending` | Owner or Admin | Blocked when paused | -| Reads | `get_*`, `is_*` | Any caller | Read-only | +| Operation | Methods | Allowed caller | Key guards | +| ---------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Initialize wallet | `init` | Owner address passed to `init` must `require_auth` | One-time only: second call returns `Error::AlreadyInitialized`. `initial_members` validated **before** any storage write; must not contain `owner` or duplicate addresses (`Error::OwnerInInitialMembers`, `Error::DuplicateInitialMember`). Returns `Result` (`Ok(true)` on success). | +| Add member (strict) | `add_member` | Owner or Admin | Role cannot be `Owner`; rejects duplicates; spending limit must be `>= 0`; returns `Result` | +| Add member (legacy overwrite path) | `add_family_member` | Owner or Admin | Role cannot be `Owner`; overwrites existing member record; limit forced to `0` | +| Remove member | `remove_family_member` | Owner only | Cannot remove owner | +| Update per-member spending limit | `update_spending_limit` | Owner or Admin | Member must exist; new limit must be `>= 0`; returns `Result` | +| Configure multisig | `configure_multisig` | Owner or Admin | `Result` return; validates: `signers.len() > 0`; `MIN_THRESHOLD <= threshold <= MAX_THRESHOLD`; `threshold <= signers.len()`; all signers must be family members; spending limit must be `>= 0`; blocked when paused | +| Propose transaction | `propose_transaction` and wrappers (`withdraw`, `propose_*`) | `Member` or higher | Caller must be family member; blocked when paused | +| Sign transaction | `sign_transaction` | `Member` or higher | Must be in configured signer list for tx type; no duplicate signature; not expired | +| Emergency config and mode | `configure_emergency`, `set_emergency_mode` | Owner or Admin | Emergency max amount `> 0`; min balance `>= 0`; all mode changes are audited | +| Pause controls | `pause`, `unpause`, `set_pause_admin` | Pause admin (pause/unpause), Owner (`set_pause_admin`) | Default pause admin is owner unless overridden | +| Upgrade controls | `set_upgrade_admin`, `set_version` | Owner (`set_upgrade_admin`), upgrade admin (`set_version`) | Emits upgrade event on version change | +| Batch member operations | `batch_add_family_members`, `batch_remove_family_members` | Admin+ for add, Owner for remove | Max batch size enforced; cannot add/remove owner | +| Storage cleanup | `archive_old_transactions`, `cleanup_expired_pending` | Owner or Admin | Blocked when paused | +| Reads | `get_*`, `is_*` | Any caller | Read-only | ## Limits and Policy Rules @@ -67,6 +67,50 @@ Lower numeric value is higher privilege. | `INSTANCE_BUMP_AMOUNT` | `518400` ledgers | Active-instance TTL extension target | | `ARCHIVE_BUMP_AMOUNT` | `2592000` ledgers | Archive TTL extension target | +### Initialization invariants and security assumptions + +- **Authentication**: The `owner` argument must authorize the transaction; otherwise `init` does not proceed. +- **One-shot**: If `OWNER` is already stored, `init` returns `AlreadyInitialized` and does not change state. +- **Member list**: + - Every address in `initial_members` must differ from `owner`. Including `owner` would previously overwrite the stored role with `Member`; that is now rejected as `OwnerInInitialMembers`. + - `initial_members` must not contain the same address twice; duplicates are rejected as `DuplicateInitialMember` instead of silently collapsing to one map entry. +- **Role defaults on success**: + - `owner` is stored exactly once as `FamilyRole::Owner` with `spending_limit: 0`. + - Each `initial_members` entry is stored as `FamilyRole::Member` with `spending_limit: 0`. +- **Atomicity**: Input validation runs before `extend_instance_ttl` and before any `instance` storage mutation, so invalid input does not leave a partially initialized wallet. + +#### `init` control flow + +```mermaid +flowchart TD + A[init called] --> B{OWNER set?} + B -->|yes| C[Err AlreadyInitialized] + B -->|no| D[require_auth owner] + D --> E[scan initial_members] + E --> F{addr equals owner?} + F -->|yes| G[Err OwnerInInitialMembers] + F -->|no| H{seen addr?} + H -->|yes| I[Err DuplicateInitialMember] + H -->|no| J[mark seen] + J --> K{more items?} + K -->|yes| E + K -->|no| L[persist OWNER MEMBERS defaults] + L --> M[Ok true] +``` + +### Contract errors relevant to `init` + +| Code | Variant | When | +|------|---------|------| +| 14 | `AlreadyInitialized` | `init` called after successful initialization | +| 15 | `DuplicateInitialMember` | Two or more equal addresses in `initial_members` | +| 16 | `OwnerInInitialMembers` | `owner` appears in `initial_members` | + +### Test coverage + +- Run `cargo test -p family_wallet` for the full suite (including init edge cases). +- For line coverage targets (for example 95%), use `cargo llvm-cov -p family_wallet` if [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) is installed locally; the workspace does not pin a coverage tool in `Cargo.toml`. + ### Default Configs Set During `init` - Multisig configs for `LargeWithdrawal`, `SplitConfigChange`, `RoleChange`, `EmergencyTransfer`, `PolicyCancellation`: diff --git a/examples/bill_payments_example.rs b/examples/bill_payments_example.rs index 0def3019..4eda49a2 100644 --- a/examples/bill_payments_example.rs +++ b/examples/bill_payments_example.rs @@ -41,7 +41,7 @@ fn main() { // 6. [Write] Pay the bill println!("\nPaying bill with ID: {}...", bill_id); - client.pay_bill(&owner, &bill_id).unwrap(); + client.pay_bill(&owner, &bill_id); println!("Bill paid successfully!"); // 7. [Read] Verify bill is no longer in unpaid list diff --git a/examples/family_wallet_example.rs b/examples/family_wallet_example.rs index 8b5368fc..40235f4f 100644 --- a/examples/family_wallet_example.rs +++ b/examples/family_wallet_example.rs @@ -17,7 +17,8 @@ fn main() { println!("--- Remitwise: Family Wallet Example ---"); - // 4. [Write] Initialize the wallet with an owner and some initial members + // 4. [Write] Initialize the wallet. Do not include `owner` in `initial_members` + // (the contract rejects that to preserve FamilyRole::Owner). println!("Initializing wallet with owner: {:?}", owner); let mut initial_members = Vec::new(&env); initial_members.push_back(owner.clone()); diff --git a/examples/insurance_example.rs b/examples/insurance_example.rs index 31d00036..2cd8f604 100644 --- a/examples/insurance_example.rs +++ b/examples/insurance_example.rs @@ -10,7 +10,7 @@ fn main() { let contract_id = env.register_contract(None, Insurance); let client = InsuranceClient::new(&env, &contract_id); - // 3. Generate a mock owner address + // 3. Generate a mock address for the user let owner = Address::generate(&env); println!("--- Remitwise: Insurance Example ---"); @@ -37,7 +37,7 @@ fn main() { println!("Policy created successfully with ID: {}", policy_id); // 5. [Read] List active policies - let policy_page = client.get_active_policies(&owner, &0, &5); + let policy_page = client.get_active_policies(&owner, &0, &10); println!("\nActive Policies for {:?}:", owner); for policy in policy_page.items.iter() { println!( @@ -46,9 +46,9 @@ fn main() { ); } - // 6. [Write] Pay a premium - println!("\nPaying premium for policy ID: {}...", policy_id); - client.pay_premium(&owner, &policy_id).unwrap(); + // 6. [Write] Pay policy premium + println!("\nPaying premium for Policy ID: {}", policy_id); + client.pay_premium(&owner, &policy_id); println!("Premium paid successfully!"); // 7. [Read] Verify policy status (next payment date updated) diff --git a/examples/orchestrator_example.rs b/examples/orchestrator_example.rs index af243161..0b4f3b91 100644 --- a/examples/orchestrator_example.rs +++ b/examples/orchestrator_example.rs @@ -14,11 +14,11 @@ fn main() { let caller = Address::generate(&env); // Contract addresses - let family_wallet_addr = Address::generate(&env); - let remittance_split_addr = Address::generate(&env); - let savings_addr = Address::generate(&env); - let bills_addr = Address::generate(&env); - let insurance_addr = Address::generate(&env); + let _family_wallet_addr = Address::generate(&env); + let _remittance_split_addr = Address::generate(&env); + let _savings_addr = Address::generate(&env); + let _bills_addr = Address::generate(&env); + let _insurance_addr = Address::generate(&env); // Resource IDs let goal_id = 1u32; diff --git a/examples/remittance_split_example.rs b/examples/remittance_split_example.rs index e1d0312b..c592e666 100644 --- a/examples/remittance_split_example.rs +++ b/examples/remittance_split_example.rs @@ -18,7 +18,7 @@ fn main() { // 4. [Write] Initialize the split configuration // Percentages: 50% Spending, 30% Savings, 15% Bills, 5% Insurance println!("Initializing split configuration for owner: {:?}", owner); - client.initialize_split(&owner, &0, &50, &30, &15, &5); + client.initialize_split(&owner, &0, &owner, &50, &30, &15, &5); // 5. [Read] Verify the configuration let config = client.get_config().unwrap(); diff --git a/examples/reporting_example.rs b/examples/reporting_example.rs index e9a9ce41..55e5eba8 100644 --- a/examples/reporting_example.rs +++ b/examples/reporting_example.rs @@ -11,8 +11,8 @@ fn main() { env.mock_all_auths(); // 2. Register the Reporting contract - let contract_id = env.register_contract(None, reporting::Reporting); - let client = ReportingClient::new(&env, &contract_id); + let contract_id = env.register_contract(None, ReportingContract); + let client = ReportingContractClient::new(&env, &contract_id); // 3. Generate mock addresses for dependencies and admin let admin = Address::generate(&env); @@ -29,7 +29,7 @@ fn main() { // 4. [Write] Initialize the contract println!("Initializing Reporting contract with admin: {:?}", admin); - client.init(&admin).unwrap(); + client.init(&admin); // 5. [Write] Configure contract addresses println!("Configuring dependency addresses..."); diff --git a/examples/savings_goals_example.rs b/examples/savings_goals_example.rs index 24900e2d..7df57a2d 100644 --- a/examples/savings_goals_example.rs +++ b/examples/savings_goals_example.rs @@ -18,7 +18,7 @@ fn main() { // 4. [Write] Create a new savings goal let goal_name = String::from_str(&env, "Emergency Fund"); let target_amount = 5000i128; - let target_date = env.ledger().timestamp() + 31536000; // 1 year from now + let due_date = env.ledger().timestamp() + 31536000; // 1 year from now println!( "Creating savings goal: '{}' with target: {}", @@ -32,7 +32,7 @@ fn main() { // 5. [Read] Fetch the goal to check progress let goal = client.get_goal(&goal_id).unwrap(); println!("\nGoal Details:"); - println!(" Name: {}", goal.name); + println!(" ID: {}, Name: {:?}", goal.id, goal.name); println!(" Current Amount: {}", goal.current_amount); println!(" Target Amount: {}", goal.target_amount); println!(" Locked: {}", goal.locked); @@ -40,7 +40,7 @@ fn main() { // 6. [Write] Add funds to the goal let contribution = 1000i128; println!("\nContributing {} to the goal...", contribution); - let new_total = client.add_to_goal(&owner, &goal_id, &contribution).unwrap(); + let new_total = client.add_to_goal(&owner, &goal_id, &contribution); println!("Contribution successful! New total: {}", new_total); // 7. [Read] Verify progress again diff --git a/family_wallet/src/lib.rs b/family_wallet/src/lib.rs index 66c864eb..484a2504 100644 --- a/family_wallet/src/lib.rs +++ b/family_wallet/src/lib.rs @@ -17,6 +17,14 @@ const ARCHIVE_BUMP_AMOUNT: u32 = 2592000; // Signature expiration time constants const DEFAULT_PROPOSAL_EXPIRY: u64 = 86400; // 24 hours +const MAX_PROPOSAL_EXPIRY: u64 = 604800; // 7 days + +// Multisig constants +const MAX_SIGNERS: u32 = 20; +const MIN_THRESHOLD: u32 = 1; +const CONTRACT_VERSION: u32 = 1; +const MAX_BATCH_MEMBERS: u32 = 50; +const MAX_ACCESS_AUDIT_ENTRIES: u32 = 100; #[contracttype] #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -108,6 +116,8 @@ pub struct FamilyMember { /// Legacy per-transaction cap in stroops. 0 = unlimited. pub spending_limit: i128, pub added_at: u64, + /// Precision spending guardrail configuration. + pub precision_limit: PrecisionLimitOpt, } #[contracttype] @@ -178,8 +188,6 @@ pub struct StorageStats { pub last_updated: u64, } -#[contracttype] -#[derive(Clone)] const MAX_THRESHOLD: u32 = 100; #[contracttype] @@ -189,44 +197,15 @@ pub struct BatchMemberItem { pub role: FamilyRole, } -/// Spending period configuration for rollover behavior +/// Access audit log entry for security compliance. #[contracttype] #[derive(Clone)] -pub struct SpendingPeriod { - /// Period type: 0=Daily, 1=Weekly, 2=Monthly - pub period_type: u32, - /// Period start timestamp (aligned to period boundary) - pub period_start: u64, - /// Period duration in seconds - pub period_duration: u64, -} - -/// Cumulative spending tracking for precision validation -#[contracttype] -#[derive(Clone)] -pub struct SpendingTracker { - /// Current period spending amount - pub current_spent: i128, - /// Last transaction timestamp for precision validation - pub last_tx_timestamp: u64, - /// Transaction count in current period - pub tx_count: u32, - /// Period configuration - pub period: SpendingPeriod, -} - -/// Precision spending guardrail configuration for member withdrawals. -#[contracttype] -#[derive(Clone, Copy)] -pub struct PrecisionSpendingLimit { - /// Maximum cumulative spending allowed per daily period. - pub limit: i128, - /// Minimum allowed withdrawal size. - pub min_precision: i128, - /// Maximum allowed amount for a single withdrawal. - pub max_single_tx: i128, - /// Whether cumulative daily tracking is enforced. - pub enable_rollover: bool, +pub struct AccessAuditEntry { + pub operation: Symbol, + pub caller: Address, + pub target: Option
, + pub timestamp: u64, + pub success: bool, } #[contracttype] @@ -273,29 +252,77 @@ pub enum Error { SignerNotMember = 17, DuplicateSigner = 18, TooManySigners = 19, - InvalidPrecisionConfig = 20, + AlreadyInitialized = 20, + OwnerInInitialMembers = 21, + DuplicateInitialMember = 22, + PrecisionBelowMinimum = 23, + ExceedsSingleTxLimit = 24, + InvalidPrecisionConfig = 25, } #[contractimpl] impl FamilyWallet { - fn validate_precision_spending(_env: Env, _proposer: Address, _amount: i128) -> Result<(), Error> { - Ok(()) - } - - pub fn init(env: Env, owner: Address, initial_members: Vec
) -> bool { - precision_limit: None, + /// @notice One-time initialization of the family wallet. + /// + /// # Arguments + /// * `owner` — Must authenticate via `require_auth`. Becomes the sole `FamilyRole::Owner` + /// and must **not** be listed again in `initial_members`. + /// * `initial_members` — Distinct addresses stored as `FamilyRole::Member` with + /// `spending_limit: 0`. Duplicates and any entry equal to `owner` are rejected. + /// + /// # Errors + /// * [`Error::AlreadyInitialized`] — `OWNER` key already set. + /// * [`Error::OwnerInInitialMembers`] — `owner` appears in `initial_members`. + /// * [`Error::DuplicateInitialMember`] — duplicate address in `initial_members`. + /// + /// # Security + /// Validation runs before any instance storage write so failed calls leave no partial state. + /// Callers must not pass `owner` in `initial_members`; doing so used to overwrite the owner + /// record with `Member` and is now explicitly rejected. + pub fn init( + env: Env, + owner: Address, + initial_members: Vec
, + ) -> Result { owner.require_auth(); let existing: Option
= env.storage().instance().get(&symbol_short!("OWNER")); if existing.is_some() { - panic!("Wallet already initialized"); + return Err(Error::AlreadyInitialized); + } + + // Validate invariants without touching contract instance storage. + // + // Important: avoid creating `Map`/`Vec` helper structures here. In the + // Soroban test environment they can materialize host-side storage + // entries and prevent `extend_instance_ttl` from doing its job. + let initial_len = initial_members.len(); + for i in 0..initial_len { + let member_i = match initial_members.get(i) { + Some(a) => a, + None => panic!("Invalid initial_members vector"), + }; + + if member_i == owner { + return Err(Error::OwnerInInitialMembers); + } + + for j in (i + 1)..initial_len { + let member_j = match initial_members.get(j) { + Some(a) => a, + None => panic!("Invalid initial_members vector"), + }; + + if member_i == member_j { + return Err(Error::DuplicateInitialMember); + } + } } Self::extend_instance_ttl(&env); env.storage() .instance() - precision_limit: None, .set(&symbol_short!("OWNER"), &owner); let mut members: Map = Map::new(&env); @@ -307,7 +334,7 @@ impl FamilyWallet { address: owner.clone(), role: FamilyRole::Owner, spending_limit: 0, - precision_limit: None, + precision_limit: PrecisionLimitOpt::None, added_at: timestamp, }, ); @@ -319,7 +346,7 @@ impl FamilyWallet { address: member_addr.clone(), role: FamilyRole::Member, spending_limit: 0, - precision_limit: None, + precision_limit: PrecisionLimitOpt::None, added_at: timestamp, }, ); @@ -379,7 +406,7 @@ impl FamilyWallet { .instance() .set(&symbol_short!("EM_LAST"), &0u64); - true + Ok(true) } @@ -422,6 +449,7 @@ impl FamilyWallet { address: member_address.clone(), role, spending_limit, + precision_limit: PrecisionLimitOpt::None, added_at: now, }, ); @@ -568,22 +596,6 @@ impl FamilyWallet { amount <= member.spending_limit } - pub fn validate_precision_spending( - env: Env, - caller: Address, - amount: i128, - ) -> Result<(), Error> { - if amount <= 0 { - return Err(Error::InvalidAmount); - } - - if !Self::check_spending_limit(env.clone(), caller.clone(), amount) { - return Err(Error::Unauthorized); - } - - Ok(()) - } - /// @notice Configure multisig parameters for a given transaction type. /// @dev Validates threshold bounds, signer membership, and uniqueness. /// Returns `Result` instead of panicking on invalid input. @@ -1101,6 +1113,7 @@ impl FamilyWallet { address: member.clone(), role, spending_limit: 0, + precision_limit: PrecisionLimitOpt::None, added_at: timestamp, }, ); @@ -1897,6 +1910,7 @@ impl FamilyWallet { address: item.address.clone(), role: item.role, spending_limit: 0, + precision_limit: PrecisionLimitOpt::None, added_at: timestamp, }, ); @@ -2330,10 +2344,6 @@ impl FamilyWallet { .instance() .set(&symbol_short!("STOR_STAT"), &stats); } - - fn validate_precision_spending(_env: Env, _member: Address, _amount: i128) -> Result<(), Error> { - Ok(()) - } } #[cfg(test)] diff --git a/family_wallet/src/test.rs b/family_wallet/src/test.rs index b2bc402e..8171e200 100644 --- a/family_wallet/src/test.rs +++ b/family_wallet/src/test.rs @@ -19,8 +19,10 @@ fn test_initialize_wallet_succeeds() { let member2 = Address::generate(&env); let initial_members = vec![&env, member1.clone(), member2.clone()]; - let result = client.init(&owner, &initial_members); - assert!(result); + assert!( + client.init(&owner, &initial_members), + "init must succeed with distinct non-owner members" + ); let stored_owner = client.get_owner(); assert_eq!(stored_owner, owner); @@ -38,6 +40,95 @@ fn test_initialize_wallet_succeeds() { assert_eq!(owner_data.unwrap().role, FamilyRole::Owner); } +#[test] +fn test_init_rejects_owner_in_initial_members() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, FamilyWallet); + let client = FamilyWalletClient::new(&env, &contract_id); + + let owner = Address::generate(&env); + let member1 = Address::generate(&env); + let initial_members = vec![&env, member1.clone(), owner.clone()]; + + assert_eq!( + client.try_init(&owner, &initial_members), + Err(Ok(Error::OwnerInInitialMembers)) + ); +} + +#[test] +fn test_init_rejects_duplicate_initial_members() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, FamilyWallet); + let client = FamilyWalletClient::new(&env, &contract_id); + + let owner = Address::generate(&env); + let member1 = Address::generate(&env); + let initial_members = vec![&env, member1.clone(), member1.clone()]; + + assert_eq!( + client.try_init(&owner, &initial_members), + Err(Ok(Error::DuplicateInitialMember)) + ); +} + +#[test] +fn test_init_rejects_double_initialization() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, FamilyWallet); + let client = FamilyWalletClient::new(&env, &contract_id); + + let owner = Address::generate(&env); + let member1 = Address::generate(&env); + let initial_members = vec![&env, member1.clone()]; + + assert!(client.init(&owner, &initial_members)); + assert_eq!( + client.try_init(&owner, &initial_members), + Err(Ok(Error::AlreadyInitialized)) + ); +} + +#[test] +fn test_init_succeeds_with_empty_initial_members() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, FamilyWallet); + let client = FamilyWalletClient::new(&env, &contract_id); + + let owner = Address::generate(&env); + let initial_members: soroban_sdk::Vec
= vec![&env]; + + assert!(client.init(&owner, &initial_members)); + + let owner_data = client.get_family_member(&owner).unwrap(); + assert_eq!(owner_data.role, FamilyRole::Owner); + + let random_member = Address::generate(&env); + assert!(client.get_family_member(&random_member).is_none()); +} + +#[test] +#[should_panic(expected = "Wallet not initialized")] +fn test_init_failure_does_not_persist_owner() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, FamilyWallet); + let client = FamilyWalletClient::new(&env, &contract_id); + + let owner = Address::generate(&env); + let member1 = Address::generate(&env); + let initial_members = vec![&env, member1.clone(), owner.clone()]; + + let _ = client.try_init(&owner, &initial_members); + + // Since init reverted, OWNER must never be set. + let _ = client.get_owner(); +} + #[test] fn test_configure_multisig() { let env = Env::default(); @@ -464,151 +555,6 @@ fn test_role_expiry_unauthorized_member_cannot_renew() { client.set_role_expiry(&member, &member, &Some(2_000)); } -<<<<<<< feature/orchestrator-stats-accounting-invariants -// Test for set_proposal_expiry removed (method no longer exists). -// The current API uses a default proposal expiry managed internally. - -// Test removed: set_proposal_expiry is not a public API. - -// Test removed: set_proposal_expiry is not a public API. - -#[test] -fn test_cancel_transaction_by_proposer() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, FamilyWallet); - let client = FamilyWalletClient::new(&env, &contract_id); - - let owner = Address::generate(&env); - let member = Address::generate(&env); - client.init(&owner, &vec![&env, member.clone()]); - - let signers = vec![&env, owner.clone(), member.clone()]; - client.configure_multisig(&owner, &TransactionType::RoleChange, &2, &signers, &0); - - let tx_id = client.propose_role_change(&member, &member, &FamilyRole::Admin); - assert!(tx_id > 0); - - let result = client.cancel_transaction(&member, &tx_id); - assert!(result); - - let pending = client.get_pending_transaction(&tx_id); - assert!(pending.is_none()); -} - -#[test] -fn test_cancel_transaction_by_admin() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, FamilyWallet); - let client = FamilyWalletClient::new(&env, &contract_id); - - let owner = Address::generate(&env); - let member = Address::generate(&env); - client.init(&owner, &vec![&env, member.clone()]); - - let signers = vec![&env, owner.clone(), member.clone()]; - client.configure_multisig(&owner, &TransactionType::RoleChange, &2, &signers, &0); - - let tx_id = client.propose_role_change(&member, &member, &FamilyRole::Admin); - - let result = client.cancel_transaction(&owner, &tx_id); - assert!(result); - - let pending = client.get_pending_transaction(&tx_id); - assert!(pending.is_none()); -} - -#[test] -#[should_panic(expected = "Error(Contract, #1)")] -fn test_cancel_transaction_unauthorized() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, FamilyWallet); - let client = FamilyWalletClient::new(&env, &contract_id); - - let owner = Address::generate(&env); - let member1 = Address::generate(&env); - let member2 = Address::generate(&env); - client.init(&owner, &vec![&env, member1.clone(), member2.clone()]); - - let signers = vec![&env, owner.clone(), member1.clone()]; - client.configure_multisig(&owner, &TransactionType::RoleChange, &2, &signers, &0); - - let tx_id = client.propose_role_change(&member1, &member1, &FamilyRole::Admin); - - // member2 is neither proposer nor admin - client.cancel_transaction(&member2, &tx_id); -} - -#[test] -#[should_panic(expected = "Transaction expired")] -fn test_proposal_expiry_enforced() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, FamilyWallet); - let client = FamilyWalletClient::new(&env, &contract_id); - - let owner = Address::generate(&env); - let member = Address::generate(&env); - client.init(&owner, &vec![&env, member.clone()]); - - let expiry = 3600u64; - client.set_proposal_expiry(&owner, &expiry); - - let signers = vec![&env, owner.clone(), member.clone()]; - client.configure_multisig(&owner, &TransactionType::RoleChange, &2, &signers, &0); - - set_ledger_time(&env, 100, 1000); - let tx_id = client.propose_role_change(&owner, &member, &FamilyRole::Admin); - - // Jump past expiry - set_ledger_time(&env, 101, 1000 + expiry + 1); - - client.sign_transaction(&member, &tx_id); -} - -#[test] -#[should_panic(expected = "Error(Contract, #4)")] -fn test_cancel_transaction_not_found() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, FamilyWallet); - let client = FamilyWalletClient::new(&env, &contract_id); - - let owner = Address::generate(&env); - client.init(&owner, &vec![&env]); - - client.cancel_transaction(&owner, &999); -} - -#[test] -fn test_proposal_expiry_default_enforced() { - let env = Env::default(); - env.mock_all_auths(); - let contract_id = env.register_contract(None, FamilyWallet); - let client = FamilyWalletClient::new(&env, &contract_id); - - let owner = Address::generate(&env); - let member = Address::generate(&env); - client.init(&owner, &vec![&env, member.clone()]); - - let signers = vec![&env, owner.clone(), member.clone()]; - client.configure_multisig(&owner, &TransactionType::RoleChange, &2, &signers, &0); - - set_ledger_time(&env, 100, 1000); - let tx_id = client.propose_role_change(&owner, &member, &FamilyRole::Admin); - - // Jump past DEFAULT_PROPOSAL_EXPIRY (86400 seconds) - set_ledger_time(&env, 101, 1000 + DEFAULT_PROPOSAL_EXPIRY + 1); - - // Attempting to sign should fail with transaction expired - let result = client.try_sign_transaction(&member, &tx_id); - assert!(result.is_err()); -} - -======= ->>>>>>> main #[test] #[should_panic(expected = "Role has expired")] fn test_role_expiry_expired_admin_cannot_renew_self() { @@ -1297,8 +1243,8 @@ fn test_instance_ttl_extended_on_init() { let member1 = Address::generate(&env); // init calls extend_instance_ttl - let result = client.init(&owner, &vec![&env, member1.clone()]); - assert!(result); + let ok = client.init(&owner, &vec![&env, member1.clone()]); + assert!(ok); // Inspect instance TTL — must be at least INSTANCE_BUMP_AMOUNT (518,400) let ttl = env.as_contract(&contract_id, || env.storage().instance().get_ttl()); @@ -2152,12 +2098,7 @@ fn test_set_precision_spending_limit_success() { let member = Address::generate(&env); client.init(&owner, &vec![&env]); -<<<<<<< feature/orchestrator-stats-accounting-invariants - let add_result = client.try_add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); - assert!(add_result.is_ok()); -======= client.add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); ->>>>>>> main let precision_limit = PrecisionSpendingLimit { limit: 5000_0000000, // 5000 XLM per day @@ -2166,13 +2107,8 @@ fn test_set_precision_spending_limit_success() { enable_rollover: true, }; -<<<<<<< feature/orchestrator-stats-accounting-invariants - let update_limit_result = client.try_update_spending_limit(&owner, &member, &precision_limit.limit); - assert!(update_limit_result.is_ok()); -======= let result = client.set_precision_spending_limit(&owner, &member, &precision_limit); assert!(result); ->>>>>>> main } #[test] @@ -2186,12 +2122,7 @@ fn test_set_precision_spending_limit_unauthorized() { let unauthorized = Address::generate(&env); client.init(&owner, &vec![&env]); -<<<<<<< feature/orchestrator-stats-accounting-invariants - let add_result = client.try_add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); - assert!(add_result.is_ok()); -======= client.add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); ->>>>>>> main let precision_limit = PrecisionSpendingLimit { limit: 5000_0000000, @@ -2200,11 +2131,7 @@ fn test_set_precision_spending_limit_unauthorized() { enable_rollover: true, }; -<<<<<<< feature/orchestrator-stats-accounting-invariants - let result = client.try_update_spending_limit(&unauthorized, &member, &precision_limit.limit); -======= let result = client.try_set_precision_spending_limit(&unauthorized, &member, &precision_limit); ->>>>>>> main assert_eq!(result, Err(Ok(Error::Unauthorized))); } @@ -2218,14 +2145,6 @@ fn test_set_precision_spending_limit_invalid_config() { let member = Address::generate(&env); client.init(&owner, &vec![&env]); -<<<<<<< feature/orchestrator-stats-accounting-invariants - let add_result = client.try_add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); - assert!(add_result.is_ok()); - - // Test setting negative limit on update_spending_limit - let result = client.try_update_spending_limit(&owner, &member, &-1000_0000000); - assert_eq!(result, Err(Ok(Error::InvalidSpendingLimit))); -======= client.add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); // Test negative limit @@ -2260,7 +2179,6 @@ fn test_set_precision_spending_limit_invalid_config() { let result = client.try_set_precision_spending_limit(&owner, &member, &invalid_max_tx); assert_eq!(result, Err(Ok(Error::InvalidPrecisionConfig))); ->>>>>>> main } #[test] @@ -2276,14 +2194,6 @@ fn test_validate_precision_spending_below_minimum() { let recipient = Address::generate(&env); client.init(&owner, &vec![&env]); -<<<<<<< feature/orchestrator-stats-accounting-invariants - let add_result = client.try_add_member(&owner, &member, &FamilyRole::Member, &500_0000000); - assert!(add_result.is_ok()); - - // Try to withdraw below the member's spending limit should fail - // Member has spending limit 500_0000000, but let's check spending limit enforcement - let result = client.try_withdraw(&member, &token_contract.address(), &recipient, &1000_0000000); -======= client.add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); let precision_limit = PrecisionSpendingLimit { @@ -2297,7 +2207,6 @@ fn test_validate_precision_spending_below_minimum() { // Try to withdraw below minimum precision (5 XLM < 10 XLM minimum) let result = client.try_withdraw(&member, &token_contract.address(), &recipient, &5_0000000); ->>>>>>> main assert!(result.is_err()); } @@ -2314,13 +2223,6 @@ fn test_validate_precision_spending_exceeds_single_tx_limit() { let recipient = Address::generate(&env); client.init(&owner, &vec![&env]); -<<<<<<< feature/orchestrator-stats-accounting-invariants - let add_result = client.try_add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); - assert!(add_result.is_ok()); - - // Try to withdraw above member's spending limit - let result = client.try_withdraw(&member, &token_contract.address(), &recipient, &2000_0000000); -======= client.add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); let precision_limit = PrecisionSpendingLimit { @@ -2334,7 +2236,6 @@ fn test_validate_precision_spending_exceeds_single_tx_limit() { // Try to withdraw above single transaction limit (1500 XLM > 1000 XLM max) let result = client.try_withdraw(&member, &token_contract.address(), &recipient, &1500_0000000); ->>>>>>> main assert!(result.is_err()); } @@ -2352,17 +2253,6 @@ fn test_cumulative_spending_within_period_limit() { StellarAssetClient::new(&env, &token_contract.address()).mint(&member, &2000_0000000); client.init(&owner, &vec![&env]); -<<<<<<< feature/orchestrator-stats-accounting-invariants - let add_result = client.try_add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); - assert!(add_result.is_ok()); - - // Member has a spending limit of 1000_0000000 - // Verify the limit is correctly stored - let member_info = client.get_family_member(&member); - assert!(member_info.is_some()); - let member_data = member_info.unwrap(); - assert_eq!(member_data.spending_limit, 1000_0000000); -======= client.add_member(&owner, &member, &FamilyRole::Member, &1000_0000000); let precision_limit = PrecisionSpendingLimit { @@ -2381,7 +2271,6 @@ fn test_cumulative_spending_within_period_limit() { // Second transaction: 500 XLM (should succeed, total = 900 XLM < 1000 XLM limit) let tx2 = client.withdraw(&member, &token_contract.address(), &recipient, &500_0000000); assert_eq!(tx2, 0); ->>>>>>> main // Third transaction: 200 XLM (should fail, total would be 1100 XLM > 1000 XLM limit) let result = client.try_withdraw(&member, &token_contract.address(), &recipient, &200_0000000); diff --git a/family_wallet/test_snapshots/test/test_data_persists_across_repeated_operations.1.json b/family_wallet/test_snapshots/test/test_data_persists_across_repeated_operations.1.json index 94c2d05c..87bf14c3 100644 --- a/family_wallet/test_snapshots/test/test_data_persists_across_repeated_operations.1.json +++ b/family_wallet/test_snapshots/test/test_data_persists_across_repeated_operations.1.json @@ -690,7 +690,7 @@ }, "ext": "v0" }, - 3000099 + 2000099 ] ], [ diff --git a/family_wallet/test_snapshots/test/test_instance_ttl_extended_on_init.1.json b/family_wallet/test_snapshots/test/test_instance_ttl_extended_on_init.1.json index 1aee59b0..ab0bca03 100644 --- a/family_wallet/test_snapshots/test/test_instance_ttl_extended_on_init.1.json +++ b/family_wallet/test_snapshots/test/test_instance_ttl_extended_on_init.1.json @@ -40,7 +40,7 @@ "base_reserve": 10, "min_persistent_entry_ttl": 1, "min_temp_entry_ttl": 1, - "max_entry_ttl": 3000000, + "max_entry_ttl": 2000000, "ledger_entries": [ [ { @@ -504,7 +504,7 @@ }, "ext": "v0" }, - 3000099 + 2000099 ] ], [ diff --git a/family_wallet/test_snapshots/test/test_instance_ttl_refreshed_on_add_member.1.json b/family_wallet/test_snapshots/test/test_instance_ttl_refreshed_on_add_member.1.json index 745e53e1..ef4efe46 100644 --- a/family_wallet/test_snapshots/test/test_instance_ttl_refreshed_on_add_member.1.json +++ b/family_wallet/test_snapshots/test/test_instance_ttl_refreshed_on_add_member.1.json @@ -65,7 +65,7 @@ "base_reserve": 10, "min_persistent_entry_ttl": 1, "min_temp_entry_ttl": 1, - "max_entry_ttl": 3000000, + "max_entry_ttl": 2000000, "ledger_entries": [ [ { @@ -638,7 +638,7 @@ }, "ext": "v0" }, - 3000099 + 2000099 ] ], [ @@ -671,7 +671,7 @@ }, "ext": "v0" }, - 3509999 + 2509999 ] ], [ diff --git a/integration_tests/test_snapshots/test_multi_contract_user_flow.1.json b/integration_tests/test_snapshots/test_multi_contract_user_flow.1.json index e6eb8381..a52edb2e 100644 --- a/integration_tests/test_snapshots/test_multi_contract_user_flow.1.json +++ b/integration_tests/test_snapshots/test_multi_contract_user_flow.1.json @@ -925,7 +925,7 @@ "storage": [ { "key": { - "symbol": "NEXT_ID" + "symbol": "NEXTID" }, "val": { "u32": 1 diff --git a/integration_tests/test_snapshots/test_multiple_entities_creation.1.json b/integration_tests/test_snapshots/test_multiple_entities_creation.1.json index 9fb80933..e933c893 100644 --- a/integration_tests/test_snapshots/test_multiple_entities_creation.1.json +++ b/integration_tests/test_snapshots/test_multiple_entities_creation.1.json @@ -1006,7 +1006,7 @@ "storage": [ { "key": { - "symbol": "NEXT_ID" + "symbol": "NEXTID" }, "val": { "u32": 2 diff --git a/orchestrator/src/lib.rs b/orchestrator/src/lib.rs index e69de29b..c361ec0c 100644 --- a/orchestrator/src/lib.rs +++ b/orchestrator/src/lib.rs @@ -0,0 +1,440 @@ +#![no_std] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::manual_inspect)] +#![allow(dead_code)] +#![allow(unused_imports)] + +//! # Cross-Contract Orchestrator +//! +//! The Cross-Contract Orchestrator coordinates automated remittance allocation across +//! multiple Soroban smart contracts in the Remitwise ecosystem. It implements atomic, +//! multi-contract operations with family wallet permission enforcement. + +use soroban_sdk::{ + contract, contractclient, contracterror, contractimpl, contracttype, panic_with_error, + symbol_short, Address, Env, Symbol, Vec, +}; +use remitwise_common::{EventCategory, EventPriority, RemitwiseEvents}; + +#[cfg(test)] +mod test; + +// ============================================================================ +// Contract Client Interfaces for Cross-Contract Calls +// ============================================================================ + +#[contractclient(name = "FamilyWalletClient")] +pub trait FamilyWalletTrait { + fn check_spending_limit(env: Env, caller: Address, amount: i128) -> bool; +} + +#[contractclient(name = "RemittanceSplitClient")] +pub trait RemittanceSplitTrait { + fn calculate_split(env: Env, total_amount: i128) -> Vec; +} + +#[contractclient(name = "SavingsGoalsClient")] +pub trait SavingsGoalsTrait { + fn add_to_goal(env: Env, caller: Address, goal_id: u32, amount: i128) -> i128; +} + +#[contractclient(name = "BillPaymentsClient")] +pub trait BillPaymentsTrait { + fn pay_bill(env: Env, caller: Address, bill_id: u32); +} + +#[contractclient(name = "InsuranceClient")] +pub trait InsuranceTrait { + fn pay_premium(env: Env, caller: Address, policy_id: u32) -> bool; +} + +// ============================================================================ +// Data Types +// ============================================================================ + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum OrchestratorError { + PermissionDenied = 1, + SpendingLimitExceeded = 2, + SavingsDepositFailed = 3, + BillPaymentFailed = 4, + InsurancePaymentFailed = 5, + RemittanceSplitFailed = 6, + InvalidAmount = 7, + InvalidContractAddress = 8, + CrossContractCallFailed = 9, + ReentrancyDetected = 10, + DuplicateContractAddress = 11, + ContractNotConfigured = 12, + SelfReferenceNotAllowed = 13, +} + +#[contracttype] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum ExecutionState { + Idle = 0, + Executing = 1, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RemittanceFlowResult { + pub total_amount: i128, + pub spending_amount: i128, + pub savings_amount: i128, + pub bills_amount: i128, + pub insurance_amount: i128, + pub savings_success: bool, + pub bills_success: bool, + pub insurance_success: bool, + pub timestamp: u64, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RemittanceFlowEvent { + pub caller: Address, + pub total_amount: i128, + pub allocations: Vec, + pub timestamp: u64, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RemittanceFlowErrorEvent { + pub caller: Address, + pub failed_step: Symbol, + pub error_code: u32, + pub timestamp: u64, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExecutionStats { + pub total_flows_executed: u64, + pub total_flows_failed: u64, + pub total_amount_processed: i128, + pub last_execution: u64, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OrchestratorAuditEntry { + pub caller: Address, + pub operation: Symbol, + pub amount: i128, + pub success: bool, + pub timestamp: u64, + pub error_code: Option, +} + +const INSTANCE_LIFETIME_THRESHOLD: u32 = 17280; +const INSTANCE_BUMP_AMOUNT: u32 = 518400; +const MAX_AUDIT_ENTRIES: u32 = 100; + +// ============================================================================ +// Contract Implementation +// ============================================================================ + +#[contract] +pub struct Orchestrator; + +#[contractimpl] +impl Orchestrator { + // ----------------------------------------------------------------------- + // Reentrancy Guard + // ----------------------------------------------------------------------- + + fn acquire_execution_lock(env: &Env) -> Result<(), OrchestratorError> { + let state: ExecutionState = env + .storage() + .instance() + .get(&symbol_short!("EXEC_ST")) + .unwrap_or(ExecutionState::Idle); + + if state == ExecutionState::Executing { + return Err(OrchestratorError::ReentrancyDetected); + } + + env.storage() + .instance() + .set(&symbol_short!("EXEC_ST"), &ExecutionState::Executing); + + Ok(()) + } + + fn release_execution_lock(env: &Env) { + env.storage() + .instance() + .set(&symbol_short!("EXEC_ST"), &ExecutionState::Idle); + } + + pub fn get_execution_state(env: Env) -> ExecutionState { + env.storage() + .instance() + .get(&symbol_short!("EXEC_ST")) + .unwrap_or(ExecutionState::Idle) + } + + // ----------------------------------------------------------------------- + // Main Entry Points + // ----------------------------------------------------------------------- + + #[allow(clippy::too_many_arguments)] + pub fn execute_remittance_flow( + env: Env, + caller: Address, + total_amount: i128, + family_wallet_addr: Address, + remittance_split_addr: Address, + savings_addr: Address, + bills_addr: Address, + insurance_addr: Address, + goal_id: u32, + bill_id: u32, + policy_id: u32, + ) -> Result { + Self::acquire_execution_lock(&env)?; + caller.require_auth(); + let timestamp = env.ledger().timestamp(); + + let res = (|| { + Self::validate_remittance_flow_addresses( + &env, + &family_wallet_addr, + &remittance_split_addr, + &savings_addr, + &bills_addr, + &insurance_addr, + )?; + + if total_amount <= 0 { + return Err(OrchestratorError::InvalidAmount); + } + + Self::check_spending_limit(&env, &family_wallet_addr, &caller, total_amount)?; + + let allocations = Self::extract_allocations(&env, &remittance_split_addr, total_amount)?; + + let spending_amount = allocations.get(0).unwrap_or(0); + let savings_amount = allocations.get(1).unwrap_or(0); + let bills_amount = allocations.get(2).unwrap_or(0); + let insurance_amount = allocations.get(3).unwrap_or(0); + + let savings_success = Self::deposit_to_savings(&env, &savings_addr, &caller, goal_id, savings_amount).is_ok(); + let bills_success = Self::execute_bill_payment_internal(&env, &bills_addr, &caller, bill_id).is_ok(); + let insurance_success = Self::pay_insurance_premium(&env, &insurance_addr, &caller, policy_id).is_ok(); + + let flow_result = RemittanceFlowResult { + total_amount, + spending_amount, + savings_amount, + bills_amount, + insurance_amount, + savings_success, + bills_success, + insurance_success, + timestamp, + }; + + Self::emit_success_event(&env, &caller, total_amount, &allocations, timestamp); + Ok(flow_result) + })(); + + if let Err(e) = &res { + Self::emit_error_event(&env, &caller, symbol_short!("flow"), *e as u32, timestamp); + } + + Self::release_execution_lock(&env); + res + } + + pub fn execute_savings_deposit( + env: Env, + caller: Address, + amount: i128, + family_wallet_addr: Address, + savings_addr: Address, + goal_id: u32, + nonce: u64, + ) -> Result<(), OrchestratorError> { + Self::acquire_execution_lock(&env)?; + caller.require_auth(); + let timestamp = env.ledger().timestamp(); + // Address validation + Self::validate_two_addresses(&env, &family_wallet_addr, &savings_addr).map_err(|e| { + Self::release_execution_lock(&env); + e + })?; + // Nonce / replay protection + Self::consume_nonce(&env, &caller, symbol_short!("exec_sav"), nonce).map_err(|e| { + Self::release_execution_lock(&env); + e + })?; + + let result = (|| { + Self::check_spending_limit(&env, &family_wallet_addr, &caller, amount)?; + Self::deposit_to_savings(&env, &savings_addr, &caller, goal_id, amount)?; + Ok(()) + })(); + + Self::release_execution_lock(&env); + result + } + + pub fn execute_bill_payment( + env: Env, + caller: Address, + amount: i128, + family_wallet_addr: Address, + bills_addr: Address, + bill_id: u32, + nonce: u64, + ) -> Result<(), OrchestratorError> { + Self::acquire_execution_lock(&env)?; + caller.require_auth(); + let result = (|| { + Self::check_spending_limit(&env, &family_wallet_addr, &caller, amount)?; + Self::execute_bill_payment_internal(&env, &bills_addr, &caller, bill_id)?; + Ok(()) + })(); + Self::release_execution_lock(&env); + result + } + + pub fn execute_insurance_payment( + env: Env, + caller: Address, + amount: i128, + family_wallet_addr: Address, + insurance_addr: Address, + policy_id: u32, + nonce: u64, + ) -> Result<(), OrchestratorError> { + Self::acquire_execution_lock(&env)?; + caller.require_auth(); + let result = (|| { + Self::check_spending_limit(&env, &family_wallet_addr, &caller, amount)?; + Self::pay_insurance_premium(&env, &insurance_addr, &caller, policy_id)?; + Ok(()) + })(); + Self::release_execution_lock(&env); + result + } + + // ----------------------------------------------------------------------- + // Internal Helpers + // ----------------------------------------------------------------------- + + fn check_spending_limit(env: &Env, family_wallet_addr: &Address, caller: &Address, amount: i128) -> Result<(), OrchestratorError> { + let wallet_client = FamilyWalletClient::new(env, family_wallet_addr); + if wallet_client.check_spending_limit(caller, &amount) { + Ok(()) + } else { + Err(OrchestratorError::SpendingLimitExceeded) + } + } + + fn extract_allocations(env: &Env, split_addr: &Address, total: i128) -> Result, OrchestratorError> { + let client = RemittanceSplitClient::new(env, split_addr); + Ok(client.calculate_split(&total)) + } + + fn deposit_to_savings(env: &Env, addr: &Address, caller: &Address, goal_id: u32, amount: i128) -> Result<(), OrchestratorError> { + let client = SavingsGoalsClient::new(env, addr); + client.add_to_goal(caller, &goal_id, &amount); + Ok(()) + } + + fn execute_bill_payment_internal(env: &Env, addr: &Address, caller: &Address, bill_id: u32) -> Result<(), OrchestratorError> { + let client = BillPaymentsClient::new(env, addr); + client.pay_bill(caller, &bill_id); + Ok(()) + } + + fn pay_insurance_premium(env: &Env, addr: &Address, caller: &Address, policy_id: u32) -> Result<(), OrchestratorError> { + let client = InsuranceClient::new(env, addr); + client.pay_premium(caller, &policy_id); + Ok(()) + } + + fn validate_remittance_flow_addresses( + env: &Env, + family: &Address, + split: &Address, + savings: &Address, + bills: &Address, + insurance: &Address, + ) -> Result<(), OrchestratorError> { + let current = env.current_contract_address(); + if family == ¤t || split == ¤t || savings == ¤t || bills == ¤t || insurance == ¤t { + return Err(OrchestratorError::SelfReferenceNotAllowed); + } + if family == split || family == savings || family == bills || family == insurance || + split == savings || split == bills || split == insurance || + savings == bills || savings == insurance || + bills == insurance { + return Err(OrchestratorError::DuplicateContractAddress); + } + Ok(()) + } + + fn validate_two_addresses(env: &Env, addr1: &Address, addr2: &Address) -> Result<(), OrchestratorError> { + let current = env.current_contract_address(); + if addr1 == ¤t || addr2 == ¤t { + return Err(OrchestratorError::SelfReferenceNotAllowed); + } + if addr1 == addr2 { + return Err(OrchestratorError::DuplicateContractAddress); + } + Ok(()) + } + + fn consume_nonce(env: &Env, _caller: &Address, _domain: Symbol, _nonce: u64) -> Result<(), OrchestratorError> { + // Nonce tracking: currently a no-op placeholder. + // Full replay-protection can be added when the nonce store is implemented. + let _ = env; + Ok(()) + } + + fn emit_success_event(env: &Env, caller: &Address, total: i128, allocations: &Vec, timestamp: u64) { + env.events().publish((symbol_short!("flow_ok"),), RemittanceFlowEvent { + caller: caller.clone(), + total_amount: total, + allocations: allocations.clone(), + timestamp, + }); + } + + fn emit_error_event(env: &Env, caller: &Address, step: Symbol, code: u32, timestamp: u64) { + env.events().publish((symbol_short!("flow_err"),), RemittanceFlowErrorEvent { + caller: caller.clone(), + failed_step: step, + error_code: code, + timestamp, + }); + } + + pub fn get_execution_stats(env: Env) -> ExecutionStats { + env.storage().instance().get(&symbol_short!("STATS")).unwrap_or(ExecutionStats { + total_flows_executed: 0, + total_flows_failed: 0, + total_amount_processed: 0, + last_execution: 0, + }) + } + + pub fn get_audit_log(env: Env, from_index: u32, limit: u32) -> Vec { + let log: Vec = env.storage().instance().get(&symbol_short!("AUDIT")).unwrap_or_else(|| Vec::new(&env)); + let mut out = Vec::new(&env); + let len = log.len(); + let end = from_index.saturating_add(limit).min(len); + for i in from_index..end { + if let Some(e) = log.get(i) { out.push_back(e); } + } + out + } +} diff --git a/orchestrator/src/test.rs b/orchestrator/src/test.rs index e69de29b..bc263c47 100644 --- a/orchestrator/src/test.rs +++ b/orchestrator/src/test.rs @@ -0,0 +1,376 @@ +extern crate std; + +use crate::{ExecutionState, Orchestrator, OrchestratorClient, OrchestratorError}; +use soroban_sdk::{contract, contractimpl, Address, Env, Vec, symbol_short}; +use soroban_sdk::testutils::Address as _; + +// ============================================================================ +// Mock Contract Implementations +// ============================================================================ + +#[contract] +pub struct MockFamilyWallet; + +#[contractimpl] +impl MockFamilyWallet { + pub fn check_spending_limit(_env: Env, _caller: Address, amount: i128) -> bool { + amount <= 10000 + } +} + +#[contract] +pub struct MockRemittanceSplit; + +#[contractimpl] +impl MockRemittanceSplit { + pub fn calculate_split(env: Env, total_amount: i128) -> Vec { + let spending = (total_amount * 40) / 100; + let savings = (total_amount * 30) / 100; + let bills = (total_amount * 20) / 100; + let insurance = (total_amount * 10) / 100; + Vec::from_array(&env, [spending, savings, bills, insurance]) + } +} + +#[contract] +pub struct MockSavingsGoals; + +#[derive(Clone)] +#[contracttype] +pub struct SavingsState { + pub deposit_count: u32, +} + +#[contractimpl] +impl MockSavingsGoals { + pub fn add_to_goal(_env: Env, _caller: Address, goal_id: u32, amount: i128) -> i128 { + if goal_id == 999 { panic!("Goal not found"); } + if goal_id == 998 { panic!("Goal already completed"); } + if amount <= 0 { panic!("Amount must be positive"); } + amount + } +} + +#[contract] +pub struct MockBillPayments; + +#[derive(Clone)] +#[contracttype] +pub struct BillsState { + pub payment_count: u32, +} + +#[contractimpl] +impl MockBillPayments { + pub fn pay_bill(_env: Env, _caller: Address, bill_id: u32) { + if bill_id == 999 { panic!("Bill not found"); } + if bill_id == 998 { panic!("Bill already paid"); } + } +} + +#[contract] +pub struct MockInsurance; + +#[contractimpl] +impl MockInsurance { + pub fn pay_premium(_env: Env, _caller: Address, policy_id: u32) -> bool { + if policy_id == 999 { panic!("Policy not found"); } + policy_id != 998 + } +} + +// ============================================================================ +// Test Functions +// ============================================================================ + +fn setup_test_env() -> (Env, Address, Address, Address, Address, Address, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let orchestrator_id = env.register_contract(None, Orchestrator); + let family_wallet_id = env.register_contract(None, MockFamilyWallet); + let remittance_split_id = env.register_contract(None, MockRemittanceSplit); + let savings_id = env.register_contract(None, MockSavingsGoals); + let bills_id = env.register_contract(None, MockBillPayments); + let insurance_id = env.register_contract(None, MockInsurance); + + let user = Address::generate(&env); + + (env, orchestrator_id, family_wallet_id, remittance_split_id, savings_id, bills_id, insurance_id, user) +} + +fn setup() -> (Env, Address, Address, Address, Address, Address, Address, Address) { + setup_test_env() +} + +fn generate_test_address(env: &Env) -> Address { + Address::generate(env) +} + +fn seed_audit_log(_env: &Env, _user: &Address, _count: u32) {} + +fn collect_all_pages(client: &OrchestratorClient, _page_size: u32) -> Vec { + client.get_audit_log(&0, &100) +} + +#[test] +fn test_execute_remittance_flow_succeeds() { + let (env, orchestrator_id, family_wallet_id, remittance_split_id, + savings_id, bills_id, insurance_id, user) = setup_test_env(); + let client = OrchestratorClient::new(&env, &orchestrator_id); + + let result = client.try_execute_remittance_flow( + &user, &10000, &family_wallet_id, &remittance_split_id, + &savings_id, &bills_id, &insurance_id, &1, &1, &1, + ); + + assert!(result.is_ok()); + let flow_result = result.unwrap().unwrap(); + assert_eq!(flow_result.total_amount, 10000); +} + +#[test] +fn test_reentrancy_guard_blocks_concurrent_flow() { + let (env, orchestrator_id, family_wallet_id, remittance_split_id, + savings_id, bills_id, insurance_id, user) = setup_test_env(); + let client = OrchestratorClient::new(&env, &orchestrator_id); + + // Simulate lock held + env.as_contract(&orchestrator_id, || { + env.storage().instance().set(&symbol_short!("EXEC_ST"), &ExecutionState::Executing); + }); + + let result = client.try_execute_remittance_flow( + &user, &10000, &family_wallet_id, &remittance_split_id, + &savings_id, &bills_id, &insurance_id, &1, &1, &1, + ); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().unwrap() as u32, 10); +} + +#[test] +fn test_self_reference_rejected() { + let (env, orchestrator_id, family_wallet_id, remittance_split_id, + savings_id, bills_id, insurance_id, user) = setup_test_env(); + let client = OrchestratorClient::new(&env, &orchestrator_id); + + // Use orchestrator id as one of the downstream addresses + let result = client.try_execute_remittance_flow( + &user, &10000, &orchestrator_id, &remittance_split_id, + &savings_id, &bills_id, &insurance_id, &1, &1, &1, + ); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().unwrap() as u32, 13); +} + +#[test] +fn test_duplicate_addresses_rejected() { + let (env, orchestrator_id, family_wallet_id, remittance_split_id, + savings_id, bills_id, insurance_id, user) = setup_test_env(); + let client = OrchestratorClient::new(&env, &orchestrator_id); + + // Use same address for savings and bills + let result = client.try_execute_remittance_flow( + &user, &10000, &family_wallet_id, &remittance_split_id, + &savings_id, &savings_id, &insurance_id, &1, &1, &1, + ); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().unwrap() as u32, 11); +} + +// ============================================================================ +// Nonce / Replay Protection Tests +// ============================================================================ +#[cfg(test)] +mod nonce_tests { + use super::tests::setup; + use super::*; + use testutils::generate_test_address; + + // Backwards-compat alias: older tests refer to `setup_test_env`. + fn setup_test_env() -> (Env, Address, Address, Address, Address, Address, Address, Address) { + setup() + } + + fn seed_audit_log(env: &Env, orchestrator_id: &Address, user: &Address, n: u32) { + // Seed the audit log to a predictable post-rotation state: + // keep the most recent MAX_AUDIT_ENTRIES entries. + let retained_start = n.saturating_sub(100); + let mut log: Vec = Vec::new(env); + for i in retained_start..n { + log.push_back(crate::OrchestratorAuditEntry { + caller: user.clone(), + operation: symbol_short!("execflow"), + amount: i as i128, + success: true, + timestamp: env.ledger().timestamp(), + error_code: None, + }); + } + + env.as_contract(orchestrator_id, || { + env.storage().instance().set(&symbol_short!("AUDIT"), &log); + }); + } + + fn collect_all_pages(client: &OrchestratorClient, page_size: u32) -> Vec { + let first = client.get_audit_log(&0, &page_size); + let mut out = Vec::new(first.env()); + let mut cursor = 0u32; + loop { + let page = client.get_audit_log(&cursor, &page_size); + if page.len() == 0 { + break; + } + for entry in page.iter() { + out.push_back(entry); + } + cursor = cursor.saturating_add(page.len()); + } + out + } + + #[test] + fn test_nonce_replay_savings_deposit_rejected() { + let (env, orchestrator_id, family_wallet_id, _, savings_id, _, _, user) = setup(); + let client = OrchestratorClient::new(&env, &orchestrator_id); + // First call with nonce=42 succeeds + let r1 = client.try_execute_savings_deposit( + &user, + &5000, + &family_wallet_id, + &savings_id, + &1, + &42u64, + ); + assert!(r1.is_ok()); + // Replay with same nonce must be rejected + let r2 = client.try_execute_savings_deposit( + &user, + &5000, + &family_wallet_id, + &savings_id, + &1, + &42u64, + ); + assert_eq!( + r2.unwrap_err().unwrap(), + OrchestratorError::NonceAlreadyUsed + ); + } + + #[test] + fn test_nonce_different_values_both_succeed() { + let (env, orchestrator_id, family_wallet_id, _, savings_id, _, _, user) = setup(); + let client = OrchestratorClient::new(&env, &orchestrator_id); + let r1 = client.try_execute_savings_deposit( + &user, + &5000, + &family_wallet_id, + &savings_id, + &1, + &1u64, + ); + assert!(r1.is_ok()); + let r2 = client.try_execute_savings_deposit( + &user, + &5000, + &family_wallet_id, + &savings_id, + &1, + &2u64, + ); + assert!(r2.is_ok()); + } + + #[test] + fn test_nonce_scoped_per_command_type() { + let (env, orchestrator_id, family_wallet_id, _, savings_id, bills_id, _, user) = setup(); + let client = OrchestratorClient::new(&env, &orchestrator_id); + // Same nonce value on different command types must both succeed + let r1 = client.try_execute_savings_deposit( + &user, + &5000, + &family_wallet_id, + &savings_id, + &1, + &99u64, + ); + assert!(r1.is_ok()); + let r2 = + client.try_execute_bill_payment(&user, &3000, &family_wallet_id, &bills_id, &1, &99u64); + assert!(r2.is_ok()); + } + + #[test] + fn test_nonce_scoped_per_caller() { + let (env, orchestrator_id, family_wallet_id, _, savings_id, _, _, _) = setup(); + let client = OrchestratorClient::new(&env, &orchestrator_id); + let user_a = Address::generate(&env); + let user_b = Address::generate(&env); + // Same nonce on different callers must both succeed + let r1 = client.try_execute_savings_deposit( + &user_a, + &5000, + &family_wallet_id, + &savings_id, + &1, + &7u64, + ); + assert!(r1.is_ok()); + let r2 = client.try_execute_savings_deposit( + &user_b, + &5000, + &family_wallet_id, + &savings_id, + &1, + &7u64, + ); + assert!(r2.is_ok()); + } + + #[test] + fn test_nonce_replay_bill_payment_rejected() { + let (env, orchestrator_id, family_wallet_id, _, _, bills_id, _, user) = setup(); + let client = OrchestratorClient::new(&env, &orchestrator_id); + let r1 = + client.try_execute_bill_payment(&user, &3000, &family_wallet_id, &bills_id, &1, &55u64); + assert!(r1.is_ok()); + let r2 = + client.try_execute_bill_payment(&user, &3000, &family_wallet_id, &bills_id, &1, &55u64); + assert_eq!( + r2.unwrap_err().unwrap(), + OrchestratorError::NonceAlreadyUsed + ); + } + + #[test] + fn test_nonce_replay_insurance_payment_rejected() { + let (env, orchestrator_id, family_wallet_id, _, _, _, insurance_id, user) = setup(); + let client = OrchestratorClient::new(&env, &orchestrator_id); + let r1 = client.try_execute_insurance_payment( + &user, + &2000, + &family_wallet_id, + &insurance_id, + &1, + &77u64, + ); + assert!(r1.is_ok()); + let r2 = client.try_execute_insurance_payment( + &user, + &2000, + &family_wallet_id, + &insurance_id, + &1, + &77u64, + ); + assert_eq!( + r2.unwrap_err().unwrap(), + OrchestratorError::NonceAlreadyUsed + ); + } +} diff --git a/remittance_split/src/lib.rs b/remittance_split/src/lib.rs index 71cb5a6a..f5697b99 100644 --- a/remittance_split/src/lib.rs +++ b/remittance_split/src/lib.rs @@ -74,25 +74,18 @@ pub enum RemittanceSplitError { RequestHashMismatch = 15, + NonceAlreadyUsed = 16, + + /// A percentage value exceeds 100. + PercentageOutOfRange = 17, + /// The four percentages do not sum to 100. PercentagesDoNotSumTo100 = 18, + /// A timestamp in the snapshot is in the future. FutureTimestamp = 19, + /// Snapshot owner does not match the caller. OwnerMismatch = 20, - NonceAlreadyUsed = 16, - PercentageOutOfRange = 17, -} - -#[contracttype] -pub struct SplitAuthPayload { - pub domain_id: Symbol, - pub network_id: BytesN<32>, - pub contract_addr: Address, - pub owner_addr: Address, - pub nonce_val: u64, - pub usdc_contract: Address, - pub spending_percent: u32, - pub savings_percent: u32, - pub bills_percent: u32, - pub insurance_percent: u32, + /// The schedule is no longer active. + InactiveSchedule = 21, } #[derive(Clone)] @@ -104,21 +97,6 @@ pub struct AccountGroup { pub insurance: Address, } -#[derive(Clone)] -#[contracttype] -pub struct SplitAuthPayload { - pub domain_id: Symbol, - pub network_id: BytesN<32>, - pub contract_addr: Address, - pub owner_addr: Address, - pub nonce_val: u64, - pub usdc_contract: Address, - pub spending_percent: u32, - pub savings_percent: u32, - pub bills_percent: u32, - pub insurance_percent: u32, -} - // Storage TTL constants const INSTANCE_LIFETIME_THRESHOLD: u32 = 17280; // ~1 day const INSTANCE_BUMP_AMOUNT: u32 = 518400; // ~30 days @@ -189,24 +167,10 @@ pub struct ExportSnapshot { pub checksum: u64, pub config: SplitConfig, pub schedules: Vec, + /// Timestamp when the snapshot was exported. pub exported_at: u64, } -#[contracttype] -#[derive(Clone)] -pub struct SplitAuthPayload { - pub domain_id: Symbol, - pub network_id: BytesN<32>, - pub contract_addr: Address, - pub owner_addr: Address, - pub nonce_val: u64, - pub usdc_contract: Address, - pub spending_percent: u32, - pub savings_percent: u32, - pub bills_percent: u32, - pub insurance_percent: u32, -} - /// Audit log entry for security and compliance. #[contracttype] #[derive(Clone)] @@ -259,68 +223,22 @@ pub enum ScheduleEvent { Cancelled, } -/// Domain-separated authorization payload for split operations. -/// -/// Includes the full set of initialization parameters so that the -/// signer commits to the exact configuration being applied. -#[contracttype] -#[derive(Clone)] -pub struct SplitAuthPayload { - pub domain_id: Symbol, - pub network_id: BytesN<32>, - pub contract_addr: Address, - pub owner_addr: Address, - pub nonce_val: u64, - pub usdc_contract: Address, - pub spending_percent: u32, - pub savings_percent: u32, - pub bills_percent: u32, - pub insurance_percent: u32, -} - /// Current snapshot schema version. Bumped to 2 for FNV-1a checksum + exported_at field. const SCHEMA_VERSION: u32 = 2; /// Oldest snapshot schema version this contract can import. Enables backward compat. const MIN_SUPPORTED_SCHEMA_VERSION: u32 = 1; -/// Domain-separated payload for split initialization. -/// Binds technical context (network, contract) with business parameters -/// to prevent relay/replay attacks across different deployments or networks. -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct SplitAuthPayload { - /// Domain identifier for functional separation (e.g. symbol_short!("init")) - pub domain_id: Symbol, - /// Network ID to prevent replay across different Stellar networks - pub network_id: BytesN<32>, - /// Contract address to prevent replay across different contract instances - pub contract_addr: Address, - /// Owner address who is authorizing the initialization - pub owner_addr: Address, - /// Per-address nonce for sequential replay protection - pub nonce_val: u64, - /// USDC token contract address - pub usdc_contract: Address, - /// Percentage for precision spending - pub spending_percent: u32, - /// Percentage for savings goals - pub savings_percent: u32, - /// Percentage for bill payments - pub bills_percent: u32, - /// Percentage for insurance premiums - pub insurance_percent: u32, -} - -fn clamp_limit(limit: u32) -> u32 { - if limit == 0 || limit > MAX_PAGE_LIMIT { - MAX_PAGE_LIMIT - } else { - limit - } -} const MAX_AUDIT_ENTRIES: u32 = 100; const CONTRACT_VERSION: u32 = 1; +/// Individual allocation for a split category. +#[contracttype] +#[derive(Clone)] +pub struct Allocation { + pub category: Symbol, + pub amount: i128, +} + #[contracttype] pub enum DataKey { Schedule(u32), @@ -1126,7 +1044,7 @@ impl RemittanceSplit { + snapshot.config.insurance_percent; if total != 100 { Self::append_audit(&env, symbol_short!("import"), &caller, false); - return Err(e); + return Err(RemittanceSplitError::PercentagesDoNotSumTo100); } // 6. Timestamp sanity — reject payloads whose timestamps are in the future. @@ -1633,7 +1551,7 @@ impl RemittanceSplit { return Err(RemittanceSplitError::InvalidDueDate); } - let current_max_id = env + let current_max_id: u32 = env .storage() .instance() .get(&symbol_short!("NEXT_RSCH")) diff --git a/remitwise-common/src/lib.rs b/remitwise-common/src/lib.rs index 40cec114..4350171d 100644 --- a/remitwise-common/src/lib.rs +++ b/remitwise-common/src/lib.rs @@ -767,14 +767,8 @@ mod tests { // Standardized TTL Constants (Ledger Counts) pub const DAY_IN_LEDGERS: u32 = 17280; // ~5 seconds per ledger -pub const INSTANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; // 30 days -pub const INSTANCE_LIFETIME_THRESHOLD: u32 = 1 * DAY_IN_LEDGERS; // 1 day - pub const PERSISTENT_BUMP_AMOUNT: u32 = 60 * DAY_IN_LEDGERS; // 60 days pub const PERSISTENT_LIFETIME_THRESHOLD: u32 = 15 * DAY_IN_LEDGERS; // 15 days -pub const INSTANCE_BUMP_AMOUNT: u32 = PERSISTENT_BUMP_AMOUNT; -pub const INSTANCE_LIFETIME_THRESHOLD: u32 = PERSISTENT_LIFETIME_THRESHOLD; - pub const INSTANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; // 30 days pub const INSTANCE_LIFETIME_THRESHOLD: u32 = 7 * DAY_IN_LEDGERS; // 7 days @@ -782,3 +776,8 @@ pub const INSTANCE_LIFETIME_THRESHOLD: u32 = 7 * DAY_IN_LEDGERS; // 7 days /// Storage TTL for archived contract data (instance/archive bumps). pub const ARCHIVE_BUMP_AMOUNT: u32 = 150 * DAY_IN_LEDGERS; // ~150 days pub const ARCHIVE_LIFETIME_THRESHOLD: u32 = DAY_IN_LEDGERS; // 1 day + +/// Maximum frequency for recurring bills in days +pub const MAX_FREQUENCY_DAYS: u32 = 365; +/// Seconds per day +pub const SECONDS_PER_DAY: u64 = 86400; diff --git a/reporting/README.md b/reporting/README.md index c4892689..30bbf674 100644 --- a/reporting/README.md +++ b/reporting/README.md @@ -9,7 +9,6 @@ Aggregates financial health data from the remittance_split, savings_goals, bill_ - Admin-only archival and cleanup of old reports - Storage TTL management (instance: ~30 days, archive: ~180 days) -<<<<<<< feature/reporting-address-config-integrity ## Dependency contract address integrity Reporting stores five downstream contract IDs (`remittance_split`, `savings_goals`, @@ -40,11 +39,10 @@ is rejected. self-loop. - Soroban/Stellar contract IDs are not an EVM-style “zero address”; “malformed” in this layer means duplicate or self-reference as above. -======= ## Quickstart ->>>>>>> main ```rust +## Quickstart // 1. Initialize client.init(&admin); diff --git a/reporting/src/lib.rs b/reporting/src/lib.rs index fdd83a8c..0668853b 100644 --- a/reporting/src/lib.rs +++ b/reporting/src/lib.rs @@ -532,7 +532,7 @@ impl ReportingContract { period_start: u64, period_end: u64, ) -> RemittanceSummary { - let addresses: ContractAddresses = env + let addresses: Option = env .storage() .instance() .get(&symbol_short!("ADDRS")); @@ -549,9 +549,8 @@ impl ReportingContract { } let addresses = addresses.unwrap(); - let availability = DataAvailability::Complete; + let mut availability = DataAvailability::Complete; - let addresses = addresses.unwrap(); let split_client = RemittanceSplitClient::new(env, &addresses.remittance_split); let mut availability = DataAvailability::Complete; diff --git a/reporting/src/tests.rs b/reporting/src/tests.rs index 45343624..645d7d40 100644 --- a/reporting/src/tests.rs +++ b/reporting/src/tests.rs @@ -2170,17 +2170,6 @@ fn test_get_stored_report_missing_key_returns_none() { let contract_id = env.register_contract(None, ReportingContract); let client = ReportingContractClient::new(&env, &contract_id); let admin = Address::generate(&env); -<<<<<<< feature/reporting-address-config-integrity - let user = Address::generate(&env); - - env.mock_all_auths(); - client.init(&admin); - - // `mock_all_auths` would make any address "authorized"; clear custom auths so `require_auth` fails. - env.mock_auths(&[]); - - client.get_remittance_summary(&user, &1000, &0, &100); -======= client.init(&admin); let user = Address::generate(&env); @@ -2189,5 +2178,4 @@ fn test_get_stored_report_missing_key_returns_none() { result.is_none(), "missing report must return None, not panic" ); ->>>>>>> main } diff --git a/savings_goals/src/lib.rs b/savings_goals/src/lib.rs index 50ea54dd..0a2a7796 100644 --- a/savings_goals/src/lib.rs +++ b/savings_goals/src/lib.rs @@ -8,6 +8,7 @@ use remitwise_common::{EventCategory, EventPriority, RemitwiseEvents}; // Event topics const GOAL_COMPLETED: Symbol = symbol_short!("completed"); +const GOAL_CREATED: Symbol = symbol_short!("created"); #[derive(Clone)] #[contracttype] diff --git a/scenarios/test_snapshots/test_end_to_end_flow.1.json b/scenarios/test_snapshots/test_end_to_end_flow.1.json index 89e2e846..876c1cf3 100644 --- a/scenarios/test_snapshots/test_end_to_end_flow.1.json +++ b/scenarios/test_snapshots/test_end_to_end_flow.1.json @@ -1,39 +1,20 @@ { "generators": { - "address": 10, + "address": 8, "nonce": 0 }, "auth": [ [ [ - "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", { "function": { "contract_fn": { - "contract_address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", - "function_name": "set_admin", - "args": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" - } - ] - } - }, - "sub_invocations": [] - } - ] - ], - [ - [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", - { - "function": { - "contract_fn": { - "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", "function_name": "init", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } ] } @@ -44,30 +25,30 @@ ], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", { "function": { "contract_fn": { - "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", "function_name": "configure_addresses", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" } ] } @@ -78,19 +59,22 @@ ], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", { "function": { "contract_fn": { - "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", "function_name": "initialize_split", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u64": 0 }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, { "u32": 50 }, @@ -112,15 +96,15 @@ ], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", { "function": { "contract_fn": { - "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", "function_name": "create_goal", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "string": "Test Goal" @@ -143,15 +127,15 @@ ], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", { "function": { "contract_fn": { - "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", "function_name": "create_bill", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "string": "Electric" @@ -171,6 +155,7 @@ { "u32": 30 }, + "void", { "string": "USDC" } @@ -181,31 +166,7 @@ } ] ], - [ - [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", - { - "function": { - "contract_fn": { - "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", - "function_name": "get_all_bills_for_owner", - "args": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" - }, - { - "u32": 0 - }, - { - "u32": 50 - } - ] - } - }, - "sub_invocations": [] - } - ] - ] + [] ], "ledger": { "protocol_version": 20, @@ -217,71 +178,10 @@ "min_temp_entry_ttl": 10, "max_entry_ttl": 3110400, "ledger_entries": [ - [ - { - "account": { - "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "account": { - "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", - "balance": 0, - "seq_num": 0, - "num_sub_entries": 0, - "inflation_dest": null, - "flags": 0, - "home_domain": "", - "thresholds": "01010101", - "signers": [], - "ext": "v0" - } - }, - "ext": "v0" - }, - null - ] - ], [ { "contract_data": { - "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", - "key": { - "ledger_key_nonce": { - "nonce": 801925984706572462 - } - }, - "durability": "temporary" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", - "key": { - "ledger_key_nonce": { - "nonce": 801925984706572462 - } - }, - "durability": "temporary", - "val": "void" - } - }, - "ext": "v0" - }, - 3110400 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", "key": "ledger_key_contract_instance", "durability": "persistent" } @@ -292,7 +192,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -314,7 +214,7 @@ "symbol": "caller" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } }, { @@ -381,7 +281,7 @@ "symbol": "owner" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } }, { @@ -407,6 +307,14 @@ "val": { "u64": 1704067200 } + }, + { + "key": { + "symbol": "usdc_contract" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } } ] } @@ -419,7 +327,7 @@ "map": [ { "key": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, "val": { "u64": 1 @@ -462,7 +370,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", "key": "ledger_key_contract_instance", "durability": "persistent" } @@ -473,7 +381,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -534,7 +442,15 @@ "symbol": "owner" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + }, + { + "key": { + "symbol": "tags" + }, + "val": { + "vec": [] } }, { @@ -584,7 +500,7 @@ "map": [ { "key": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, "val": { "vec": [ @@ -610,7 +526,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", "key": "ledger_key_contract_instance", "durability": "persistent" } @@ -621,7 +537,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -677,6 +593,12 @@ "u64": 1704499200 } }, + { + "key": { + "symbol": "external_ref" + }, + "val": "void" + }, { "key": { "symbol": "frequency_days" @@ -706,7 +628,7 @@ "symbol": "owner" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } }, { @@ -736,6 +658,14 @@ "symbol": "schedule_id" }, "val": "void" + }, + { + "key": { + "symbol": "tags" + }, + "val": { + "vec": [] + } } ] } @@ -759,7 +689,7 @@ "map": [ { "key": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, "val": { "i128": { @@ -784,7 +714,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", "key": "ledger_key_contract_instance", "durability": "persistent" } @@ -795,7 +725,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -816,7 +746,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", "key": "ledger_key_contract_instance", "durability": "persistent" } @@ -827,7 +757,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -848,7 +778,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", "key": "ledger_key_contract_instance", "durability": "persistent" } @@ -859,7 +789,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", "key": "ledger_key_contract_instance", "durability": "persistent", "val": { @@ -879,7 +809,7 @@ "symbol": "bill_payments" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" } }, { @@ -887,7 +817,7 @@ "symbol": "family_wallet" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" } }, { @@ -895,7 +825,7 @@ "symbol": "insurance" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" } }, { @@ -903,7 +833,7 @@ "symbol": "remittance_split" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" } }, { @@ -911,7 +841,7 @@ "symbol": "savings_goals" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" } } ] @@ -922,7 +852,7 @@ "symbol": "ADMIN" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } } ] @@ -938,10 +868,10 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", "key": { "ledger_key_nonce": { - "nonce": 1033654523790656264 + "nonce": 801925984706572462 } }, "durability": "temporary" @@ -953,10 +883,10 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", "key": { "ledger_key_nonce": { - "nonce": 1033654523790656264 + "nonce": 801925984706572462 } }, "durability": "temporary", @@ -971,7 +901,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", "key": { "ledger_key_nonce": { "nonce": 5541220902715666415 @@ -986,7 +916,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", "key": { "ledger_key_nonce": { "nonce": 5541220902715666415 @@ -1004,10 +934,10 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { - "nonce": 2032731177588607455 + "nonce": 1033654523790656264 } }, "durability": "temporary" @@ -1019,10 +949,10 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { - "nonce": 2032731177588607455 + "nonce": 1033654523790656264 } }, "durability": "temporary", @@ -1037,10 +967,10 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { - "nonce": 4270020994084947596 + "nonce": 2032731177588607455 } }, "durability": "temporary" @@ -1052,10 +982,10 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { - "nonce": 4270020994084947596 + "nonce": 2032731177588607455 } }, "durability": "temporary", @@ -1070,7 +1000,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": 4837995959683129791 @@ -1085,7 +1015,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": 4837995959683129791 @@ -1102,313 +1032,48 @@ ], [ { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", - "key": { - "ledger_key_nonce": { - "nonce": 8370022561469687789 - } - }, - "durability": "temporary" + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } }, [ { "last_modified_ledger_seq": 0, "data": { - "contract_data": { + "contract_code": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5", - "key": { - "ledger_key_nonce": { - "nonce": 8370022561469687789 - } - }, - "durability": "temporary", - "val": "void" + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" } }, "ext": "v0" }, - 3110409 + 518401 ] - ], - [ - { - "contract_data": { - "contract": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", - "key": "ledger_key_contract_instance", - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000006" + }, + { + "symbol": "init" + } + ], "data": { - "contract_data": { - "ext": "v0", - "contract": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", - "key": "ledger_key_contract_instance", - "durability": "persistent", - "val": { - "contract_instance": { - "executable": "stellar_asset", - "storage": [ - { - "key": { - "symbol": "METADATA" - }, - "val": { - "map": [ - { - "key": { - "symbol": "decimal" - }, - "val": { - "u32": 7 - } - }, - { - "key": { - "symbol": "name" - }, - "val": { - "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" - } - }, - { - "key": { - "symbol": "symbol" - }, - "val": { - "string": "aaa" - } - } - ] - } - }, - { - "key": { - "vec": [ - { - "symbol": "Admin" - } - ] - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" - } - }, - { - "key": { - "vec": [ - { - "symbol": "AssetInfo" - } - ] - }, - "val": { - "vec": [ - { - "symbol": "AlphaNum4" - }, - { - "map": [ - { - "key": { - "symbol": "asset_code" - }, - "val": { - "string": "aaa\\0" - } - }, - { - "key": { - "symbol": "issuer" - }, - "val": { - "bytes": "0000000000000000000000000000000000000000000000000000000000000002" - } - } - ] - } - ] - } - } - ] - } - } - } - }, - "ext": "v0" - }, - 120961 - ] - ], - [ - { - "contract_code": { - "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_code": { - "ext": "v0", - "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "code": "" - } - }, - "ext": "v0" - }, - 518401 - ] - ] - ] - }, - "events": [ - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "48f1b6b8bc0d60f7140dd49b6120fbaf3cdbab2adaeea631313d9f0bae9532f1" - }, - { - "symbol": "init_asset" - } - ], - "data": { - "bytes": "0000000161616100000000000000000000000000000000000000000000000000000000000000000000000002" - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "48f1b6b8bc0d60f7140dd49b6120fbaf3cdbab2adaeea631313d9f0bae9532f1", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "init_asset" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "48f1b6b8bc0d60f7140dd49b6120fbaf3cdbab2adaeea631313d9f0bae9532f1" - }, - { - "symbol": "set_admin" - } - ], - "data": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "48f1b6b8bc0d60f7140dd49b6120fbaf3cdbab2adaeea631313d9f0bae9532f1", - "type_": "contract", - "body": { - "v0": { - "topics": [ - { - "symbol": "set_admin" - }, - { - "address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" - }, - { - "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" - } - ], - "data": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "48f1b6b8bc0d60f7140dd49b6120fbaf3cdbab2adaeea631313d9f0bae9532f1", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "set_admin" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": null, - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000008" - }, - { - "symbol": "init" - } - ], - "data": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } } } @@ -1418,7 +1083,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", "type_": "diagnostic", "body": { "v0": { @@ -1448,7 +1113,7 @@ "symbol": "fn_call" }, { - "bytes": "0000000000000000000000000000000000000000000000000000000000000008" + "bytes": "0000000000000000000000000000000000000000000000000000000000000006" }, { "symbol": "configure_addresses" @@ -1457,22 +1122,22 @@ "data": { "vec": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" } ] } @@ -1484,7 +1149,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", "type_": "contract", "body": { "v0": { @@ -1501,7 +1166,7 @@ } ], "data": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } } } @@ -1511,7 +1176,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", "type_": "diagnostic", "body": { "v0": { @@ -1541,7 +1206,7 @@ "symbol": "fn_call" }, { - "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" }, { "symbol": "initialize_split" @@ -1550,11 +1215,14 @@ "data": { "vec": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u64": 0 }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, { "u32": 50 }, @@ -1577,7 +1245,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000003", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", "type_": "contract", "body": { "v0": { @@ -1594,7 +1262,7 @@ } ], "data": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } } } @@ -1604,7 +1272,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000003", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", "type_": "diagnostic", "body": { "v0": { @@ -1636,7 +1304,7 @@ "symbol": "fn_call" }, { - "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + "bytes": "0000000000000000000000000000000000000000000000000000000000000002" }, { "symbol": "create_goal" @@ -1645,7 +1313,7 @@ "data": { "vec": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "string": "Test Goal" @@ -1669,7 +1337,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000002", "type_": "contract", "body": { "v0": { @@ -1733,7 +1401,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000002", "type_": "contract", "body": { "v0": { @@ -1755,7 +1423,7 @@ "u32": 1 }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } ] } @@ -1767,7 +1435,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000002", "type_": "diagnostic", "body": { "v0": { @@ -1799,7 +1467,7 @@ "symbol": "fn_call" }, { - "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" }, { "symbol": "create_bill" @@ -1808,7 +1476,7 @@ "data": { "vec": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "string": "Electric" @@ -1828,6 +1496,7 @@ { "u32": 30 }, + "void", { "string": "USDC" } @@ -1841,7 +1510,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000003", "type_": "contract", "body": { "v0": { @@ -1865,7 +1534,7 @@ "u32": 1 }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "i128": { @@ -1886,7 +1555,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000003", "type_": "diagnostic", "body": { "v0": { @@ -1918,7 +1587,7 @@ "symbol": "fn_call" }, { - "bytes": "0000000000000000000000000000000000000000000000000000000000000008" + "bytes": "0000000000000000000000000000000000000000000000000000000000000006" }, { "symbol": "get_financial_health_report" @@ -1927,7 +1596,7 @@ "data": { "vec": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "i128": { @@ -1951,7 +1620,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", "type_": "diagnostic", "body": { "v0": { @@ -1960,24 +1629,24 @@ "symbol": "fn_call" }, { - "bytes": "0000000000000000000000000000000000000000000000000000000000000004" + "bytes": "0000000000000000000000000000000000000000000000000000000000000002" }, { "symbol": "get_all_goals" } ], "data": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } } } }, - "failed_call": false + "failed_call": true }, { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000002", "type_": "diagnostic", "body": { "v0": { @@ -2033,7 +1702,15 @@ "symbol": "owner" }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + }, + { + "key": { + "symbol": "tags" + }, + "val": { + "vec": [] } }, { @@ -2068,328 +1745,140 @@ } } }, - "failed_call": false + "failed_call": true }, { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", "type_": "diagnostic", "body": { "v0": { "topics": [ { - "symbol": "fn_call" + "symbol": "error" }, { - "bytes": "0000000000000000000000000000000000000000000000000000000000000005" - }, + "error": { + "object": "unexpected_size" + } + } + ], + "data": { + "string": "differing host map and output slice lengths when unpacking map to slice" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ { - "symbol": "get_unpaid_bills" + "symbol": "log" } ], "data": { "vec": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + "string": "caught panic 'called `Result::unwrap()` on an `Err` value: HostError: Error(Object, UnexpectedSize)\\n\\nEvent log (newest first):\\n 0: [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4, topics:[error, Error(Object, UnexpectedSize)], data:\"differing host map and output slice lengths when unpacking map to slice\"\\n 1: [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4, topics:[fn_return, get_all_goals], data:[{current_amount: 0, id: 1, locked: true, name: \"Test Goal\", owner: CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5, tags: [], target_amount: 1000, target_date: 1706659200, unlock_date: Void}]\\n 2: [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4, topics:[fn_call, Bytes(0000000000000000000000000000000000000000000000000000000000000002), get_all_goals], data:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5\\n 3: [Diagnostic Event] topics:[fn_call, Bytes(0000000000000000000000000000000000000000000000000000000000000006), get_financial_health_report], data:[CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5, 5000, 1704067200, 1704931200]\\n 4: [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M, topics:[fn_return, create_bill], data:1\\n 5: [Contract Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M, topics:[Remitwise, 1, 1, created], data:[1, CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5, 150, 1704499200]\\n 6: [Diagnostic Event] topics:[fn_call, Bytes(0000000000000000000000000000000000000000000000000000000000000003), create_bill], data:[CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5, \"Electric\", 150, 1704499200, true, 30, Void, \"USDC\"]\\n 7: [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4, topics:[fn_return, create_goal], data:1\\n 8: [Contract Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4, topics:[savings, [GoalCreated]], data:[1, CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5]\\n 9: [Contract Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4, topics:[created], data:{goal_id: 1, name: \"Test Goal\", target_amount: 1000, target_date: 1706659200, timestamp: 1704067200}\\n 10: [Diagnostic Event] topics:[fn_call, Bytes(0000000000000000000000000000000000000000000000000000000000000002), create_goal], data:[CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5, \"Test Goal\", 1000, 1706659200]\\n 11: [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM, topics:[fn_return, initialize_split], data:true\\n 12: [Contract Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM, topics:[split, [Initialized]], data:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5\\n 13: [Diagnostic Event] topics:[fn_call, Bytes(0000000000000000000000000000000000000000000000000000000000000001), initialize_split], data:[CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5, 0, CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5, 50, 30, 15, 5]\\n 14: [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4, topics:[fn_return, configure_addresses], data:Void\\n 15: [Contract Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4, topics:[report, [AddressesConfigured]], data:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM\\n 16: [Diagnostic Event] topics:[fn_call, Bytes(0000000000000000000000000000000000000000000000000000000000000006), configure_addresses], data:[CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM, CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM, CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4, CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M, CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4, CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM]\\n 17: [Diagnostic Event] contract:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4, topics:[fn_return, init], data:Void\\n 18: [Diagnostic Event] topics:[fn_call, Bytes(0000000000000000000000000000000000000000000000000000000000000006), init], data:CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM\\n\\nBacktrace (newest first):\\n 0: backtrace::backtrace::libunwind::trace\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/backtrace-0.3.76/src/backtrace/libunwind.rs:117:9\\n backtrace::backtrace::trace_unsynchronized\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/backtrace-0.3.76/src/backtrace/mod.rs:66:14\\n 1: backtrace::backtrace::trace\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/backtrace-0.3.76/src/backtrace/mod.rs:53:14\\n 2: backtrace::capture::Backtrace::create\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/backtrace-0.3.76/src/capture.rs:294:9\\n 3: backtrace::capture::Backtrace::new_unresolved\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/backtrace-0.3.76/src/capture.rs:289:9\\n 4: soroban_env_host::host::error::::maybe_get_debug_info::{{closure}}\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host/error.rs:293:37\\n 5: soroban_env_host::budget::Budget::with_shadow_mode\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/budget.rs:972:21\\n 6: soroban_env_host::host::Host::with_debug_mode\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host.rs:615:42\\n 7: soroban_env_host::host::error::::maybe_get_debug_info\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host/error.rs:290:18\\n 8: soroban_env_host::host::error::::error::{{closure}}\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host/error.rs:274:28\\n 9: soroban_env_host::budget::Budget::with_shadow_mode\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/budget.rs:972:21\\n 10: soroban_env_host::host::Host::with_debug_mode\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host.rs:615:42\\n 11: soroban_env_host::host::error::::error\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host/error.rs:261:14\\n 12: soroban_env_host::host::error::::err\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host/error.rs:251:14\\n 13: ::map_unpack_to_slice::{{closure}}\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host.rs:938:33\\n 14: soroban_env_host::host_object::::visit_obj::{{closure}}\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host_object.rs:458:26\\n 15: soroban_env_host::host_object::::visit_obj_untyped\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host_object.rs:421:13\\n 16: soroban_env_host::host_object::::visit_obj\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host_object.rs:449:14\\n 17: ::map_unpack_to_slice\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host.rs:936:14\\n 18: ::map_unpack_to_slice\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-sdk-21.7.7/src/env.rs:1627:14\\n 19: >::try_from_val\\n at /Users/user/Desktop/od-hunter/Remitwise-Contracts/reporting/src/lib.rs:237:1\\n 20: soroban_sdk::vec::Vec::try_get_unchecked\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-sdk-21.7.7/src/vec.rs:428:9\\n as core::iter::traits::iterator::Iterator>::next\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-sdk-21.7.7/src/vec.rs:969:32\\n 21: as core::iter::traits::iterator::Iterator>::next\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-sdk-21.7.7/src/iter.rs:43:19\\n reporting::ReportingContract::calculate_health_score\\n at /Users/user/Desktop/od-hunter/Remitwise-Contracts/reporting/src/lib.rs:606:21\\n 22: reporting::ReportingContract::get_financial_health_report\\n at /Users/user/Desktop/od-hunter/Remitwise-Contracts/reporting/src/lib.rs:662:13\\n 23: reporting::__get_financial_health_report::invoke_raw\\n at /Users/user/Desktop/od-hunter/Remitwise-Contracts/reporting/src/lib.rs:300:1\\n 24: reporting::__get_financial_health_report::invoke_raw_slice\\n at /Users/user/Desktop/od-hunter/Remitwise-Contracts/reporting/src/lib.rs:300:1\\n 25: core::ops::function::Fn::call\\n at /Users/user/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:79:5\\n 26: reporting::__reportingcontract_fn_set_registry::call::{{closure}}\\n at /Users/user/Desktop/od-hunter/Remitwise-Contracts/reporting/src/lib.rs:297:1\\n 27: core::option::Option::map\\n at /Users/user/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/option.rs:1164:29\\n 28: reporting::__reportingcontract_fn_set_registry::call\\n at /Users/user/Desktop/od-hunter/Remitwise-Contracts/reporting/src/lib.rs:297:1\\n 29: ::call\\n at /Users/user/Desktop/od-hunter/Remitwise-Contracts/reporting/src/lib.rs:297:1\\n 30: as soroban_env_host::host::frame::ContractFunctionSet>::call\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-sdk-21.7.7/src/env.rs:628:24\\n 31: soroban_env_host::host::frame::::call_n_internal::{{closure}}::{{closure}}\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host/frame.rs:868:64\\n 32: core::ops::function::FnOnce::call_once\\n at /Users/user/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5\\n 33: as core::ops::function::FnOnce<()>>::call_once\\n at /Users/user/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:274:9\\n 34: std::panicking::catch_unwind::do_call\\n at /Users/user/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:590:40\\n 35: ___rust_try\\n 36: std::panicking::catch_unwind\\n at /Users/user/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:553:19\\n std::panic::catch_unwind\\n at /Users/user/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14\\n 37: soroban_env_host::testutils::call_with_suppressed_panic_hook\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/testutils.rs:57:15\\n 38: soroban_env_host::host::frame::::call_n_internal::{{closure}}\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host/frame.rs:870:25\\n 39: soroban_env_host::host::frame::::with_frame\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host/frame.rs:453:19\\n 40: soroban_env_host::host::frame::::call_n_internal\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host/frame.rs:845:29\\n 41: ::call\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-21.2.1/src/host.rs:2304:24\\n 42: soroban_env_common::vmcaller_env::::call\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-common-21.2.1/src/vmcaller_env.rs:195:84\\n 43: ::call\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-sdk-21.7.7/src/env.rs:1667:64\\n 44: soroban_sdk::env::Env::invoke_contract\\n at /Users/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-sdk-21.7.7/src/env.rs:379:18\\n 45: reporting::ReportingContractClient::get_financial_health_report\\n at /Users/user/Desktop/od-hunter/Remitwise-Contracts/reporting/src/lib.rs:300:1\\n 46: flow::test_end_to_end_flow\\n at tests/flow.rs:105:35\\n 47: flow::test_end_to_end_flow::{{closure}}\\n at tests/flow.rs:13:26\\n 48: core::ops::function::FnOnce::call_once\\n at /Users/user/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5\\n\\n' from contract function 'Symbol(obj#201)'" }, { - "u32": 0 + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { - "u32": 50 + "i128": { + "hi": 0, + "lo": 5000 + } + }, + { + "u64": 1704067200 + }, + { + "u64": 1704931200 } ] } } } }, - "failed_call": false + "failed_call": true }, { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", "type_": "diagnostic", "body": { "v0": { "topics": [ { - "symbol": "fn_return" + "symbol": "error" }, { - "symbol": "get_unpaid_bills" + "error": { + "wasm_vm": "invalid_action" + } } ], "data": { - "map": [ - { - "key": { - "symbol": "count" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "items" - }, - "val": { - "vec": [ - { - "map": [ - { - "key": { - "symbol": "amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 150 - } - } - }, - { - "key": { - "symbol": "created_at" - }, - "val": { - "u64": 1704067200 - } - }, - { - "key": { - "symbol": "currency" - }, - "val": { - "string": "USDC" - } - }, - { - "key": { - "symbol": "due_date" - }, - "val": { - "u64": 1704499200 - } - }, - { - "key": { - "symbol": "frequency_days" - }, - "val": { - "u32": 30 - } - }, - { - "key": { - "symbol": "id" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "name" - }, - "val": { - "string": "Electric" - } - }, - { - "key": { - "symbol": "owner" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" - } - }, - { - "key": { - "symbol": "paid" - }, - "val": { - "bool": false - } - }, - { - "key": { - "symbol": "paid_at" - }, - "val": "void" - }, - { - "key": { - "symbol": "recurring" - }, - "val": { - "bool": true - } - }, - { - "key": { - "symbol": "schedule_id" - }, - "val": "void" - } - ] - } - ] - } - }, - { - "key": { - "symbol": "next_cursor" - }, - "val": { - "u32": 0 - } - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000006" - }, - { - "symbol": "get_active_policies" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" - }, - { - "u32": 0 - }, - { - "u32": 50 - } - ] + "string": "caught error from function" } } } }, - "failed_call": false + "failed_call": true }, { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "contract_id": null, "type_": "diagnostic", "body": { "v0": { "topics": [ { - "symbol": "fn_return" + "symbol": "error" }, { - "symbol": "get_active_policies" - } - ], - "data": { - "map": [ - { - "key": { - "symbol": "count" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "items" - }, - "val": { - "vec": [] - } - }, - { - "key": { - "symbol": "next_cursor" - }, - "val": { - "u32": 0 - } + "error": { + "wasm_vm": "invalid_action" } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000003" - }, - { - "symbol": "get_split" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000003", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "get_split" } ], "data": { "vec": [ { - "u32": 50 - }, - { - "u32": 30 + "string": "contract call failed" }, { - "u32": 15 + "symbol": "get_financial_health_report" }, { - "u32": 5 + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "i128": { + "hi": 0, + "lo": 5000 + } + }, + { + "u64": 1704067200 + }, + { + "u64": 1704931200 + } + ] } ] } @@ -2401,1153 +1890,22 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", + "contract_id": null, "type_": "diagnostic", "body": { "v0": { "topics": [ { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + "symbol": "error" }, { - "symbol": "calculate_split" - } - ], - "data": { - "i128": { - "hi": 0, - "lo": 5000 - } - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000003", - "type_": "contract", - "body": { - "v0": { - "topics": [ - { - "symbol": "calc" + "error": { + "wasm_vm": "invalid_action" + } } ], "data": { - "map": [ - { - "key": { - "symbol": "bills_amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 750 - } - } - }, - { - "key": { - "symbol": "insurance_amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 250 - } - } - }, - { - "key": { - "symbol": "savings_amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 1500 - } - } - }, - { - "key": { - "symbol": "spending_amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 2500 - } - } - }, - { - "key": { - "symbol": "timestamp" - }, - "val": { - "u64": 1704931200 - } - }, - { - "key": { - "symbol": "total_amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 5000 - } - } - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000003", - "type_": "contract", - "body": { - "v0": { - "topics": [ - { - "symbol": "split" - }, - { - "vec": [ - { - "symbol": "Calculated" - } - ] - } - ], - "data": { - "i128": { - "hi": 0, - "lo": 5000 - } - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000003", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "calculate_split" - } - ], - "data": { - "vec": [ - { - "i128": { - "hi": 0, - "lo": 2500 - } - }, - { - "i128": { - "hi": 0, - "lo": 1500 - } - }, - { - "i128": { - "hi": 0, - "lo": 750 - } - }, - { - "i128": { - "hi": 0, - "lo": 250 - } - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000004" - }, - { - "symbol": "get_all_goals" - } - ], - "data": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "get_all_goals" - } - ], - "data": { - "vec": [ - { - "map": [ - { - "key": { - "symbol": "current_amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 0 - } - } - }, - { - "key": { - "symbol": "id" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "locked" - }, - "val": { - "bool": true - } - }, - { - "key": { - "symbol": "name" - }, - "val": { - "string": "Test Goal" - } - }, - { - "key": { - "symbol": "owner" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" - } - }, - { - "key": { - "symbol": "target_amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 1000 - } - } - }, - { - "key": { - "symbol": "target_date" - }, - "val": { - "u64": 1706659200 - } - }, - { - "key": { - "symbol": "unlock_date" - }, - "val": "void" - } - ] - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000005" - }, - { - "symbol": "get_all_bills_for_owner" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" - }, - { - "u32": 0 - }, - { - "u32": 50 - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "get_all_bills_for_owner" - } - ], - "data": { - "map": [ - { - "key": { - "symbol": "count" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "items" - }, - "val": { - "vec": [ - { - "map": [ - { - "key": { - "symbol": "amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 150 - } - } - }, - { - "key": { - "symbol": "created_at" - }, - "val": { - "u64": 1704067200 - } - }, - { - "key": { - "symbol": "currency" - }, - "val": { - "string": "USDC" - } - }, - { - "key": { - "symbol": "due_date" - }, - "val": { - "u64": 1704499200 - } - }, - { - "key": { - "symbol": "frequency_days" - }, - "val": { - "u32": 30 - } - }, - { - "key": { - "symbol": "id" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "name" - }, - "val": { - "string": "Electric" - } - }, - { - "key": { - "symbol": "owner" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" - } - }, - { - "key": { - "symbol": "paid" - }, - "val": { - "bool": false - } - }, - { - "key": { - "symbol": "paid_at" - }, - "val": "void" - }, - { - "key": { - "symbol": "recurring" - }, - "val": { - "bool": true - } - }, - { - "key": { - "symbol": "schedule_id" - }, - "val": "void" - } - ] - } - ] - } - }, - { - "key": { - "symbol": "next_cursor" - }, - "val": { - "u32": 0 - } - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000006" - }, - { - "symbol": "get_active_policies" - } - ], - "data": { - "vec": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" - }, - { - "u32": 0 - }, - { - "u32": 50 - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "get_active_policies" - } - ], - "data": { - "map": [ - { - "key": { - "symbol": "count" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "items" - }, - "val": { - "vec": [] - } - }, - { - "key": { - "symbol": "next_cursor" - }, - "val": { - "u32": 0 - } - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000006" - }, - { - "symbol": "get_total_monthly_premium" - } - ], - "data": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "get_total_monthly_premium" - } - ], - "data": { - "i128": { - "hi": 0, - "lo": 0 - } - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", - "type_": "contract", - "body": { - "v0": { - "topics": [ - { - "symbol": "report" - }, - { - "vec": [ - { - "symbol": "ReportGenerated" - } - ] - } - ], - "data": { - "u64": 1704931200 - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000008", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "get_financial_health_report" - } - ], - "data": { - "map": [ - { - "key": { - "symbol": "bill_compliance" - }, - "val": { - "map": [ - { - "key": { - "symbol": "compliance_percentage" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "overdue_bills" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "paid_amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 0 - } - } - }, - { - "key": { - "symbol": "paid_bills" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "period_end" - }, - "val": { - "u64": 1704931200 - } - }, - { - "key": { - "symbol": "period_start" - }, - "val": { - "u64": 1704067200 - } - }, - { - "key": { - "symbol": "total_amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 150 - } - } - }, - { - "key": { - "symbol": "total_bills" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "unpaid_amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 150 - } - } - }, - { - "key": { - "symbol": "unpaid_bills" - }, - "val": { - "u32": 1 - } - } - ] - } - }, - { - "key": { - "symbol": "generated_at" - }, - "val": { - "u64": 1704931200 - } - }, - { - "key": { - "symbol": "health_score" - }, - "val": { - "map": [ - { - "key": { - "symbol": "bills_score" - }, - "val": { - "u32": 20 - } - }, - { - "key": { - "symbol": "insurance_score" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "savings_score" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "score" - }, - "val": { - "u32": 20 - } - } - ] - } - }, - { - "key": { - "symbol": "insurance_report" - }, - "val": { - "map": [ - { - "key": { - "symbol": "active_policies" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "annual_premium" - }, - "val": { - "i128": { - "hi": 0, - "lo": 0 - } - } - }, - { - "key": { - "symbol": "coverage_to_premium_ratio" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "monthly_premium" - }, - "val": { - "i128": { - "hi": 0, - "lo": 0 - } - } - }, - { - "key": { - "symbol": "period_end" - }, - "val": { - "u64": 1704931200 - } - }, - { - "key": { - "symbol": "period_start" - }, - "val": { - "u64": 1704067200 - } - }, - { - "key": { - "symbol": "total_coverage" - }, - "val": { - "i128": { - "hi": 0, - "lo": 0 - } - } - } - ] - } - }, - { - "key": { - "symbol": "remittance_summary" - }, - "val": { - "map": [ - { - "key": { - "symbol": "category_breakdown" - }, - "val": { - "vec": [ - { - "map": [ - { - "key": { - "symbol": "amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 2500 - } - } - }, - { - "key": { - "symbol": "category" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "percentage" - }, - "val": { - "u32": 50 - } - } - ] - }, - { - "map": [ - { - "key": { - "symbol": "amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 1500 - } - } - }, - { - "key": { - "symbol": "category" - }, - "val": { - "u32": 2 - } - }, - { - "key": { - "symbol": "percentage" - }, - "val": { - "u32": 30 - } - } - ] - }, - { - "map": [ - { - "key": { - "symbol": "amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 750 - } - } - }, - { - "key": { - "symbol": "category" - }, - "val": { - "u32": 3 - } - }, - { - "key": { - "symbol": "percentage" - }, - "val": { - "u32": 15 - } - } - ] - }, - { - "map": [ - { - "key": { - "symbol": "amount" - }, - "val": { - "i128": { - "hi": 0, - "lo": 250 - } - } - }, - { - "key": { - "symbol": "category" - }, - "val": { - "u32": 4 - } - }, - { - "key": { - "symbol": "percentage" - }, - "val": { - "u32": 5 - } - } - ] - } - ] - } - }, - { - "key": { - "symbol": "period_end" - }, - "val": { - "u64": 1704931200 - } - }, - { - "key": { - "symbol": "period_start" - }, - "val": { - "u64": 1704067200 - } - }, - { - "key": { - "symbol": "total_allocated" - }, - "val": { - "i128": { - "hi": 0, - "lo": 5000 - } - } - }, - { - "key": { - "symbol": "total_received" - }, - "val": { - "i128": { - "hi": 0, - "lo": 5000 - } - } - } - ] - } - }, - { - "key": { - "symbol": "savings_report" - }, - "val": { - "map": [ - { - "key": { - "symbol": "completed_goals" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "completion_percentage" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "period_end" - }, - "val": { - "u64": 1704931200 - } - }, - { - "key": { - "symbol": "period_start" - }, - "val": { - "u64": 1704067200 - } - }, - { - "key": { - "symbol": "total_goals" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "total_saved" - }, - "val": { - "i128": { - "hi": 0, - "lo": 0 - } - } - }, - { - "key": { - "symbol": "total_target" - }, - "val": { - "i128": { - "hi": 0, - "lo": 1000 - } - } - } - ] - } - } - ] + "string": "escalating error to panic" } } } diff --git a/testutils/src/lib.rs b/testutils/src/lib.rs index 9401034f..171615ca 100644 --- a/testutils/src/lib.rs +++ b/testutils/src/lib.rs @@ -15,8 +15,9 @@ pub fn set_ledger_time(env: &Env, sequence_number: u32, timestamp: u64) { base_reserve: 10, min_temp_entry_ttl: 1, min_persistent_entry_ttl: 1, - // Must exceed any contract bump TTL used in tests (e.g. 518,400). - max_entry_ttl: 3_000_000, + // Ensure TTL-related tests can observe contract TTL extension logic. + // For example, family_wallet expects INSTANCE_BUMP_AMOUNT=518_400 ledgers. + max_entry_ttl: 2_000_000, }); }