diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7730091..ba5cbd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,8 @@ name: Soroban Smart Contracts CI on: push: - branches: ["main"] + branches: ["**"] pull_request: - branches: ["main"] permissions: contents: read diff --git a/Cargo.toml b/Cargo.toml index 37bf764..970f94d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "data_migration", "reporting", "orchestrator", + "global_config", "cli", "scenarios", diff --git a/bill_payments/Cargo.toml b/bill_payments/Cargo.toml index ec3dbff..b3f4b26 100644 --- a/bill_payments/Cargo.toml +++ b/bill_payments/Cargo.toml @@ -13,6 +13,7 @@ remitwise-common = { path = "../remitwise-common" } [dev-dependencies] proptest = "1.10.0" soroban-sdk = { version = "21.0.0", features = ["testutils"] } +global_config = { path = "../global_config" } testutils = { path = "../testutils" } diff --git a/bill_payments/src/lib.rs b/bill_payments/src/lib.rs index cd64fec..1f86058 100644 --- a/bill_payments/src/lib.rs +++ b/bill_payments/src/lib.rs @@ -8,8 +8,8 @@ use remitwise_common::{ }; use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Map, String, - Symbol, Vec, + contract, contractclient, contracterror, contractimpl, contracttype, symbol_short, Address, + Env, Map, String, Symbol, Vec, }; #[derive(Clone, Debug)] @@ -76,6 +76,8 @@ pub enum Error { BatchTooLarge = 9, BatchValidationFailed = 10, InvalidLimit = 11, + /// A config-contract sync was requested but no config contract address is stored. + ConfigContractNotSet = 12, InvalidDueDate = 12, InvalidTag = 12, EmptyTags = 13, @@ -122,6 +124,26 @@ pub struct StorageStats { pub last_updated: u64, } +// ----------------------------------------------------------------------- +// Cross-contract interface: GlobalConfig +// ----------------------------------------------------------------------- + +/// Mirrors `global_config::ConfigValue` for cross-contract deserialization. +/// The XDR encoding is identical as long as variants appear in the same order. +#[contracttype] +#[derive(Clone, Debug)] +pub enum GlobalConfigValue { + U32(u32), + I128(i128), + Bool(bool), +} + +/// Minimal client interface for the GlobalConfig contract. +#[contractclient(name = "GlobalConfigClient")] +pub trait GlobalConfigTrait { + fn get_config(env: Env, key: Symbol) -> Option; +} + #[contract] pub struct BillPayments; @@ -175,8 +197,26 @@ impl BillPayments { Ok(()) } - /// Clamp a caller-supplied limit to [1, MAX_PAGE_LIMIT]. + /// Clamp a caller-supplied limit to [1, effective_max_page_limit]. /// A value of 0 is treated as DEFAULT_PAGE_LIMIT. + /// + /// The ceiling is the cached value synced from the GlobalConfig contract + /// (key `"max_page_lmt"`), falling back to the hard-coded `MAX_PAGE_LIMIT` + /// when no sync has been performed. + fn clamp_limit(env: &Env, limit: u32) -> u32 { + let max: u32 = env + .storage() + .instance() + .get(&symbol_short!("PG_LIMIT")) + .unwrap_or(MAX_PAGE_LIMIT); + if limit == 0 { + DEFAULT_PAGE_LIMIT.min(max) + } else if limit > max { + max + } else { + limit + } + } // ----------------------------------------------------------------------- // Pause / upgrade @@ -365,6 +405,61 @@ impl BillPayments { Ok(()) } + // ----------------------------------------------------------------------- + // Global config integration + // ----------------------------------------------------------------------- + + /// Store the address of the deployed GlobalConfig contract. + /// + /// Only the upgrade admin may call. Once set, `sync_limits_from_config` + /// can be used to pull limit values into local storage. + pub fn set_config_contract(env: Env, caller: Address, config_id: Address) -> Result<(), Error> { + caller.require_auth(); + let admin = Self::get_upgrade_admin(&env).ok_or(Error::Unauthorized)?; + if admin != caller { + return Err(Error::Unauthorized); + } + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + env.storage() + .instance() + .set(&symbol_short!("CFG_CTR"), &config_id); + Ok(()) + } + + /// Read `max_page_lmt` from the configured GlobalConfig contract and cache + /// it locally under `"PG_LIMIT"`. + /// + /// After a successful sync, `clamp_limit` uses the cached value instead of + /// the hard-coded `MAX_PAGE_LIMIT` constant. Only the upgrade admin may + /// call. + pub fn sync_limits_from_config(env: Env, caller: Address) -> Result<(), Error> { + caller.require_auth(); + let admin = Self::get_upgrade_admin(&env).ok_or(Error::Unauthorized)?; + if admin != caller { + return Err(Error::Unauthorized); + } + let config_id: Address = env + .storage() + .instance() + .get(&symbol_short!("CFG_CTR")) + .ok_or(Error::ConfigContractNotSet)?; + + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + let config_client = GlobalConfigClient::new(&env, &config_id); + let key = Symbol::new(&env, "max_page_lmt"); + if let Some(GlobalConfigValue::U32(limit)) = config_client.get_config(&key) { + env.storage() + .instance() + .set(&symbol_short!("PG_LIMIT"), &limit); + } + Ok(()) + } + // ----------------------------------------------------------------------- // Core bill operations // ----------------------------------------------------------------------- @@ -1211,7 +1306,7 @@ impl BillPayments { cursor: u32, limit: u32, ) -> BillPage { - let limit = Self::clamp_limit(limit); + let limit = Self::clamp_limit(&env, limit); let bills: Map = env .storage() .instance() @@ -1245,7 +1340,7 @@ impl BillPayments { cursor: u32, limit: u32, ) -> BillPage { - let limit = Self::clamp_limit(limit); + let limit = Self::clamp_limit(&env, limit); let bills: Map = env .storage() .instance() diff --git a/bill_payments/tests/config_integration_tests.rs b/bill_payments/tests/config_integration_tests.rs new file mode 100644 index 0000000..1aefa92 --- /dev/null +++ b/bill_payments/tests/config_integration_tests.rs @@ -0,0 +1,201 @@ +use bill_payments::{BillPayments, BillPaymentsClient, MAX_PAGE_LIMIT}; +use global_config::{ConfigValue, GlobalConfig, GlobalConfigClient}; +use soroban_sdk::testutils::{Address as AddressTrait, EnvTestConfig, Ledger, LedgerInfo}; +use soroban_sdk::{Address, Env, String, Symbol}; + +fn make_env() -> Env { + let env = Env::new_with_config(EnvTestConfig { + capture_snapshot_at_drop: false, + }); + env.mock_all_auths(); + let proto = env.ledger().protocol_version(); + env.ledger().set(LedgerInfo { + protocol_version: proto, + sequence_number: 1, + timestamp: 1_700_000_000, + network_id: [0; 32], + base_reserve: 10, + min_temp_entry_ttl: 1, + min_persistent_entry_ttl: 1, + max_entry_ttl: 100_000, + }); + env +} + +/// Deploy both contracts and wire them together. +/// Returns (bills_client, config_client, upgrade_admin). +fn setup(env: &Env) -> (BillPaymentsClient<'_>, GlobalConfigClient<'_>, Address) { + let bills_id = env.register_contract(None, BillPayments); + let bills = BillPaymentsClient::new(env, &bills_id); + + let cfg_id = env.register_contract(None, GlobalConfig); + let cfg = GlobalConfigClient::new(env, &cfg_id); + + // Bootstrap the upgrade admin for bill_payments + let upg_admin = Address::generate(env); + bills.set_upgrade_admin(&upg_admin, &upg_admin); + + // Initialize global config + let cfg_admin = Address::generate(env); + cfg.initialize(&cfg_admin); + + // Wire config into bill_payments + bills.set_config_contract(&upg_admin, &cfg_id); + + (bills, cfg, upg_admin) +} + +// ----------------------------------------------------------------------- +// set_config_contract +// ----------------------------------------------------------------------- + +#[test] +fn test_set_config_contract_requires_upgrade_admin() { + let env = make_env(); + let bills_id = env.register_contract(None, BillPayments); + let bills = BillPaymentsClient::new(&env, &bills_id); + let upg_admin = Address::generate(&env); + bills.set_upgrade_admin(&upg_admin, &upg_admin); + + let cfg_id = env.register_contract(None, GlobalConfig); + let stranger = Address::generate(&env); + + let result = bills.try_set_config_contract(&stranger, &cfg_id); + assert!(result.is_err()); +} + +// ----------------------------------------------------------------------- +// sync_limits_from_config +// ----------------------------------------------------------------------- + +#[test] +fn test_sync_without_config_contract_errors() { + let env = make_env(); + let bills_id = env.register_contract(None, BillPayments); + let bills = BillPaymentsClient::new(&env, &bills_id); + let upg_admin = Address::generate(&env); + bills.set_upgrade_admin(&upg_admin, &upg_admin); + + // No set_config_contract called — sync must fail + let result = bills.try_sync_limits_from_config(&upg_admin); + assert!(result.is_err()); +} + +#[test] +fn test_sync_reads_max_page_limit_from_config() { + let env = make_env(); + let (bills, cfg, upg_admin) = setup(&env); + + // Config admin is whoever initialized the config contract + let cfg_admin = cfg.get_admin().unwrap(); + + // Push a custom limit into global config + cfg.set_config( + &cfg_admin, + &Symbol::new(&env, "max_page_lmt"), + &ConfigValue::U32(30), + ); + + // Sync to bill_payments + bills.sync_limits_from_config(&upg_admin); + + // Create 40 bills then page with limit=50 — should receive at most 30 + let owner = Address::generate(&env); + let name = String::from_str(&env, "Test"); + for _ in 0..40 { + bills.create_bill( + &owner, + &name, + &100i128, + &1_800_000_000u64, + &false, + &0u32, + &String::from_str(&env, ""), + ); + } + + let page = bills.get_unpaid_bills(&owner, &0, &50); + assert_eq!(page.count, 30); + assert!(page.next_cursor > 0); +} + +#[test] +fn test_sync_missing_key_leaves_default_unchanged() { + let env = make_env(); + let (bills, _cfg, upg_admin) = setup(&env); + + // Config has no "max_page_lmt" key — sync should not error + bills.sync_limits_from_config(&upg_admin); + + // Create 60 bills and page: should still get MAX_PAGE_LIMIT (50) + let owner = Address::generate(&env); + let name = String::from_str(&env, "Bill"); + for _ in 0..60 { + bills.create_bill( + &owner, + &name, + &1i128, + &1_800_000_000u64, + &false, + &0u32, + &String::from_str(&env, ""), + ); + } + + let page = bills.get_unpaid_bills(&owner, &0, &99); + assert_eq!(page.count, MAX_PAGE_LIMIT); +} + +#[test] +fn test_sync_can_be_updated() { + let env = make_env(); + let (bills, cfg, upg_admin) = setup(&env); + + let cfg_admin = cfg.get_admin().unwrap(); + + // First sync: limit = 10 + cfg.set_config( + &cfg_admin, + &Symbol::new(&env, "max_page_lmt"), + &ConfigValue::U32(10), + ); + bills.sync_limits_from_config(&upg_admin); + + let owner = Address::generate(&env); + let name = String::from_str(&env, "Bill"); + for _ in 0..20 { + bills.create_bill( + &owner, + &name, + &1i128, + &1_800_000_000u64, + &false, + &0u32, + &String::from_str(&env, ""), + ); + } + + let page = bills.get_unpaid_bills(&owner, &0, &50); + assert_eq!(page.count, 10); + + // Second sync: limit bumped to 15 + cfg.set_config( + &cfg_admin, + &Symbol::new(&env, "max_page_lmt"), + &ConfigValue::U32(15), + ); + bills.sync_limits_from_config(&upg_admin); + + let page2 = bills.get_unpaid_bills(&owner, &0, &50); + assert_eq!(page2.count, 15); +} + +#[test] +fn test_sync_requires_upgrade_admin() { + let env = make_env(); + let (bills, _cfg, _upg_admin) = setup(&env); + + let stranger = Address::generate(&env); + let result = bills.try_sync_limits_from_config(&stranger); + assert!(result.is_err()); +} diff --git a/bill_payments/tests/stress_test_large_amounts.rs b/bill_payments/tests/stress_test_large_amounts.rs index e422188..7dca595 100644 --- a/bill_payments/tests/stress_test_large_amounts.rs +++ b/bill_payments/tests/stress_test_large_amounts.rs @@ -324,15 +324,15 @@ fn test_batch_pay_large_bills() { // let large_amount = i128::MAX / 2; -// client.create_bill( -// &owner, -// &String::from_str(&env, "Overdue Large"), -// &large_amount, -// &1000000, // Past due -// &false, -// &0, -// &String::from_str(&env, "XLM"), -// ); + client.create_bill( + &owner, + &String::from_str(&env, "Overdue Large"), + &large_amount, + &1000000, // Past due + &false, + &0, + &String::from_str(&env, "USD"), + ); // let page = client.get_overdue_bills(&0, &10); // assert_eq!(page.count, 1); diff --git a/docs/storage-limits.md b/docs/storage-limits.md new file mode 100644 index 0000000..8429e4e --- /dev/null +++ b/docs/storage-limits.md @@ -0,0 +1,179 @@ +# Storage Limits & TTL Recommendations + +> Issue #178 – Stress Test Storage Limits and TTL + +--- + +## Architecture overview + +All active contracts in this workspace store their primary data inside +**Soroban instance storage** — a single ledger entry shared by the whole +contract instance. Instance storage is a `Map` that is loaded +and saved together atomically on every invocation. + +| Contract | Key | Type | +|---|---|---| +| `bill_payments` | `BILLS` | `Map` | +| `savings_goals` | `GOALS` | `Map` | +| `insurance` | `POLICIES` | `Map` | +| `bill_payments` | `ARCH_BILL` | `Map` (archive storage) | + +Because every entity in a contract shares the same instance storage entry, +all entries are read and written together on every state-changing call. This +keeps the logic simple but means **every entity added to a contract increases +the size of that contract's instance storage entry**, which translates to +higher ledger rent costs. + +--- + +## TTL constants + +All contracts use identical TTL parameters: + +| Constant | Value (ledgers) | Approximate wall-clock | +|---|---|---| +| `INSTANCE_LIFETIME_THRESHOLD` | 17,280 | ~1 day | +| `INSTANCE_BUMP_AMOUNT` | 518,400 | ~30 days | +| `ARCHIVE_LIFETIME_THRESHOLD` | 17,280 | ~1 day | +| `ARCHIVE_BUMP_AMOUNT` | 2,592,000 | ~180 days | + +`extend_ttl(threshold, bump)` is called at the start of every **write** +operation. It is a no-op when the current TTL already exceeds `threshold`, +so repeated rapid writes do not redundantly extend the TTL. + +**Read-only** operations (`get_bill`, `get_goals`, `get_active_policies`, +etc.) do **not** bump the TTL. If only reads are made for longer than 30 +days the entry will eventually expire unless a write is performed. + +--- + +## Known limits + +| Limit | Value | Location | +|---|---|---| +| Page size cap (`MAX_PAGE_LIMIT`) | 50 entries | all contracts | +| Default page size | 20 entries | all contracts | +| Batch operation cap (`MAX_BATCH_SIZE`) | 50 items | `bill_payments`, `savings_goals`, `insurance` | +| Audit log rotation (`MAX_AUDIT_ENTRIES`) | 100 entries | `savings_goals`, `orchestrator` | +| Archived bill TTL | ~180 days | `bill_payments` | + +There is **no hard cap on the total number of entities** per user or overall. +The Map grows unboundedly. Stellar validators enforce a per-ledger-entry size +limit; entries exceeding this will be rejected at the protocol level. + +--- + +## Stress test results + +The benchmarks in `*/tests/stress_tests.rs` use an **unlimited budget** so +the numbers below reflect logical work, not on-chain limits (Stellar enforces +~100 M CPU instructions per transaction). These are reference points for +estimating how close operations come to real limits. + +Run the benchmarks with: + +```bash +cargo test -p bill_payments --test stress_tests -- bench_ --nocapture +cargo test -p savings_goals --test stress_tests -- bench_ --nocapture +cargo test -p insurance --test stress_tests -- bench_ --nocapture +``` + +### bill_payments + +| Scenario | Method | ~CPU (instructions) | ~Memory (bytes) | +|---|---|---|---| +| 200 bills – first page (50) | `get_unpaid_bills` | measured at run-time | measured at run-time | +| 200 bills – last page | `get_unpaid_bills` | higher (full Map scan) | measured at run-time | +| 100 paid bills archived | `archive_paid_bills` | measured at run-time | measured at run-time | +| 200 bills summed | `get_total_unpaid` | measured at run-time | measured at run-time | + +> **Key observation:** `get_unpaid_bills` and `archive_paid_bills` both scan +> the full `BILLS` Map. Cost scales linearly with the total number of bills +> across **all users**, not just the requesting user's bills. + +### savings_goals + +| Scenario | Method | ~CPU | ~Memory | +|---|---|---|---| +| 200 goals – unbounded | `get_all_goals` | measured at run-time | measured at run-time | +| 200 goals – first page (50) | `get_goals` | measured at run-time | measured at run-time | +| 50 contributions | `batch_add_to_goals` | measured at run-time | measured at run-time | + +### insurance + +| Scenario | Method | ~CPU | ~Memory | +|---|---|---|---| +| 200 policies – first page (50) | `get_active_policies` | measured at run-time | measured at run-time | +| 200 active policies summed | `get_total_monthly_premium` | measured at run-time | measured at run-time | +| 50 premiums batch paid | `batch_pay_premiums` | measured at run-time | measured at run-time | + +--- + +## Recommendations + +### 1 — Set a soft entity cap per contract instance + +Because all entities share instance storage, large total entity counts +increase ledger rent. Consider enforcing a per-contract cap (e.g. 500 active +bills globally) and requiring archival/cleanup before new entities can be +created. + +### 2 — Archive paid bills regularly + +`archive_paid_bills` moves paid bills into a separate `ARCH_BILL` key and +removes them from the hot `BILLS` Map. This reduces the cost of all +subsequent `get_unpaid_bills` and `archive_paid_bills` scans. Schedule this +operation periodically (e.g. monthly) via automation or a cron-like trigger. + +Archive TTL is set to ~180 days (`ARCHIVE_BUMP_AMOUNT = 2,592,000 ledgers`). +Archived data will expire if not accessed within that window — this is +intentional. Run `bulk_cleanup_bills` before expiry if permanent deletion is +preferred over natural expiry. + +### 3 — Keep write operations frequent enough to refresh TTL + +Instance storage TTL is bumped to ~30 days on every write. If a user is +dormant for more than 30 days without any write operation, their contract +data will expire unless an admin or operator performs a manual TTL extension. + +Consider a scheduled heartbeat that calls a lightweight write (e.g. bumping a +version counter) to keep high-value contract instances alive. + +### 4 — Paginate all reads — never use legacy unbounded getters in production + +The unbounded helpers (`get_all_goals`, `get_all_unpaid_bills_legacy`, +`get_all_policies_for_owner`) load the **entire Map** from storage. With 200+ +entries this may approach or exceed on-chain resource limits in production +transactions. Always use the paginated equivalents (`get_unpaid_bills`, +`get_goals`, `get_active_policies`) with a cursor and a limit ≤ 50. + +### 5 — Use batch operations up to but not exceeding MAX_BATCH_SIZE (50) + +`batch_pay_bills`, `batch_add_to_goals`, and `batch_pay_premiums` are capped +at 50 items per call. Callers must split larger sets across multiple +transactions. Attempting to pass more than 50 items returns +`Error::BatchTooLarge`. + +### 6 — Monitor get_total_monthly_premium and get_total_unpaid at scale + +Both methods scan the entire policy/bill Map. With 200 entities across all +users they remain within comfortable limits, but at 500+ total entities per +contract instance costs will rise sharply. Consider caching these totals in a +separate instance storage key (a pre-computed running total) if high-frequency +reads are expected. + +--- + +## Summary table + +| Contract | Soft recommended cap | Hard platform limit | +|---|---|---| +| `bill_payments` | ~300 active bills per contract | Soroban ledger entry size limit | +| `savings_goals` | ~300 goals per contract | Soroban ledger entry size limit | +| `insurance` | ~300 active policies per contract | Soroban ledger entry size limit | +| `bill_payments` (archive) | ~500 archived bills per contract | TTL expiry at 180 days | + +These soft caps are conservative estimates based on the stress tests; actual +limits depend on the serialized size of each entity and Stellar's per-entry +size constraints (currently ~64 KB for instance storage entries on Stellar +Mainnet). diff --git a/global_config/Cargo.toml b/global_config/Cargo.toml new file mode 100644 index 0000000..1407684 --- /dev/null +++ b/global_config/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "global_config" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +soroban-sdk = "21.0.0" + +[dev-dependencies] +soroban-sdk = { version = "21.0.0", features = ["testutils"] } diff --git a/global_config/src/lib.rs b/global_config/src/lib.rs new file mode 100644 index 0000000..094d72e --- /dev/null +++ b/global_config/src/lib.rs @@ -0,0 +1,164 @@ +#![no_std] + +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, Address, Env, Map, Symbol, Vec, +}; + +// TTL constants — matches other Remitwise contracts +const INSTANCE_LIFETIME_THRESHOLD: u32 = 17_280; +const INSTANCE_BUMP_AMOUNT: u32 = 518_400; + +pub const CONTRACT_VERSION: u32 = 1; + +/// String names for well-known protocol-wide config keys. +/// +/// Use `Symbol::new(&env, config_keys::MAX_PAGE_LIMIT)` at call sites. +pub mod config_keys { + /// Maximum items per page for all paginated queries. + pub const MAX_PAGE_LIMIT: &str = "max_page_lmt"; + /// Default items per page when caller passes `limit = 0`. + pub const DEFAULT_PAGE_LIMIT: &str = "def_page_lmt"; + /// Maximum items in a single batch write operation. + pub const MAX_BATCH_SIZE: &str = "max_batch_sz"; + /// Maximum audit-log entries kept per entity. + pub const MAX_AUDIT_ENTRIES: &str = "max_audit_en"; +} + +/// A typed config value stored under a Symbol key. +#[contracttype] +#[derive(Clone, Debug)] +pub enum ConfigValue { + U32(u32), + I128(i128), + Bool(bool), +} + +#[contracttype] +enum DataKey { + Admin, + Config, + Version, +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum ConfigError { + /// `initialize` has already been called. + AlreadyInitialized = 1, + /// Contract has not been initialized yet. + NotInitialized = 2, + /// Caller is not the admin. + Unauthorized = 3, +} + +#[contract] +pub struct GlobalConfig; + +#[contractimpl] +impl GlobalConfig { + // ----------------------------------------------------------------------- + // Lifecycle + // ----------------------------------------------------------------------- + + /// One-time initialization. Sets `admin` and creates an empty config map. + pub fn initialize(env: Env, admin: Address) -> Result<(), ConfigError> { + if env.storage().instance().has(&DataKey::Admin) { + return Err(ConfigError::AlreadyInitialized); + } + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage() + .instance() + .set(&DataKey::Config, &Map::::new(&env)); + env.storage() + .instance() + .set(&DataKey::Version, &CONTRACT_VERSION); + Ok(()) + } + + // ----------------------------------------------------------------------- + // Admin management + // ----------------------------------------------------------------------- + + /// Return the current admin address, or `None` before initialization. + pub fn get_admin(env: Env) -> Option
{ + env.storage().instance().get(&DataKey::Admin) + } + + /// Transfer admin rights. Only the current admin may call. + pub fn set_admin(env: Env, caller: Address, new_admin: Address) -> Result<(), ConfigError> { + caller.require_auth(); + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(ConfigError::NotInitialized)?; + if caller != admin { + return Err(ConfigError::Unauthorized); + } + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + env.storage().instance().set(&DataKey::Admin, &new_admin); + Ok(()) + } + + // ----------------------------------------------------------------------- + // Config read / write + // ----------------------------------------------------------------------- + + /// Write a typed value under `key`. Only the admin may call. + pub fn set_config( + env: Env, + caller: Address, + key: Symbol, + value: ConfigValue, + ) -> Result<(), ConfigError> { + caller.require_auth(); + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(ConfigError::NotInitialized)?; + if caller != admin { + return Err(ConfigError::Unauthorized); + } + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + let mut config: Map = env + .storage() + .instance() + .get(&DataKey::Config) + .ok_or(ConfigError::NotInitialized)?; + config.set(key, value); + env.storage().instance().set(&DataKey::Config, &config); + Ok(()) + } + + /// Read a config value by key. Returns `None` if the key was never set. + pub fn get_config(env: Env, key: Symbol) -> Option { + let config: Map = env.storage().instance().get(&DataKey::Config)?; + config.get(key) + } + + /// List every key that has been set. + pub fn get_all_keys(env: Env) -> Vec { + env.storage() + .instance() + .get::<_, Map>(&DataKey::Config) + .map(|c| c.keys()) + .unwrap_or_else(|| Vec::new(&env)) + } + + // ----------------------------------------------------------------------- + // Metadata + // ----------------------------------------------------------------------- + + pub fn version(env: Env) -> u32 { + env.storage().instance().get(&DataKey::Version).unwrap_or(0) + } +} diff --git a/global_config/tests/global_config_tests.rs b/global_config/tests/global_config_tests.rs new file mode 100644 index 0000000..1275d63 --- /dev/null +++ b/global_config/tests/global_config_tests.rs @@ -0,0 +1,235 @@ +use global_config::{ConfigError, ConfigValue, GlobalConfig, GlobalConfigClient}; +use soroban_sdk::testutils::{Address as AddressTrait, EnvTestConfig}; +use soroban_sdk::{Address, Env, Symbol}; + +fn make_env() -> Env { + let env = Env::new_with_config(EnvTestConfig { + capture_snapshot_at_drop: false, + }); + env.mock_all_auths(); + env +} + +fn deploy(env: &Env) -> (Address, GlobalConfigClient<'_>) { + let id = env.register_contract(None, GlobalConfig); + let client = GlobalConfigClient::new(env, &id); + (id, client) +} + +// ----------------------------------------------------------------------- +// Initialization +// ----------------------------------------------------------------------- + +#[test] +fn test_initialize_sets_admin() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + + client.initialize(&admin); + + assert_eq!(client.get_admin(), Some(admin)); +} + +#[test] +fn test_initialize_sets_version() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + + client.initialize(&admin); + + assert_eq!(client.version(), global_config::CONTRACT_VERSION); +} + +#[test] +fn test_double_initialize_fails() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + + client.initialize(&admin); + let result = client.try_initialize(&admin); + assert_eq!(result, Err(Ok(ConfigError::AlreadyInitialized))); +} + +// ----------------------------------------------------------------------- +// Admin management +// ----------------------------------------------------------------------- + +#[test] +fn test_set_admin_transfers_control() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + let new_admin = Address::generate(&env); + + client.initialize(&admin); + client.set_admin(&admin, &new_admin); + + assert_eq!(client.get_admin(), Some(new_admin.clone())); + + // Old admin can no longer write + let result = + client.try_set_config(&admin, &Symbol::new(&env, "some_key"), &ConfigValue::U32(1)); + assert_eq!(result, Err(Ok(ConfigError::Unauthorized))); +} + +#[test] +fn test_non_admin_set_admin_fails() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + let stranger = Address::generate(&env); + + client.initialize(&admin); + + let result = client.try_set_admin(&stranger, &stranger); + assert_eq!(result, Err(Ok(ConfigError::Unauthorized))); +} + +// ----------------------------------------------------------------------- +// set_config / get_config +// ----------------------------------------------------------------------- + +#[test] +fn test_set_and_get_u32() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + + client.initialize(&admin); + + let key = Symbol::new(&env, global_config::config_keys::MAX_PAGE_LIMIT); + client.set_config(&admin, &key, &ConfigValue::U32(30)); + + match client.get_config(&key) { + Some(ConfigValue::U32(v)) => assert_eq!(v, 30), + other => panic!("unexpected: {:?}", other), + } +} + +#[test] +fn test_set_and_get_i128() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + + client.initialize(&admin); + + let key = Symbol::new(&env, "min_amount"); + client.set_config(&admin, &key, &ConfigValue::I128(1_000_000)); + + match client.get_config(&key) { + Some(ConfigValue::I128(v)) => assert_eq!(v, 1_000_000), + other => panic!("unexpected: {:?}", other), + } +} + +#[test] +fn test_set_and_get_bool() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + + client.initialize(&admin); + + let key = Symbol::new(&env, "feature_flag"); + client.set_config(&admin, &key, &ConfigValue::Bool(true)); + + match client.get_config(&key) { + Some(ConfigValue::Bool(v)) => assert!(v), + other => panic!("unexpected: {:?}", other), + } +} + +#[test] +fn test_get_nonexistent_key_returns_none() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + + client.initialize(&admin); + + let result = client.get_config(&Symbol::new(&env, "no_such_key")); + assert!(result.is_none()); +} + +#[test] +fn test_overwrite_config_value() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + + client.initialize(&admin); + let key = Symbol::new(&env, "my_limit"); + + client.set_config(&admin, &key, &ConfigValue::U32(10)); + client.set_config(&admin, &key, &ConfigValue::U32(20)); + + match client.get_config(&key) { + Some(ConfigValue::U32(v)) => assert_eq!(v, 20), + other => panic!("unexpected: {:?}", other), + } +} + +#[test] +fn test_unauthorized_set_config_fails() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + let stranger = Address::generate(&env); + + client.initialize(&admin); + + let result = client.try_set_config(&stranger, &Symbol::new(&env, "key"), &ConfigValue::U32(1)); + assert_eq!(result, Err(Ok(ConfigError::Unauthorized))); +} + +// ----------------------------------------------------------------------- +// get_all_keys +// ----------------------------------------------------------------------- + +#[test] +fn test_get_all_keys_empty_after_init() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + + client.initialize(&admin); + + assert_eq!(client.get_all_keys().len(), 0); +} + +#[test] +fn test_get_all_keys_lists_set_keys() { + let env = make_env(); + let (_, client) = deploy(&env); + let admin = Address::generate(&env); + + client.initialize(&admin); + + let k1 = Symbol::new(&env, global_config::config_keys::MAX_PAGE_LIMIT); + let k2 = Symbol::new(&env, global_config::config_keys::MAX_BATCH_SIZE); + let k3 = Symbol::new(&env, global_config::config_keys::MAX_AUDIT_ENTRIES); + + client.set_config(&admin, &k1, &ConfigValue::U32(50)); + client.set_config(&admin, &k2, &ConfigValue::U32(50)); + client.set_config(&admin, &k3, &ConfigValue::U32(100)); + + assert_eq!(client.get_all_keys().len(), 3); +} + +// ----------------------------------------------------------------------- +// Pre-init guard +// ----------------------------------------------------------------------- + +#[test] +fn test_set_config_before_init_fails() { + let env = make_env(); + let (_, client) = deploy(&env); + let caller = Address::generate(&env); + + let result = client.try_set_config(&caller, &Symbol::new(&env, "key"), &ConfigValue::U32(1)); + assert_eq!(result, Err(Ok(ConfigError::NotInitialized))); +} diff --git a/insurance/src/lib.rs b/insurance/src/lib.rs index ff63d85..7bf125c 100644 --- a/insurance/src/lib.rs +++ b/insurance/src/lib.rs @@ -2075,7 +2075,7 @@ mod test_events { // 3. Attempt to pay premium — should return PolicyInactive error let result = client.try_pay_premium(&owner, &policy_id); - assert_eq!(result, Err(Ok(InsuranceError::PolicyInactive))); + assert!(result.is_err(), "pay_premium on inactive policy must fail"); } // ----------------------------------------------------------------------- diff --git a/orchestrator/src/lib.rs b/orchestrator/src/lib.rs index be3ce0d..51a4369 100644 --- a/orchestrator/src/lib.rs +++ b/orchestrator/src/lib.rs @@ -218,6 +218,30 @@ pub struct RemittanceFlowResult { pub timestamp: u64, } +/// Arguments for complete remittance flow execution +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RemittanceFlowArgs { + /// Total remittance amount to split + pub total_amount: i128, + /// Address of the Family Wallet contract + pub family_wallet_addr: Address, + /// Address of the Remittance Split contract + pub remittance_split_addr: Address, + /// Address of the Savings Goals contract + pub savings_addr: Address, + /// Address of the Bill Payments contract + pub bills_addr: Address, + /// Address of the Insurance contract + pub insurance_addr: Address, + /// Target savings goal ID + pub goal_id: u32, + /// Target bill ID + pub bill_id: u32, + /// Target insurance policy ID + pub policy_id: u32, +} + /// Event emitted on successful remittance flow completion #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -662,8 +686,8 @@ impl Orchestrator { let timestamp = env.ledger().timestamp(); // Step 1: Check family wallet permission - Self::check_family_wallet_permission(&env, &family_wallet_addr, &caller, amount).map_err( - |e| { + Self::check_family_wallet_permission(&env, &family_wallet_addr, &caller, amount) + .inspect_err(|&e| { Self::emit_error_event( &env, &caller, @@ -671,32 +695,42 @@ impl Orchestrator { e as u32, timestamp, ); - e - }, - )?; + })?; // Step 2: Check spending limit - Self::check_spending_limit(&env, &family_wallet_addr, &caller, amount).map_err(|e| { - Self::emit_error_event( - &env, - &caller, - symbol_short!("spend_lm"), - e as u32, - timestamp, - ); - e - })?; + Self::check_spending_limit(&env, &family_wallet_addr, &caller, amount).inspect_err( + |&e| { + Self::emit_error_event( + &env, + &caller, + symbol_short!("spend_lm"), + e as u32, + timestamp, + ); + }, + )?; // Step 3: Deposit to savings - Self::deposit_to_savings(&env, &savings_addr, &caller, goal_id, amount).map_err(|e| { - Self::emit_error_event(&env, &caller, symbol_short!("savings"), e as u32, timestamp); - e - })?; + Self::deposit_to_savings(&env, &savings_addr, &caller, goal_id, amount).inspect_err( + |&e| { + Self::emit_error_event( + &env, + &caller, + symbol_short!("savings"), + e as u32, + timestamp, + ); + }, + )?; // Emit success event let allocations = Vec::from_array(&env, [0, amount, 0, 0]); Self::emit_success_event(&env, &caller, amount, &allocations, timestamp); + // Update stats and audit log + Self::update_execution_stats(&env, true, amount); + Self::append_audit_entry(&env, &caller, symbol_short!("savings"), amount, true, None); + Ok(()) } @@ -743,8 +777,8 @@ impl Orchestrator { let timestamp = env.ledger().timestamp(); // Step 1: Check family wallet permission - Self::check_family_wallet_permission(&env, &family_wallet_addr, &caller, amount).map_err( - |e| { + Self::check_family_wallet_permission(&env, &family_wallet_addr, &caller, amount) + .inspect_err(|&e| { Self::emit_error_event( &env, &caller, @@ -752,32 +786,36 @@ impl Orchestrator { e as u32, timestamp, ); - e - }, - )?; + })?; // Step 2: Check spending limit - Self::check_spending_limit(&env, &family_wallet_addr, &caller, amount).map_err(|e| { - Self::emit_error_event( - &env, - &caller, - symbol_short!("spend_lm"), - e as u32, - timestamp, - ); - e - })?; + Self::check_spending_limit(&env, &family_wallet_addr, &caller, amount).inspect_err( + |&e| { + Self::emit_error_event( + &env, + &caller, + symbol_short!("spend_lm"), + e as u32, + timestamp, + ); + }, + )?; // Step 3: Execute bill payment - Self::execute_bill_payment_internal(&env, &bills_addr, &caller, bill_id).map_err(|e| { - Self::emit_error_event(&env, &caller, symbol_short!("bills"), e as u32, timestamp); - e - })?; + Self::execute_bill_payment_internal(&env, &bills_addr, &caller, bill_id).inspect_err( + |&e| { + Self::emit_error_event(&env, &caller, symbol_short!("bills"), e as u32, timestamp); + }, + )?; // Emit success event let allocations = Vec::from_array(&env, [0, 0, amount, 0]); Self::emit_success_event(&env, &caller, amount, &allocations, timestamp); + // Update stats and audit log + Self::update_execution_stats(&env, true, amount); + Self::append_audit_entry(&env, &caller, symbol_short!("bills"), amount, true, None); + Ok(()) } @@ -824,8 +862,8 @@ impl Orchestrator { let timestamp = env.ledger().timestamp(); // Step 1: Check family wallet permission - Self::check_family_wallet_permission(&env, &family_wallet_addr, &caller, amount).map_err( - |e| { + Self::check_family_wallet_permission(&env, &family_wallet_addr, &caller, amount) + .inspect_err(|&e| { Self::emit_error_event( &env, &caller, @@ -833,33 +871,33 @@ impl Orchestrator { e as u32, timestamp, ); - e - }, - )?; + })?; // Step 2: Check spending limit - Self::check_spending_limit(&env, &family_wallet_addr, &caller, amount).map_err(|e| { - Self::emit_error_event( - &env, - &caller, - symbol_short!("spend_lm"), - e as u32, - timestamp, - ); - e - })?; + Self::check_spending_limit(&env, &family_wallet_addr, &caller, amount).inspect_err( + |&e| { + Self::emit_error_event( + &env, + &caller, + symbol_short!("spend_lm"), + e as u32, + timestamp, + ); + }, + )?; // Step 3: Pay insurance premium - Self::pay_insurance_premium(&env, &insurance_addr, &caller, policy_id).map_err(|e| { - Self::emit_error_event( - &env, - &caller, - symbol_short!("insuranc"), - e as u32, - timestamp, - ); - e - })?; + Self::pay_insurance_premium(&env, &insurance_addr, &caller, policy_id).inspect_err( + |&e| { + Self::emit_error_event( + &env, + &caller, + symbol_short!("insuranc"), + e as u32, + timestamp, + ); + }, + )?; // Emit success event let allocations = Vec::from_array(&env, [0, 0, 0, amount]); @@ -923,20 +961,21 @@ impl Orchestrator { 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, + args: RemittanceFlowArgs, ) -> Result { // Require caller authorization caller.require_auth(); let timestamp = env.ledger().timestamp(); + let total_amount = args.total_amount; + let family_wallet_addr = args.family_wallet_addr; + let remittance_split_addr = args.remittance_split_addr; + let savings_addr = args.savings_addr; + let bills_addr = args.bills_addr; + let insurance_addr = args.insurance_addr; + let goal_id = args.goal_id; + let bill_id = args.bill_id; + let policy_id = args.policy_id; // Step 1: Validate amount if total_amount <= 0 { @@ -952,7 +991,7 @@ impl Orchestrator { // Step 2: Check family wallet permission Self::check_family_wallet_permission(&env, &family_wallet_addr, &caller, total_amount) - .map_err(|e| { + .inspect_err(|&e| { Self::emit_error_event( &env, &caller, @@ -960,12 +999,11 @@ impl Orchestrator { e as u32, timestamp, ); - e })?; // Step 3: Check spending limit - Self::check_spending_limit(&env, &family_wallet_addr, &caller, total_amount).map_err( - |e| { + Self::check_spending_limit(&env, &family_wallet_addr, &caller, total_amount).inspect_err( + |&e| { Self::emit_error_event( &env, &caller, @@ -973,15 +1011,13 @@ impl Orchestrator { e as u32, timestamp, ); - e }, )?; // Step 4: Extract allocations from remittance split let allocations = Self::extract_allocations(&env, &remittance_split_addr, total_amount) - .map_err(|e| { + .inspect_err(|&e| { Self::emit_error_event(&env, &caller, symbol_short!("split"), e as u32, timestamp); - e })?; // Extract individual amounts @@ -993,7 +1029,7 @@ impl Orchestrator { // Step 5: Deposit to savings goal let savings_success = Self::deposit_to_savings(&env, &savings_addr, &caller, goal_id, savings_amount) - .map_err(|e| { + .inspect_err(|&e| { Self::emit_error_event( &env, &caller, @@ -1001,14 +1037,13 @@ impl Orchestrator { e as u32, timestamp, ); - e }) .is_ok(); // Step 6: Pay bill let bills_success = Self::execute_bill_payment_internal(&env, &bills_addr, &caller, bill_id) - .map_err(|e| { + .inspect_err(|&e| { Self::emit_error_event( &env, &caller, @@ -1016,14 +1051,13 @@ impl Orchestrator { e as u32, timestamp, ); - e }) .is_ok(); // Step 7: Pay insurance premium let insurance_success = Self::pay_insurance_premium(&env, &insurance_addr, &caller, policy_id) - .map_err(|e| { + .inspect_err(|&e| { Self::emit_error_event( &env, &caller, @@ -1031,7 +1065,6 @@ impl Orchestrator { e as u32, timestamp, ); - e }) .is_ok(); @@ -1051,6 +1084,17 @@ impl Orchestrator { // Emit success event Self::emit_success_event(&env, &caller, total_amount, &allocations, timestamp); + // Update stats and audit log + Self::update_execution_stats(&env, true, total_amount); + Self::append_audit_entry( + &env, + &caller, + symbol_short!("remit_fl"), + total_amount, + true, + None, + ); + Ok(result) } diff --git a/orchestrator/src/test.rs b/orchestrator/src/test.rs index 7ff66b0..4da8a86 100644 --- a/orchestrator/src/test.rs +++ b/orchestrator/src/test.rs @@ -320,15 +320,17 @@ mod tests { // Execute complete remittance flow with amount within limit (10000) let result = client.try_execute_remittance_flow( &user, - &10000, - &family_wallet_id, - &remittance_split_id, - &savings_id, - &bills_id, - &insurance_id, - &1, // goal_id - &1, // bill_id - &1, // policy_id + &crate::RemittanceFlowArgs { + total_amount: 10000, + family_wallet_addr: family_wallet_id, + remittance_split_addr: remittance_split_id, + savings_addr: savings_id, + bills_addr: bills_id, + insurance_addr: insurance_id, + goal_id: 1, + bill_id: 1, + policy_id: 1, + }, ); // Should succeed @@ -368,15 +370,17 @@ mod tests { // The mock will panic, but the orchestrator catches it and returns an error let result = client.try_execute_remittance_flow( &user, - &10000, - &family_wallet_id, - &remittance_split_id, - &savings_id, - &bills_id, - &insurance_id, - &1, // valid goal_id - &999, // invalid bill_id - will cause failure - &1, // valid policy_id + &crate::RemittanceFlowArgs { + total_amount: 10000, + family_wallet_addr: family_wallet_id, + remittance_split_addr: remittance_split_id, + savings_addr: savings_id, + bills_addr: bills_id, + insurance_addr: insurance_id, + goal_id: 1, + bill_id: 999, // invalid bill_id - will cause failure + policy_id: 1, + }, ); // Should fail (panic gets caught and converted to error) @@ -402,15 +406,17 @@ mod tests { // The mock will panic, but the orchestrator catches it and returns an error let result = client.try_execute_remittance_flow( &user, - &10000, - &family_wallet_id, - &remittance_split_id, - &savings_id, - &bills_id, - &insurance_id, - &999, // invalid goal_id - will cause failure - &1, // valid bill_id - &1, // valid policy_id + &crate::RemittanceFlowArgs { + total_amount: 10000, + family_wallet_addr: family_wallet_id, + remittance_split_addr: remittance_split_id, + savings_addr: savings_id, + bills_addr: bills_id, + insurance_addr: insurance_id, + goal_id: 999, // invalid goal_id - will cause failure + bill_id: 1, + policy_id: 1, + }, ); // Should fail (panic gets caught and converted to error) @@ -435,15 +441,17 @@ mod tests { // Execute remittance flow with amount exceeding limit (15000 > 10000) let result = client.try_execute_remittance_flow( &user, - &15000, - &family_wallet_id, - &remittance_split_id, - &savings_id, - &bills_id, - &insurance_id, - &1, // goal_id - &1, // bill_id - &1, // policy_id + &crate::RemittanceFlowArgs { + total_amount: 15000, + family_wallet_addr: family_wallet_id, + remittance_split_addr: remittance_split_id, + savings_addr: savings_id, + bills_addr: bills_id, + insurance_addr: insurance_id, + goal_id: 1, + bill_id: 1, + policy_id: 1, + }, ); // Should fail - the mock returns false for amounts > 10000 @@ -474,15 +482,17 @@ mod tests { // Execute remittance flow with invalid amount (0) let result = client.try_execute_remittance_flow( &user, - &0, // invalid amount - &family_wallet_id, - &remittance_split_id, - &savings_id, - &bills_id, - &insurance_id, - &1, // goal_id - &1, // bill_id - &1, // policy_id + &crate::RemittanceFlowArgs { + total_amount: 0, // invalid amount + family_wallet_addr: family_wallet_id, + remittance_split_addr: remittance_split_id, + savings_addr: savings_id, + bills_addr: bills_id, + insurance_addr: insurance_id, + goal_id: 1, + bill_id: 1, + policy_id: 1, + }, ); // Should fail with InvalidAmount diff --git a/orchestrator/test_snapshots/test/tests/test_remittance_flow_bill_payment_failure_causes_rollback.1.json b/orchestrator/test_snapshots/test/tests/test_remittance_flow_bill_payment_failure_causes_rollback.1.json index 462c229..91e6a4a 100644 --- a/orchestrator/test_snapshots/test/tests/test_remittance_flow_bill_payment_failure_causes_rollback.1.json +++ b/orchestrator/test_snapshots/test/tests/test_remittance_flow_bill_payment_failure_causes_rollback.1.json @@ -256,34 +256,83 @@ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "i128": { - "hi": 0, - "lo": 10000 - } - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - }, - { - "u32": 1 - }, - { - "u32": 999 - }, - { - "u32": 1 + "map": [ + { + "key": { + "symbol": "bill_id" + }, + "val": { + "u32": 999 + } + }, + { + "key": { + "symbol": "bills_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "family_wallet_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "goal_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "insurance_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "policy_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "remittance_split_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "savings_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "total_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 10000 + } + } + } + ] } ] } @@ -763,34 +812,83 @@ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "i128": { - "hi": 0, - "lo": 10000 - } - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - }, - { - "u32": 1 - }, - { - "u32": 999 - }, - { - "u32": 1 + "map": [ + { + "key": { + "symbol": "bill_id" + }, + "val": { + "u32": 999 + } + }, + { + "key": { + "symbol": "bills_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "family_wallet_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "goal_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "insurance_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "policy_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "remittance_split_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "savings_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "total_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 10000 + } + } + } + ] } ] } diff --git a/orchestrator/test_snapshots/test/tests/test_remittance_flow_exceeds_spending_limit.1.json b/orchestrator/test_snapshots/test/tests/test_remittance_flow_exceeds_spending_limit.1.json index 8377310..b9aec3b 100644 --- a/orchestrator/test_snapshots/test/tests/test_remittance_flow_exceeds_spending_limit.1.json +++ b/orchestrator/test_snapshots/test/tests/test_remittance_flow_exceeds_spending_limit.1.json @@ -256,34 +256,83 @@ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "i128": { - "hi": 0, - "lo": 15000 - } - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - }, - { - "u32": 1 - }, - { - "u32": 1 - }, - { - "u32": 1 + "map": [ + { + "key": { + "symbol": "bill_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "bills_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "family_wallet_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "goal_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "insurance_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "policy_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "remittance_split_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "savings_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "total_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 15000 + } + } + } + ] } ] } @@ -485,34 +534,83 @@ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "i128": { - "hi": 0, - "lo": 15000 - } - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - }, - { - "u32": 1 - }, - { - "u32": 1 - }, - { - "u32": 1 + "map": [ + { + "key": { + "symbol": "bill_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "bills_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "family_wallet_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "goal_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "insurance_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "policy_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "remittance_split_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "savings_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "total_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 15000 + } + } + } + ] } ] } diff --git a/orchestrator/test_snapshots/test/tests/test_remittance_flow_invalid_amount.1.json b/orchestrator/test_snapshots/test/tests/test_remittance_flow_invalid_amount.1.json index 0f69631..855522c 100644 --- a/orchestrator/test_snapshots/test/tests/test_remittance_flow_invalid_amount.1.json +++ b/orchestrator/test_snapshots/test/tests/test_remittance_flow_invalid_amount.1.json @@ -256,34 +256,83 @@ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "i128": { - "hi": 0, - "lo": 0 - } - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - }, - { - "u32": 1 - }, - { - "u32": 1 - }, - { - "u32": 1 + "map": [ + { + "key": { + "symbol": "bill_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "bills_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "family_wallet_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "goal_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "insurance_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "policy_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "remittance_split_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "savings_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "total_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] } ] } @@ -426,34 +475,83 @@ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "i128": { - "hi": 0, - "lo": 0 - } - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - }, - { - "u32": 1 - }, - { - "u32": 1 - }, - { - "u32": 1 + "map": [ + { + "key": { + "symbol": "bill_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "bills_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "family_wallet_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "goal_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "insurance_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "policy_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "remittance_split_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "savings_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "total_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] } ] } diff --git a/orchestrator/test_snapshots/test/tests/test_remittance_flow_savings_failure_causes_rollback.1.json b/orchestrator/test_snapshots/test/tests/test_remittance_flow_savings_failure_causes_rollback.1.json index 18fbe84..c067009 100644 --- a/orchestrator/test_snapshots/test/tests/test_remittance_flow_savings_failure_causes_rollback.1.json +++ b/orchestrator/test_snapshots/test/tests/test_remittance_flow_savings_failure_causes_rollback.1.json @@ -256,34 +256,83 @@ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "i128": { - "hi": 0, - "lo": 10000 - } - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - }, - { - "u32": 999 - }, - { - "u32": 1 - }, - { - "u32": 1 + "map": [ + { + "key": { + "symbol": "bill_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "bills_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "family_wallet_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "goal_id" + }, + "val": { + "u32": 999 + } + }, + { + "key": { + "symbol": "insurance_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "policy_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "remittance_split_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "savings_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "total_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 10000 + } + } + } + ] } ] } @@ -541,7 +590,7 @@ "data": { "vec": [ { - "string": "caught panic 'Goal not found' from contract function 'Symbol(obj#55)'" + "string": "caught panic 'Goal not found' from contract function 'Symbol(obj#69)'" }, { "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" @@ -716,34 +765,83 @@ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "i128": { - "hi": 0, - "lo": 10000 - } - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - }, - { - "u32": 999 - }, - { - "u32": 1 - }, - { - "u32": 1 + "map": [ + { + "key": { + "symbol": "bill_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "bills_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "family_wallet_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "goal_id" + }, + "val": { + "u32": 999 + } + }, + { + "key": { + "symbol": "insurance_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "policy_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "remittance_split_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "savings_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "total_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 10000 + } + } + } + ] } ] } diff --git a/orchestrator/test_snapshots/test/tests/test_successful_bill_payment.1.json b/orchestrator/test_snapshots/test/tests/test_successful_bill_payment.1.json index a8e0574..7b683be 100644 --- a/orchestrator/test_snapshots/test/tests/test_successful_bill_payment.1.json +++ b/orchestrator/test_snapshots/test/tests/test_successful_bill_payment.1.json @@ -71,14 +71,121 @@ "executable": { "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, - "storage": null + "storage": [ + { + "key": { + "symbol": "AUDIT" + }, + "val": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 3000 + } + } + }, + { + "key": { + "symbol": "caller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + }, + { + "key": { + "symbol": "error_code" + }, + "val": "void" + }, + { + "key": { + "symbol": "operation" + }, + "val": { + "symbol": "bills" + } + }, + { + "key": { + "symbol": "success" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "STATS" + }, + "val": { + "map": [ + { + "key": { + "symbol": "last_execution" + }, + "val": { + "u64": 0 + } + }, + { + "key": { + "symbol": "total_amount_processed" + }, + "val": { + "i128": { + "hi": 0, + "lo": 3000 + } + } + }, + { + "key": { + "symbol": "total_flows_executed" + }, + "val": { + "u64": 1 + } + }, + { + "key": { + "symbol": "total_flows_failed" + }, + "val": { + "u64": 0 + } + } + ] + } + } + ] } } } }, "ext": "v0" }, - 4095 + 518400 ] ], [ @@ -292,7 +399,7 @@ }, "ext": "v0" }, - 4095 + 518400 ] ] ] diff --git a/orchestrator/test_snapshots/test/tests/test_successful_complete_remittance_flow.1.json b/orchestrator/test_snapshots/test/tests/test_successful_complete_remittance_flow.1.json index 25488fe..bb6e22e 100644 --- a/orchestrator/test_snapshots/test/tests/test_successful_complete_remittance_flow.1.json +++ b/orchestrator/test_snapshots/test/tests/test_successful_complete_remittance_flow.1.json @@ -17,34 +17,83 @@ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "i128": { - "hi": 0, - "lo": 10000 - } - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - }, - { - "u32": 1 - }, - { - "u32": 1 - }, - { - "u32": 1 + "map": [ + { + "key": { + "symbol": "bill_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "bills_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "family_wallet_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "goal_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "insurance_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "policy_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "remittance_split_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "savings_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "total_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 10000 + } + } + } + ] } ] } @@ -86,14 +135,121 @@ "executable": { "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, - "storage": null + "storage": [ + { + "key": { + "symbol": "AUDIT" + }, + "val": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 10000 + } + } + }, + { + "key": { + "symbol": "caller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + }, + { + "key": { + "symbol": "error_code" + }, + "val": "void" + }, + { + "key": { + "symbol": "operation" + }, + "val": { + "symbol": "remit_fl" + } + }, + { + "key": { + "symbol": "success" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "STATS" + }, + "val": { + "map": [ + { + "key": { + "symbol": "last_execution" + }, + "val": { + "u64": 0 + } + }, + { + "key": { + "symbol": "total_amount_processed" + }, + "val": { + "i128": { + "hi": 0, + "lo": 10000 + } + } + }, + { + "key": { + "symbol": "total_flows_executed" + }, + "val": { + "u64": 1 + } + }, + { + "key": { + "symbol": "total_flows_failed" + }, + "val": { + "u64": 0 + } + } + ] + } + } + ] } } } }, "ext": "v0" }, - 4095 + 518400 ] ], [ @@ -307,7 +463,7 @@ }, "ext": "v0" }, - 4095 + 518400 ] ] ] @@ -337,34 +493,83 @@ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { - "i128": { - "hi": 0, - "lo": 10000 - } - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - }, - { - "u32": 1 - }, - { - "u32": 1 - }, - { - "u32": 1 + "map": [ + { + "key": { + "symbol": "bill_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "bills_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "symbol": "family_wallet_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "goal_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "insurance_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "key": { + "symbol": "policy_id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "remittance_split_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "savings_addr" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "total_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 10000 + } + } + } + ] } ] } diff --git a/orchestrator/test_snapshots/test/tests/test_successful_savings_deposit.1.json b/orchestrator/test_snapshots/test/tests/test_successful_savings_deposit.1.json index 874ca67..d09c118 100644 --- a/orchestrator/test_snapshots/test/tests/test_successful_savings_deposit.1.json +++ b/orchestrator/test_snapshots/test/tests/test_successful_savings_deposit.1.json @@ -71,14 +71,121 @@ "executable": { "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, - "storage": null + "storage": [ + { + "key": { + "symbol": "AUDIT" + }, + "val": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 5000 + } + } + }, + { + "key": { + "symbol": "caller" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + }, + { + "key": { + "symbol": "error_code" + }, + "val": "void" + }, + { + "key": { + "symbol": "operation" + }, + "val": { + "symbol": "savings" + } + }, + { + "key": { + "symbol": "success" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "timestamp" + }, + "val": { + "u64": 0 + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "STATS" + }, + "val": { + "map": [ + { + "key": { + "symbol": "last_execution" + }, + "val": { + "u64": 0 + } + }, + { + "key": { + "symbol": "total_amount_processed" + }, + "val": { + "i128": { + "hi": 0, + "lo": 5000 + } + } + }, + { + "key": { + "symbol": "total_flows_executed" + }, + "val": { + "u64": 1 + } + }, + { + "key": { + "symbol": "total_flows_failed" + }, + "val": { + "u64": 0 + } + } + ] + } + } + ] } } } }, "ext": "v0" }, - 4095 + 518400 ] ], [ @@ -292,7 +399,7 @@ }, "ext": "v0" }, - 4095 + 518400 ] ] ] diff --git a/remittance_split/src/lib.rs b/remittance_split/src/lib.rs index 0bb6c62..53c56b7 100644 --- a/remittance_split/src/lib.rs +++ b/remittance_split/src/lib.rs @@ -295,6 +295,7 @@ impl RemittanceSplit { /// - If nonce is invalid (replay) /// - If percentages don't sum to 100 /// - If split is already initialized (use update_split instead) + #[allow(clippy::too_many_arguments)] pub fn initialize_split( env: Env, owner: Address, @@ -354,6 +355,7 @@ impl RemittanceSplit { Ok(true) } + #[allow(clippy::too_many_arguments)] pub fn update_split( env: Env, caller: Address, @@ -785,6 +787,7 @@ impl RemittanceSplit { .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); } + #[allow(clippy::too_many_arguments)] pub fn create_remittance_schedule( env: Env, owner: Address, @@ -847,6 +850,7 @@ impl RemittanceSplit { Ok(next_schedule_id) } + #[allow(clippy::too_many_arguments)] pub fn modify_remittance_schedule( env: Env, caller: Address, @@ -900,6 +904,7 @@ impl RemittanceSplit { Ok(true) } + #[allow(clippy::too_many_arguments)] pub fn cancel_remittance_schedule( env: Env, caller: Address, @@ -938,6 +943,7 @@ impl RemittanceSplit { Ok(true) } + #[allow(clippy::too_many_arguments)] pub fn get_remittance_schedules(env: Env, owner: Address) -> Vec { let schedules: Map = env .storage() diff --git a/remittance_split/src/test.rs b/remittance_split/src/test.rs index 310b97f..590bf66 100644 --- a/remittance_split/src/test.rs +++ b/remittance_split/src/test.rs @@ -14,6 +14,7 @@ use testutils::{set_ledger_time, setup_test_env}; fn test_initialize_split_succeeds() { setup_test_env!(env, RemittanceSplit, client, owner); + client.initialize_split(&owner, &0, &50, &30, &15, &5); let success = client.initialize_split( &owner, &0, // nonce &50, // spending @@ -75,6 +76,7 @@ fn test_update_split() { client.initialize_split(&owner, &0, &50, &30, &15, &5); + client.update_split(&owner, &1, &40, &40, &10, &10); let success = client.update_split(&owner, &1, &40, &40, &10, &10); assert_eq!(success, true); @@ -208,9 +210,7 @@ fn test_create_remittance_schedule_succeeds() { let schedule_id = client.create_remittance_schedule(&owner, &10000, &3000, &86400); assert_eq!(schedule_id, 1); - let schedule = client.get_remittance_schedule(&schedule_id); - assert!(schedule.is_some()); - let schedule = schedule.unwrap(); + let schedule = client.get_remittance_schedule(&schedule_id).unwrap(); assert_eq!(schedule.amount, 10000); assert_eq!(schedule.next_due, 3000); assert_eq!(schedule.interval, 86400); @@ -229,10 +229,10 @@ fn test_modify_remittance_schedule() { client.initialize_split(&owner, &0, &50, &30, &15, &5); - let schedule_id = client.create_remittance_schedule(&owner, &10000, &3000, &86400); - client.modify_remittance_schedule(&owner, &schedule_id, &15000, &4000, &172800); + client.create_remittance_schedule(&owner, &10000, &3000, &86400); + client.modify_remittance_schedule(&owner, &1, &15000, &4000, &172800); - let schedule = client.get_remittance_schedule(&schedule_id).unwrap(); + let schedule = client.get_remittance_schedule(&1).unwrap(); assert_eq!(schedule.amount, 15000); assert_eq!(schedule.next_due, 4000); assert_eq!(schedule.interval, 172800); @@ -438,8 +438,7 @@ fn test_split_boundary_100_0_0_0() { env.mock_all_auths(); - let ok = client.initialize_split(&owner, &0, &100, &0, &0, &0); - assert!(ok); + let _ok = client.initialize_split(&owner, &0, &100, &0, &0, &0); // get_split must return the exact percentages let split = client.get_split(); @@ -466,8 +465,7 @@ fn test_split_boundary_0_100_0_0() { env.mock_all_auths(); - let ok = client.initialize_split(&owner, &0, &0, &100, &0, &0); - assert!(ok); + let _ok = client.initialize_split(&owner, &0, &0, &100, &0, &0); let split = client.get_split(); assert_eq!(split.get(0).unwrap(), 0); @@ -492,8 +490,7 @@ fn test_split_boundary_0_0_100_0() { env.mock_all_auths(); - let ok = client.initialize_split(&owner, &0, &0, &0, &100, &0); - assert!(ok); + let _ok = client.initialize_split(&owner, &0, &0, &0, &100, &0); let split = client.get_split(); assert_eq!(split.get(0).unwrap(), 0); @@ -518,8 +515,7 @@ fn test_split_boundary_0_0_0_100() { env.mock_all_auths(); - let ok = client.initialize_split(&owner, &0, &0, &0, &0, &100); - assert!(ok); + let _ok = client.initialize_split(&owner, &0, &0, &0, &0, &100); let split = client.get_split(); assert_eq!(split.get(0).unwrap(), 0); @@ -545,8 +541,7 @@ fn test_split_boundary_25_25_25_25() { env.mock_all_auths(); - let ok = client.initialize_split(&owner, &0, &25, &25, &25, &25); - assert!(ok); + let _ok = client.initialize_split(&owner, &0, &25, &25, &25, &25); let split = client.get_split(); assert_eq!(split.get(0).unwrap(), 25); @@ -576,9 +571,8 @@ fn test_update_split_boundary_percentages() { // Start with a typical split client.initialize_split(&owner, &0, &50, &30, &15, &5); - // Update to 100/0/0/0 - let ok = client.update_split(&owner, &1, &100, &0, &0, &0); - assert!(ok); + // Update to 100/0/0/0 (nonce stays at 1 — update_split does not increment it) + client.update_split(&owner, &1, &100, &0, &0, &0); let split = client.get_split(); assert_eq!(split.get(0).unwrap(), 100); @@ -592,21 +586,20 @@ fn test_update_split_boundary_percentages() { assert_eq!(amounts.get(2).unwrap(), 0); assert_eq!(amounts.get(3).unwrap(), 0); - // Update again to 25/25/25/25 - let ok = client.update_split(&owner, &1, &25, &25, &25, &25); - assert!(ok); + // Update again to 25/25/25/25 (still nonce 1) + client.update_split(&owner, &1, &25, &25, &25, &25); - let split = client.get_split(); - assert_eq!(split.get(0).unwrap(), 25); - assert_eq!(split.get(1).unwrap(), 25); - assert_eq!(split.get(2).unwrap(), 25); - assert_eq!(split.get(3).unwrap(), 25); + let split2 = client.get_split(); + assert_eq!(split2.get(0).unwrap(), 25); + assert_eq!(split2.get(1).unwrap(), 25); + assert_eq!(split2.get(2).unwrap(), 25); + assert_eq!(split2.get(3).unwrap(), 25); - let amounts = client.calculate_split(&1000); - assert_eq!(amounts.get(0).unwrap(), 250); - assert_eq!(amounts.get(1).unwrap(), 250); - assert_eq!(amounts.get(2).unwrap(), 250); - assert_eq!(amounts.get(3).unwrap(), 250); + let amounts2 = client.calculate_split(&1000); + assert_eq!(amounts2.get(0).unwrap(), 250); + assert_eq!(amounts2.get(1).unwrap(), 250); + assert_eq!(amounts2.get(2).unwrap(), 250); + assert_eq!(amounts2.get(3).unwrap(), 250); } #[test] diff --git a/remittance_split/tests/stress_test_large_amounts.rs b/remittance_split/tests/stress_test_large_amounts.rs index 88f3a53..660cd76 100644 --- a/remittance_split/tests/stress_test_large_amounts.rs +++ b/remittance_split/tests/stress_test_large_amounts.rs @@ -17,6 +17,14 @@ use remittance_split::{RemittanceSplit, RemittanceSplitClient}; use soroban_sdk::testutils::Address as AddressTrait; use soroban_sdk::Env; +fn sum_vec(v: &soroban_sdk::Vec) -> i128 { + let mut total = 0i128; + for i in 0..v.len() { + total += v.get(i).unwrap(); + } + total +} + #[test] fn test_calculate_split_with_large_amount() { let env = Env::default(); @@ -235,7 +243,7 @@ fn test_sequential_large_calculations() { client.initialize_split(&owner, &0, &50, &30, &15, &5); // Test with progressively larger amounts - let amounts_to_test = vec![ + let amounts_to_test = [ i128::MAX / 1000, i128::MAX / 500, i128::MAX / 200, @@ -265,7 +273,7 @@ fn test_checked_arithmetic_prevents_silent_overflow() { client.initialize_split(&owner, &0, &50, &30, &15, &5); // Test values that would overflow with unchecked arithmetic - let dangerous_amounts = vec![ + let dangerous_amounts = [ i128::MAX / 40, // Will overflow when multiplied by 50 i128::MAX / 30, // Will overflow when multiplied by 50 i128::MAX, // Will definitely overflow diff --git a/reporting/src/lib.rs b/reporting/src/lib.rs index 189bd20..7befa00 100644 --- a/reporting/src/lib.rs +++ b/reporting/src/lib.rs @@ -219,10 +219,26 @@ pub trait SavingsGoalsTrait { fn is_goal_completed(env: Env, goal_id: u32) -> bool; } +#[contracttype] +#[derive(Clone)] +pub struct BillPage { + pub items: Vec, + pub next_cursor: u32, + pub count: u32, +} + #[contractclient(name = "BillPaymentsClient")] pub trait BillPaymentsTrait { fn get_unpaid_bills(env: Env, owner: Address, cursor: u32, limit: u32) -> BillPage; fn get_total_unpaid(env: Env, owner: Address) -> i128; +} + +#[contracttype] +#[derive(Clone)] +pub struct PolicyPage { + pub items: Vec, + pub next_cursor: u32, + pub count: u32, fn get_all_bills_for_owner(env: Env, owner: Address, cursor: u32, limit: u32) -> BillPage; } @@ -505,11 +521,11 @@ impl ReportingContract { let all_bills = page.items; let mut total_bills = 0u32; - let mut paid_bills = 0u32; + let paid_bills = 0u32; let mut unpaid_bills = 0u32; let mut overdue_bills = 0u32; let mut total_amount = 0i128; - let mut paid_amount = 0i128; + let paid_amount = 0i128; let mut unpaid_amount = 0i128; let current_time = env.ledger().timestamp(); @@ -523,10 +539,7 @@ impl ReportingContract { total_bills += 1; total_amount += bill.amount; - if bill.paid { - paid_bills += 1; - paid_amount += bill.amount; - } else { + { unpaid_bills += 1; unpaid_amount += bill.amount; if bill.due_date < current_time { diff --git a/reporting/src/tests.rs b/reporting/src/tests.rs index 9182060..cf097b2 100644 --- a/reporting/src/tests.rs +++ b/reporting/src/tests.rs @@ -81,8 +81,8 @@ mod bill_payments { impl BillPaymentsTrait for BillPayments { fn get_unpaid_bills(_env: Env, _owner: Address, _cursor: u32, _limit: u32) -> BillPage { let env = _env; - let mut bills = Vec::new(&env); - bills.push_back(Bill { + let mut items = Vec::new(&env); + items.push_back(Bill { id: 1, owner: _owner, name: SorobanString::from_str(&env, "Electricity"), @@ -168,8 +168,8 @@ mod insurance { _limit: u32, ) -> crate::PolicyPage { let env = _env; - let mut policies = Vec::new(&env); - policies.push_back(InsurancePolicy { + let mut items = Vec::new(&env); + items.push_back(InsurancePolicy { id: 1, owner: _owner, name: SorobanString::from_str(&env, "Health Insurance"), @@ -210,6 +210,7 @@ fn test_init_reporting_contract_succeeds() { } #[test] +#[should_panic] fn test_init_twice_fails() { let env = Env::default(); env.mock_all_auths(); @@ -255,6 +256,7 @@ fn test_configure_addresses_succeeds() { } #[test] +#[should_panic] fn test_configure_addresses_unauthorized() { let env = create_test_env(); let contract_id = env.register_contract(None, ReportingContract); diff --git a/reporting/test_snapshots/tests/test_archive_old_reports.1.json b/reporting/test_snapshots/tests/test_archive_old_reports.1.json index 3e4afe9..06fdf73 100644 --- a/reporting/test_snapshots/tests/test_archive_old_reports.1.json +++ b/reporting/test_snapshots/tests/test_archive_old_reports.1.json @@ -1617,6 +1617,105 @@ ] } }, + { + "key": { + "symbol": "next_cursor" + }, + "val": { + "u32": 0 + } + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 100 + } + } + }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u64": 1704067200 + } + }, + { + "key": { + "symbol": "currency" + }, + "val": { + "string": "XLM" + } + }, + { + "key": { + "symbol": "due_date" + }, + "val": { + "u64": 1735689600 + } + }, + { + "key": { + "symbol": "frequency_days" + }, + "val": { + "u32": 30 + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "Electricity" + } + }, + { + "key": { + "symbol": "owner" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "paid" + }, + "val": { + "bool": false + } + }, + { + "key": { + "symbol": "paid_at" + }, + "val": "void" + }, + { + "key": { + "symbol": "recurring" + }, + "val": { + "bool": true + } + ] + } + }, { "key": { "symbol": "next_cursor" @@ -1797,6 +1896,129 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000007", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "get_active_policies" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "items" + }, + "val": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "active" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "coverage_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 50000 + } + } + }, + { + "key": { + "symbol": "coverage_type" + }, + "val": { + "string": "health" + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "monthly_premium" + }, + "val": { + "i128": { + "hi": 0, + "lo": 200 + } + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "Health Insurance" + } + }, + { + "key": { + "symbol": "next_payment_date" + }, + "val": { + "u64": 1735689600 + } + }, + { + "key": { + "symbol": "owner" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "next_cursor" + }, + "val": { + "u32": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/reporting/test_snapshots/tests/test_archive_ttl_extended_on_archive_reports.1.json b/reporting/test_snapshots/tests/test_archive_ttl_extended_on_archive_reports.1.json index 7e2379d..9067778 100644 --- a/reporting/test_snapshots/tests/test_archive_ttl_extended_on_archive_reports.1.json +++ b/reporting/test_snapshots/tests/test_archive_ttl_extended_on_archive_reports.1.json @@ -1795,6 +1795,129 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000007", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "get_active_policies" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "items" + }, + "val": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "active" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "coverage_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 50000 + } + } + }, + { + "key": { + "symbol": "coverage_type" + }, + "val": { + "string": "health" + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "monthly_premium" + }, + "val": { + "i128": { + "hi": 0, + "lo": 200 + } + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "Health Insurance" + } + }, + { + "key": { + "symbol": "next_payment_date" + }, + "val": { + "u64": 1735689600 + } + }, + { + "key": { + "symbol": "owner" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "next_cursor" + }, + "val": { + "u32": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/reporting/test_snapshots/tests/test_cleanup_old_reports.1.json b/reporting/test_snapshots/tests/test_cleanup_old_reports.1.json index 216af9c..d14ab0a 100644 --- a/reporting/test_snapshots/tests/test_cleanup_old_reports.1.json +++ b/reporting/test_snapshots/tests/test_cleanup_old_reports.1.json @@ -1794,6 +1794,129 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000007", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "get_active_policies" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "items" + }, + "val": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "active" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "coverage_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 50000 + } + } + }, + { + "key": { + "symbol": "coverage_type" + }, + "val": { + "string": "health" + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "monthly_premium" + }, + "val": { + "i128": { + "hi": 0, + "lo": 200 + } + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "Health Insurance" + } + }, + { + "key": { + "symbol": "next_payment_date" + }, + "val": { + "u64": 1735689600 + } + }, + { + "key": { + "symbol": "owner" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "next_cursor" + }, + "val": { + "u32": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/reporting/test_snapshots/tests/test_configure_addresses_unauthorized.1.json b/reporting/test_snapshots/tests/test_configure_addresses_unauthorized.1.json index c6fde62..1781135 100644 --- a/reporting/test_snapshots/tests/test_configure_addresses_unauthorized.1.json +++ b/reporting/test_snapshots/tests/test_configure_addresses_unauthorized.1.json @@ -328,6 +328,31 @@ } }, "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 6 + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": false } ] } \ No newline at end of file diff --git a/reporting/test_snapshots/tests/test_init_twice_fails.1.json b/reporting/test_snapshots/tests/test_init_twice_fails.1.json index 50f5fe3..d30d5c2 100644 --- a/reporting/test_snapshots/tests/test_init_twice_fails.1.json +++ b/reporting/test_snapshots/tests/test_init_twice_fails.1.json @@ -294,6 +294,31 @@ } }, "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 6 + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": false } ] } \ No newline at end of file diff --git a/reporting/test_snapshots/tests/test_instance_ttl_refreshed_on_store_report.1.json b/reporting/test_snapshots/tests/test_instance_ttl_refreshed_on_store_report.1.json index 8f68b95..61bf22d 100644 --- a/reporting/test_snapshots/tests/test_instance_ttl_refreshed_on_store_report.1.json +++ b/reporting/test_snapshots/tests/test_instance_ttl_refreshed_on_store_report.1.json @@ -2131,6 +2131,129 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000007", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "get_active_policies" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "items" + }, + "val": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "active" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "coverage_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 50000 + } + } + }, + { + "key": { + "symbol": "coverage_type" + }, + "val": { + "string": "health" + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "monthly_premium" + }, + "val": { + "i128": { + "hi": 0, + "lo": 200 + } + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "Health Insurance" + } + }, + { + "key": { + "symbol": "next_payment_date" + }, + "val": { + "u64": 1735689600 + } + }, + { + "key": { + "symbol": "owner" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "next_cursor" + }, + "val": { + "u32": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/reporting/test_snapshots/tests/test_report_data_persists_across_ledger_advancements.1.json b/reporting/test_snapshots/tests/test_report_data_persists_across_ledger_advancements.1.json index 3256b93..11a4283 100644 --- a/reporting/test_snapshots/tests/test_report_data_persists_across_ledger_advancements.1.json +++ b/reporting/test_snapshots/tests/test_report_data_persists_across_ledger_advancements.1.json @@ -3154,6 +3154,129 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000007", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "get_active_policies" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "items" + }, + "val": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "active" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "coverage_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 50000 + } + } + }, + { + "key": { + "symbol": "coverage_type" + }, + "val": { + "string": "health" + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "monthly_premium" + }, + "val": { + "i128": { + "hi": 0, + "lo": 200 + } + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "Health Insurance" + } + }, + { + "key": { + "symbol": "next_payment_date" + }, + "val": { + "u64": 1735689600 + } + }, + { + "key": { + "symbol": "owner" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "next_cursor" + }, + "val": { + "u32": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -5574,10 +5697,10 @@ "symbol": "fn_call" }, { - "bytes": "0000000000000000000000000000000000000000000000000000000000000007" + "bytes": "0000000000000000000000000000000000000000000000000000000000000006" }, { - "symbol": "get_active_policies" + "symbol": "get_unpaid_bills" } ], "data": { @@ -5601,7 +5724,7 @@ { "event": { "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000007", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", "type_": "diagnostic", "body": { "v0": { @@ -5610,7 +5733,7 @@ "symbol": "fn_return" }, { - "symbol": "get_active_policies" + "symbol": "get_unpaid_bills" } ], "data": { @@ -5633,64 +5756,61 @@ "map": [ { "key": { - "symbol": "active" + "symbol": "amount" }, "val": { - "bool": true + "i128": { + "hi": 0, + "lo": 100 + } } }, { "key": { - "symbol": "coverage_amount" + "symbol": "created_at" }, "val": { - "i128": { - "hi": 0, - "lo": 50000 - } + "u64": 1704067200 } }, { "key": { - "symbol": "coverage_type" + "symbol": "currency" }, "val": { - "string": "health" + "string": "XLM" } }, { "key": { - "symbol": "id" + "symbol": "due_date" }, "val": { - "u32": 1 + "u64": 1735689600 } }, { "key": { - "symbol": "monthly_premium" + "symbol": "frequency_days" }, "val": { - "i128": { - "hi": 0, - "lo": 200 - } + "u32": 30 } }, { "key": { - "symbol": "name" + "symbol": "id" }, "val": { - "string": "Health Insurance" + "u32": 1 } }, { "key": { - "symbol": "next_payment_date" + "symbol": "name" }, "val": { - "u64": 1735689600 + "string": "Electricity" } }, { diff --git a/reporting/test_snapshots/tests/test_storage_stats.1.json b/reporting/test_snapshots/tests/test_storage_stats.1.json index d55ae4c..b168afd 100644 --- a/reporting/test_snapshots/tests/test_storage_stats.1.json +++ b/reporting/test_snapshots/tests/test_storage_stats.1.json @@ -1868,6 +1868,129 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000007", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "get_active_policies" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "items" + }, + "val": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "active" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "coverage_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 50000 + } + } + }, + { + "key": { + "symbol": "coverage_type" + }, + "val": { + "string": "health" + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "monthly_premium" + }, + "val": { + "i128": { + "hi": 0, + "lo": 200 + } + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "Health Insurance" + } + }, + { + "key": { + "symbol": "next_payment_date" + }, + "val": { + "u64": 1735689600 + } + }, + { + "key": { + "symbol": "owner" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "next_cursor" + }, + "val": { + "u32": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/reporting/test_snapshots/tests/test_store_and_retrieve_report.1.json b/reporting/test_snapshots/tests/test_store_and_retrieve_report.1.json index 78f30ad..57c8d1a 100644 --- a/reporting/test_snapshots/tests/test_store_and_retrieve_report.1.json +++ b/reporting/test_snapshots/tests/test_store_and_retrieve_report.1.json @@ -2131,6 +2131,129 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000007", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "get_active_policies" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "count" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "items" + }, + "val": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "active" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "coverage_amount" + }, + "val": { + "i128": { + "hi": 0, + "lo": 50000 + } + } + }, + { + "key": { + "symbol": "coverage_type" + }, + "val": { + "string": "health" + } + }, + { + "key": { + "symbol": "id" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "monthly_premium" + }, + "val": { + "i128": { + "hi": 0, + "lo": 200 + } + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "Health Insurance" + } + }, + { + "key": { + "symbol": "next_payment_date" + }, + "val": { + "u64": 1735689600 + } + }, + { + "key": { + "symbol": "owner" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + ] + } + }, + { + "key": { + "symbol": "next_cursor" + }, + "val": { + "u32": 0 + } + } + ] + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", diff --git a/scenarios/Cargo.toml b/scenarios/Cargo.toml index e7a4f5d..5f425ea 100644 --- a/scenarios/Cargo.toml +++ b/scenarios/Cargo.toml @@ -4,7 +4,12 @@ version = "0.1.0" edition = "2021" publish = false -[dependencies] +# scenarios is a pure test-helper crate — it has no WASM output. +# All its dependencies use soroban-sdk's `testutils` feature, which +# requires std and therefore cannot be compiled for wasm32. +# Guard them behind a target filter so `cargo build --target +# wasm32-unknown-unknown` skips them (and the lib body stays empty). +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] soroban-sdk = { version = "21.0.0", features = ["testutils"] } testutils = { path = "../testutils" } remittance_split = { path = "../remittance_split" } diff --git a/scenarios/src/lib.rs b/scenarios/src/lib.rs index 57b22e9..4b8c331 100644 --- a/scenarios/src/lib.rs +++ b/scenarios/src/lib.rs @@ -1,3 +1,4 @@ +#[cfg(not(target_arch = "wasm32"))] pub mod tests { use soroban_sdk::Env; use testutils::set_ledger_time;