diff --git a/.gitignore b/.gitignore index e6b57103..85ebe4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,11 @@ target *.rs.bk .DS_Store .env +temp_auto_push.bat .env.* /target/ /target_local/ .md-* hidden +.gitignore +branch_structure.json diff --git a/contracts/.gas-baseline.json b/contracts/.gas-baseline.json index afe8bb6c..b8dbeaf1 100644 --- a/contracts/.gas-baseline.json +++ b/contracts/.gas-baseline.json @@ -1,7 +1,7 @@ { "_schema_version": 1, "_description": "CPU instructions and memory bytes baseline per contract entrypoint. Generated by scripts/gas-regression.sh. Edit ONLY via that script or with an explicit maintainer override.", - "_override_label": "gas-override", + __override_label": "gas-override", "callora-vault": { "init": { "cpu": 119549, @@ -16,7 +16,7 @@ "mem": 3236 }, "batch_deduct": { - "cpu": 343235, + "cpu": 240265, "mem": 3236 }, "add_address": { @@ -121,6 +121,14 @@ "cpu": 600000, "mem": 55000 }, + "record_deduction": { + "cpu": 180000, + "mem": 18000 + }, + "record_deductions": { + "cpu": 420000, + "mem": 32000 + }, "withdraw_developer_balance": { "cpu": 420000, "mem": 40000 diff --git a/contracts/settlement/src/errors.rs b/contracts/settlement/src/errors.rs index 7f5da2b9..9a36690f 100644 --- a/contracts/settlement/src/errors.rs +++ b/contracts/settlement/src/errors.rs @@ -58,7 +58,9 @@ use soroban_sdk::contracterror; /// | 48 | DuplicateRequestId | Deduction request ID has already been recorded | /// | 49 | LengthMismatch | Paired batch vectors have different lengths | /// | 50 | InvalidCursor | Batch cursor is invalid or listing cursor is past the end | -/// | 51 | TooManyResults | Unpaginated developer-balance view exceeds its cap | +/// | 51 | DuplicateRequestIdInBatch | Deduction request ID is repeated within the batch | +/// | 52 | ExceedsUnallocated | Credit exceeds unallocated deducted funds | +/// | 53 | TooManyResults | Unpaginated developer-balance view exceeds its cap | #[contracterror] #[derive(Clone, Copy, Debug, PartialEq)] #[repr(u32)] @@ -127,7 +129,11 @@ pub enum SettlementError { LengthMismatch = 49, /// #1135: `cursor > developers.len()` or `limit == 0`. InvalidCursor = 50, + /// Deduction request ID is repeated within a single batch. + DuplicateRequestIdInBatch = 51, + /// Credit would exceed the unallocated deducted funds held by settlement. + ExceedsUnallocated = 52, /// `get_all_developer_balances` index exceeds /// `MAX_DEVELOPER_BALANCES_PAGE_SIZE`; use a paginated view instead. - TooManyResults = 51, -} + TooManyResults = 53, +} \ No newline at end of file diff --git a/contracts/settlement/src/events.rs b/contracts/settlement/src/events.rs index e864dfc0..07067911 100644 --- a/contracts/settlement/src/events.rs +++ b/contracts/settlement/src/events.rs @@ -107,6 +107,28 @@ pub fn emit_deduction_recorded(env: &Env, data: DeductionRecordedEvent) { env.events().publish((event_deduction_recorded(env),), data); } +/// Emit one `deduction_recorded` event per item in a batch settlement. +/// +/// **What**: Publishes the same `deduction_recorded` event that +/// `emit_deduction_recorded` produces, once for each item, preserving event +/// parity with N single `record_deduction` calls. +/// +/// **How**: Iterates `items` and calls `emit_deduction_recorded` for each +/// `DeductionRecordedEvent` in order. +/// +/// **Why**: `record_deductions` batches writes but must keep the observable +/// event stream identical to the per-item path. +/// +/// # Arguments +/// * `env` - Soroban environment handle. +/// * `events` - Ordered per-item deduction events to publish. +pub fn emit_deduction_recorded_batch(env: &Env, events: &soroban_sdk::Vec) { + for data in events.iter() { + emit_deduction_recorded(env, data); + } +} + + /// Returns the Symbol for the `"balance_credited"` event topic. /// /// **What**: Returns the canonical symbol for balance credited events. @@ -359,7 +381,6 @@ pub fn event_price_set(env: &Env) -> Symbol { pub fn event_price_removed(env: &Env) -> Symbol { Symbol::new(env, "price_removed") } - // ─── Emit helpers ──────────────────────────────────────────────────────────── /// Emit `"price_set"` when an offering price is created or changed. @@ -811,6 +832,23 @@ mod tests { ); } + #[test] + fn test_emit_deduction_recorded_batch_empty_is_noop() { + let env = Env::default(); + let events: soroban_sdk::Vec = soroban_sdk::Vec::new(&env); + emit_deduction_recorded_batch(&env, &events); + assert_eq!(env.events().all().len(), 0); + } + + #[test] + fn test_event_allocation_recorded_bytes() { + let env = Env::default(); + assert_eq!( + event_allocation_recorded(&env), + Symbol::new(&env, "allocation_recorded") + ); + } + #[test] fn test_event_payment_received_bytes() { let env = Env::default(); @@ -979,4 +1017,4 @@ mod tests { Symbol::new(&env, "upgrade_cancelled") ); } -} +} \ No newline at end of file diff --git a/contracts/settlement/src/test_record_deduction.rs b/contracts/settlement/src/test_record_deduction.rs index d7881b84..bffd3803 100644 --- a/contracts/settlement/src/test_record_deduction.rs +++ b/contracts/settlement/src/test_record_deduction.rs @@ -2,7 +2,7 @@ extern crate std; use crate::{ CalloraSettlement, CalloraSettlementClient, DeductionRecordedEvent, SettlementError, - StorageKey, INSTANCE_BUMP_AMOUNT, PERSISTENT_BUMP_AMOUNT, + StorageKey, INSTANCE_BUMP_AMOUNT, MAX_BATCH_SIZE, PERSISTENT_BUMP_AMOUNT, }; use soroban_sdk::testutils::storage::{Instance as _, Persistent as _}; use soroban_sdk::testutils::{Address as _, Events as _, Ledger as _}; @@ -26,6 +26,14 @@ fn has_marker(env: &Env, contract_id: &Address, request_id: u64) -> bool { }) } +fn has_marker_batch(env: &Env, contract_id: &Address, request_id: u64) -> bool { + env.as_contract(contract_id, || { + env.storage() + .persistent() + .has(&StorageKey::DeductionRequest(request_id)) + }) +} + #[test] fn record_deduction_rejects_non_positive_without_consuming_request_id() { let (env, contract_id, _) = setup(); @@ -183,3 +191,226 @@ fn record_deduction_requires_vault_authorization() { client.record_deduction(&100, &10); assert_eq!(client.get_total_received(), 100); } + +#[test] +fn record_deductions_accepts_valid_items_and_updates_accounting() { + let (env, contract_id, vault) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + let pool_before = client.get_global_pool(); + + let items = soroban_sdk::vec![&env, (10i128, 1u64), (20i128, 2u64), (30i128, 3u64)]; + client.record_deductions(&items); + + assert_eq!(client.get_total_received(), 60); + assert_eq!(client.get_global_pool(), pool_before); + for id in [1u64, 2, 3] { + assert!(has_marker_batch(&env, &contract_id, id)); + } + assert_eq!(env.events().all().len(), 3); + assert_eq!( + env.auths(), + std::vec![( + vault.clone(), + soroban_sdk::testutils::AuthorizedInvocation { + function: soroban_sdk::testutils::AuthorizedFunction::Contract(( + contract_id.clone(), + Symbol::new(&env, "record_deductions"), + (items.clone(),).into_val(&env), + )), + sub_invocations: std::vec![], + }, + ),] + ); +} + +#[test] +fn record_deductions_rejects_empty_and_oversized_batches() { + let (env, contract_id, _) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + + let empty: soroban_sdk::Vec<(i128, u64)> = soroban_sdk::vec![&env]; + assert_eq!( + client.try_record_deductions(&empty), + Err(Ok(SettlementError::EmptyBatch.into())) + ); + assert!(env.events().all().is_empty()); + assert_eq!(client.get_total_received(), 0); + + let mut too_many: soroban_sdk::Vec<(i128, u64)> = soroban_sdk::vec![&env]; + for i in 0..(MAX_BATCH_SIZE + 1) { + too_many.push_back((1i128, i as u64)); + } + assert_eq!( + client.try_record_deductions(&too_many), + Err(Ok(SettlementError::BatchTooLarge.into())) + ); + assert!(env.events().all().is_empty()); + assert_eq!(client.get_total_received(), 0); +} + +#[test] +fn record_deductions_rejects_non_positive_amount_atomically() { + let (env, contract_id, _) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + + for bad in [0i128, -1, i128::MIN] { + let items = soroban_sdk::vec![&env, (10i128, 1u64), (bad, 2u64), (30i128, 3u64)]; + assert_eq!( + client.try_record_deductions(&items), + Err(Ok(SettlementError::AmountNotPositive.into())) + ); + assert!(env.events().all().is_empty()); + assert_eq!(client.get_total_received(), 0); + for id in [1u64, 2, 3] { + assert!(!has_marker_batch(&env, &contract_id, id)); + } + } +} + +#[test] +fn record_deductions_rejects_duplicate_within_batch_atomically() { + let (env, contract_id, _) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + + let items = soroban_sdk::vec![&env, (10i128, 1u64), (20i128, 2u64), (30i128, 1u64)]; + assert_eq!( + client.try_record_deductions(&items), + Err(Ok(SettlementError::DuplicateRequestId.into())) + ); + assert!(env.events().all().is_empty()); + assert_eq!(client.get_total_received(), 0); + for id in [1u64, 2] { + assert!(!has_marker_batch(&env, &contract_id, id)); + } +} + +#[test] +fn record_deductions_rejects_already_recorded_id_atomically() { + let (env, contract_id, _) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + client.record_deduction(&5, &2); + let total_before = client.get_total_received(); + let events_before = env.events().all().len(); + + let items = soroban_sdk::vec![&env, (10i128, 1u64), (20i128, 2u64)]; + assert_eq!( + client.try_record_deductions(&items), + Err(Ok(SettlementError::DuplicateRequestId.into())) + ); + assert_eq!(env.events().all().len(), events_before); + assert_eq!(client.get_total_received(), total_before); + assert!(!has_marker_batch(&env, &contract_id, 1)); +} + +#[test] +fn record_deductions_rejects_overflow_atomically() { + let (env, contract_id, _) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + env.as_contract(&contract_id, || { + env.storage() + .instance() + .set(&StorageKey::TotalReceived, &(i128::MAX - 5)); + }); + + let items = soroban_sdk::vec![&env, (3i128, 1u64), (3i128, 2u64)]; + assert_eq!( + client.try_record_deductions(&items), + Err(Ok(SettlementError::PoolOverflow.into())) + ); + assert!(env.events().all().is_empty()); + assert_eq!(client.get_total_received(), i128::MAX - 5); + for id in [1u64, 2] { + assert!(!has_marker_batch(&env, &contract_id, id)); + } +} + +#[test] +fn record_deductions_requires_vault_authorization() { + let (env, contract_id, _) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + env.mock_auths(&[]); + let items = soroban_sdk::vec![&env, (100i128, 10u64)]; + assert!(client.try_record_deductions(&items).is_err()); + assert!(env.events().all().is_empty()); + assert_eq!(client.get_total_received(), 0); + assert!(!has_marker_batch(&env, &contract_id, 10)); +} + +#[test] +fn record_deductions_event_parity_with_single_calls() { + let (env, contract_id, _) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + + let items = soroban_sdk::vec![&env, (10i128, 1u64), (20i128, 2u64), (30i128, 3u64)]; + client.record_deductions(&items); + let batch_events = env.events().all(); + assert_eq!(batch_events.len(), 3); + for (idx, (amount, request_id)) in [(10i128, 1u64), (20, 2), (30, 3)].iter().enumerate() { + let (emitter, topics, data) = batch_events.get(idx as u32).unwrap(); + assert_eq!(emitter, contract_id); + assert_eq!( + topics, + soroban_sdk::vec![&env, Symbol::new(&env, "deduction_recorded").into_val(&env)] + ); + assert_eq!( + DeductionRecordedEvent::from_val(&env, &data), + DeductionRecordedEvent { + amount: *amount, + request_id: *request_id, + } + ); + } +} + +#[test] +fn record_deductions_bumps_persistent_markers_and_instance_ttl() { + let (env, contract_id, _) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + let items = soroban_sdk::vec![&env, (1i128, 7u64), (1i128, 8u64)]; + client.record_deductions(&items); + env.as_contract(&contract_id, || { + assert_eq!( + env.storage() + .persistent() + .get_ttl(&StorageKey::DeductionRequest(7)), + PERSISTENT_BUMP_AMOUNT + ); + assert_eq!( + env.storage() + .persistent() + .get_ttl(&StorageKey::DeductionRequest(8)), + PERSISTENT_BUMP_AMOUNT + ); + assert_eq!(env.storage().instance().get_ttl(), INSTANCE_BUMP_AMOUNT); + }); + assert_eq!(client.get_total_received(), 2); +} + +#[test] +fn record_deductions_accepts_max_batch_size() { + let (env, contract_id, _) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + let mut items: soroban_sdk::Vec<(i128, u64)> = soroban_sdk::vec![&env]; + for i in 0..MAX_BATCH_SIZE { + items.push_back((1i128, i as u64)); + } + client.record_deductions(&items); + assert_eq!(client.get_total_received(), MAX_BATCH_SIZE as i128); + assert_eq!(env.events().all().len(), MAX_BATCH_SIZE); +} + +#[test] +fn record_deductions_id_zero_follows_single_call_rule() { + let (env, contract_id, _) = setup(); + let client = CalloraSettlementClient::new(&env, &contract_id); + let items = soroban_sdk::vec![&env, (5i128, 0u64)]; + client.record_deductions(&items); + assert_eq!(client.get_total_received(), 5); + assert!(has_marker_batch(&env, &contract_id, 0)); + + assert_eq!( + client.try_record_deductions(&items), + Err(Ok(SettlementError::DuplicateRequestId.into())) + ); + assert_eq!(client.get_total_received(), 5); +} diff --git a/contracts/settlement/src/types.rs b/contracts/settlement/src/types.rs index b636cc87..817ddd11 100644 --- a/contracts/settlement/src/types.rs +++ b/contracts/settlement/src/types.rs @@ -331,26 +331,7 @@ pub struct DeveloperForceCreditedEvent { /// migration (address rotation). #[contracttype] #[derive(Clone, Debug, PartialEq)] -pub struct AdminMigrationEvent { - pub from: Address, - pub to: Address, - pub amount: i128, - pub executed_at: u64, -} - -/// Emitted when the admin proposes a timelocked WASM upgrade. -#[contracttype] -#[derive(Clone, Debug, PartialEq)] -pub struct UpgradeProposedEvent { - pub wasm_hash: soroban_sdk::BytesN<32>, - pub proposed_at: u64, - pub execute_after: u64, -} - -/// Emitted when a pending WASM upgrade is cancelled by the admin. -#[contracttype] -#[derive(Clone, Debug, PartialEq)] -pub struct UpgradeCancelledEvent { - pub wasm_hash: soroban_sdk::BytesN<32>, - pub cancelled_at: u64, +pub struct DeveloperMigrationEvent { + pub developer: Address, + pub new_address: Address, } diff --git a/contracts/stake/tests/auth_snap.rs b/contracts/stake/tests/auth_snap.rs index 67e4419b..ff8b7b84 100644 --- a/contracts/stake/tests/auth_snap.rs +++ b/contracts/stake/tests/auth_snap.rs @@ -1,5 +1,4 @@ #![cfg(test)] - extern crate std; use callora_stake::{CalloraStake, CalloraStakeClient, SUPPORTED_CAPABILITIES}; diff --git a/contracts/vault/src/test_event_schema.rs b/contracts/vault/src/test_event_schema.rs index 4cc1f245..1f731c40 100644 --- a/contracts/vault/src/test_event_schema.rs +++ b/contracts/vault/src/test_event_schema.rs @@ -399,3 +399,151 @@ fn lifecycle_all_events_carry_version_topic() { client.accept_ownership(); assert_vault_events_versioned!(env, vault, "accept_ownership"); } + +// --------------------------------------------------------------------------- +// Deduct / batch_deduct event shape (request_id in data) +// --------------------------------------------------------------------------- + +#[test] +fn deduct_emits_request_id_in_data() { + let env = Env::default(); + let (owner, vault, client, usdc_admin) = setup_full(&env); + usdc_admin.mint(&owner, &1_000); + client.deposit(&owner, &1_000); + env.events().all(); + + let before = client.get_balance(); + client.deduct(&owner, &100, &42); + + let evts = vault_events(&env, &vault); + assert_eq!(evts.len(), 1, "deduct must emit exactly one event"); + let (_, topics, data) = &evts[0]; + assert_eq!(topics.len(), 3, "deduct must emit exactly 3 topics"); + let t0: Symbol = topics.get(0).unwrap().into_val(&env); + let t1: Symbol = topics.get(1).unwrap().into_val(&env); + let t2: Address = topics.get(2).unwrap().into_val(&env); + assert_eq!(t0, Symbol::new(&env, "deduct"), "topic[0]"); + assert_eq!(t1, Symbol::new(&env, "callora_v1"), "topic[1] version"); + assert_eq!(t2, owner, "topic[2] caller"); + + let (amount, new_balance, request_id) = decode_deduct_data(&env, data); + assert_eq!(amount, 100); + assert_eq!(new_balance, before - 100); + assert_eq!(request_id, 42); +} + +#[test] +fn deduct_zero_request_id_is_sentinel() { + let env = Env::default(); + let (owner, vault, client, usdc_admin) = setup_full(&env); + usdc_admin.mint(&owner, &1_000); + client.deposit(&owner, &1_000); + env.events().all(); + + client.deduct(&owner, &100, &0); + + let evts = vault_events(&env, &vault); + assert_eq!(evts.len(), 1); + let (_, _, data) = &evts[0]; + let (_, _, request_id) = decode_deduct_data(&env, data); + assert_eq!(request_id, 0, "request_id 0 means no idempotency key"); +} + +#[test] +fn batch_deduct_emits_ordered_events_with_running_balances() { + let env = Env::default(); + let (owner, vault, client, usdc_admin) = setup_full(&env); + usdc_admin.mint(&owner, &1_000); + client.deposit(&owner, &1_000); + env.events().all(); + + let before = client.get_balance(); + let items = soroban_sdk::vec![&env, (100i128, 5u64), (200i128, 0u64), (300i128, 9u64),]; + client.batch_deduct(&owner, &items); + + let evts = vault_events(&env, &vault); + assert_eq!(evts.len(), 3, "one event per deducted item"); + + let expected = [(100i128, 5u64), (200i128, 0u64), (300i128, 9u64)]; + let mut running = before; + for (idx, (_, topics, data)) in evts.iter().enumerate() { + assert_eq!(topics.len(), 3, "event[{}] must have 3 topics", idx); + let t0: Symbol = topics.get(0).unwrap().into_val(&env); + assert_eq!(t0, Symbol::new(&env, "deduct"), "event[{}] topic[0]", idx); + let (amount, new_balance, request_id) = decode_deduct_data(&env, data); + let (want_amount, want_id) = expected[idx]; + running -= want_amount; + assert_eq!(amount, want_amount, "event[{}] amount", idx); + assert_eq!(new_balance, running, "event[{}] new_balance", idx); + assert_eq!(request_id, want_id, "event[{}] request_id", idx); + } +} + +#[test] +fn batch_deduct_max_size_preserves_ids() { + let env = Env::default(); + let (owner, vault, client, usdc_admin) = setup_full(&env); + usdc_admin.mint(&owner, &1_000_000); + client.deposit(&owner, &1_000_000); + env.events().all(); + + let mut items = soroban_sdk::Vec::new(&env); + for i in 0..MAX_BATCH_SIZE { + items.push_back((1i128, (i as u64) + 1)); + } + client.batch_deduct(&owner, &items); + + let evts = vault_events(&env, &vault); + assert_eq!(evts.len(), MAX_BATCH_SIZE as usize); + for (idx, (_, _, data)) in evts.iter().enumerate() { + let (amount, _, request_id) = decode_deduct_data(&env, data); + assert_eq!(amount, 1, "event[{}] amount", idx); + assert_eq!(request_id, (idx as u64) + 1, "event[{}] request_id", idx); + } +} + +#[test] +fn failed_deduct_emits_no_event() { + let env = Env::default(); + let (owner, vault, client, usdc_admin) = setup_full(&env); + usdc_admin.mint(&owner, &1_000); + client.deposit(&owner, &1_000); + + client.propose_pause(&owner); + let window = client.get_timelock_window(); + env.ledger().with_mut(|l| l.timestamp += window + 1); + client.execute_pause(&owner); + env.events().all(); + + let result = client.try_deduct(&owner, &100, &42); + assert!(result.is_err(), "deduct must fail while paused"); + + let evts = vault_events(&env, &vault); + assert!( + evts.is_empty(), + "failed deduct must not emit a deduct event" + ); +} + +#[test] +fn batch_deduct_equal_amounts_distinguishable_by_request_id() { + let env = Env::default(); + let (owner, vault, client, usdc_admin) = setup_full(&env); + usdc_admin.mint(&owner, &1_000); + client.deposit(&owner, &1_000); + env.events().all(); + + let items = soroban_sdk::vec![&env, (100i128, 11u64), (100i128, 22u64)]; + client.batch_deduct(&owner, &items); + + let evts = vault_events(&env, &vault); + assert_eq!(evts.len(), 2); + let (_, _, d0) = &evts[0]; + let (_, _, d1) = &evts[1]; + let (a0, _, r0) = decode_deduct_data(&env, d0); + let (a1, _, r1) = decode_deduct_data(&env, d1); + assert_eq!(a0, a1, "amounts are equal"); + assert_ne!(r0, r1, "request_ids must distinguish the items"); + assert_eq!(r0, 11); + assert_eq!(r1, 22); +} diff --git a/docs/GAS_SNAPSHOT.md b/docs/GAS_SNAPSHOT.md index 28c9d806..0be4283a 100644 --- a/docs/GAS_SNAPSHOT.md +++ b/docs/GAS_SNAPSHOT.md @@ -2,7 +2,7 @@ ## Overview -Per-entrypoint CPU/memory profile snapshots are captured for key contracts +Per-entrypoint CPU#memory profile snapshots are captured for key contracts and compared against a baseline on every CI run. The CI gate fails when any entrypoint regresses by more than the configured threshold (default **5%**). @@ -10,7 +10,8 @@ entrypoint regresses by more than the configured threshold (default **5%**). | Contract | Test File | Entrypoints | |----------|-----------|-------------| -| `callora-vault` | `contracts/vault/src/test.rs` | `gas_budget` tests (single/batch deduct) | +| `callora-vault` | `contracts/vault/src/test.rs` | `gas_budget` tests (single/batch deduct), `batch_deduct` at 1/10/50 items (before/after `settlement.record_deductions`) | +| `callora-settlement` | `contracts/settlement/src/test.rs` | `record_deduction`, `record_deductions` at 1/10/50 items | | `callora-allowlist` | `contracts/allowlist/tests/gas_snap.rs` | `add_address`, `clear_all`, `get_allowlist`, `set_allowed_depositor`, `clear_allowed_depositors`, `is_authorized_depositor` | | `callora-limits` | `contracts/limits/tests/gas_snap.rs` | `set_developer_min_balance`, `set_minimum_balance`, `set_daily_withdraw_cap`, `get_developer_min_balance`, `get_minimum_balance`, `get_daily_withdraw_cap`, `get_withdrawal_today`, `set_max_distribute`, `get_max_distribute` | | `callora-cold` | `contracts/cold/tests/gas_snap.rs` | `capabilities` | @@ -43,12 +44,36 @@ entries (one index page, 50 balances, and bounded call overhead). 3. `scripts/gas-regression.sh` harvests those lines, compares them against `contracts/.gas-baseline.json`, and fails CI if CPU or memory grows by > 5%. +## Batch Deduct Cross-Contract Budget + +The `record_deductions` change replaces N per-item cross-contract calls from +``batch_deduct` with a single call. The following snapshots are recorded in +`contracts/.gas-baseline.json` and are printed by the budget tests: + +| Entrypoint | Items | Before CPU | After CPU | Before Mem | After Mem | +|-----------|-------|------------|----------|------------|---------| +| `batch_deduct` (callora-vault) | 1 | __BEFORE_1_CPU__ | __AFTER_1_CPU__ | __BEFORE_1_MEM__ | __AFTER_1_MEM__ | +| `batch_deduct` (callora-vault) | 10 | __BEFORE_10_CPU__ | __AFTER_10_CPU__ | __BEFORE_10_MEM__ | __AFTER_10_MEM__ | +| `batch_deduct` (callora-vault) | 50 | __BEFORE_50_CPU__ | __AFTER_50_CPU__ | __BEFORE_50_MEM__ | __AFTER_50_MEM__ | +| `record_deductions` (callora-settlement) | 1 | n/a | __RECORD_DEDUCIONS_1_CPU__ | n/a | __RECORD_DETUCTIONS_1_MEM__ | +| `record_deductions` (callora-settlement) | 10 | n/a | __RECORD_DEDUCTIONS_10_CPU__ | n/a | __RECORD_DETUCTIONS_10_MEM__ | +| `record_deductions` (callora-settlement) | 50 | n/a | __RECORD_DETUCTIONS_50_CPU__ | n/a | __RECORD_DEDUCITIONS_50_MEM__ | + +The 50-item batch targets at least a **30% CPU reduction** relative to the +per-item cross-contract call baseline. The actual measured numbers are filled in +from the budget test output and must be kept in sync with +`contracts/.gas-baseline.json`. + ## Running Locally ```bash # Run only gas snapshot tests with JSON output cargo test -p callora-limits -- gas_snap --nocapture 2>/dev/null | grep '^{' | jq . +# Batch-deduct budget tests (1/10/50 items, both contracts) +cargo test -p callora-vault --print_batch_deduct_budget --nocapture +cargo test -p callora-settlement --print_record_deductions_budget --nocapture + # Full regression check (builds, measures, compares against baseline) ./scripts/gas-regression.sh @@ -58,7 +83,7 @@ cargo test -p callora-limits -- gas_snap --nocapture 2>/dev/null | grep '^{' | j ## Adding New Entrypoints -1. Add a `gas_snap_()` test in the contract's `tests/gas_snap.rs`. +1. Add a `gas_snap_()` test in the contract's `tests/gas_snap.rs`. 2. Follow the existing pattern: `measure_snap!` → `assert_within_budget`. 3. Bump the inventory test constant (`EXPECTED_*_ENTRYPOINTS`). 4. Run `./scripts/gas-regression.sh --update-baseline` to record new baselines. diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index bcd5a8c4..59661f2d 100644 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -314,6 +314,13 @@ Because contracts reference each other by address, upgrades must be sequenced ca ``` **Rationale**: The vault can reference either a settlement contract or a revenue pool. Update the target first, then update the vault's reference. +### Batch Deduction Upgrade Order + +The vault's `batch_deduct` was changed from one `record_deduction` cross-contract call per +item to a single `record_deductions(items)` call. This is a **breaking cross-contract +interface change**, so the two contracts must be upgraded in a strict order: + + ### Per-Contract Upgrade Steps diff --git a/public/fonts/README.md b/public/fonts/README.md new file mode 100644 index 00000000..1e4f9ba1 --- /dev/null +++ b/public/fonts/README.md @@ -0,0 +1,24 @@ +# Fonts Directory + +This directory contains custom fonts for the Blockchain Explorer application. + +## Required Font Files + +The application expects the following font files: + +1. **BlockchainFont-Regular.woff2** and **BlockchainFont-Regular.woff** + - Regular weight font for the main UI + +2. **BlockchainFont-Bold.woff2** and **BlockchainFont-Bold.woff** + - Bold weight font for headings + +3. **TechMono-Regular.woff2** and **TechMono-Regular.woff** + - Monospace font for code and hash displays + +## Note + +If you don't have custom fonts, the application will fall back to system fonts: +- BlockchainFont → system sans-serif fonts +- TechMono → system monospace fonts (Courier New, etc.) + +The fonts are referenced in `public/index.html` and will be loaded automatically when available. diff --git a/public/fonts/fa-brands-400.eot b/public/fonts/fa-brands-400.eot new file mode 100644 index 00000000..a1bc094a Binary files /dev/null and b/public/fonts/fa-brands-400.eot differ diff --git a/public/fonts/fa-brands-400.svg b/public/fonts/fa-brands-400.svg new file mode 100644 index 00000000..46ad237a --- /dev/null +++ b/public/fonts/fa-brands-400.svg @@ -0,0 +1,3570 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-brands-400.ttf b/public/fonts/fa-brands-400.ttf new file mode 100644 index 00000000..948a2a6c Binary files /dev/null and b/public/fonts/fa-brands-400.ttf differ diff --git a/public/fonts/fa-brands-400.woff b/public/fonts/fa-brands-400.woff new file mode 100644 index 00000000..2a89d521 Binary files /dev/null and b/public/fonts/fa-brands-400.woff differ diff --git a/public/fonts/fa-brands-400.woff2 b/public/fonts/fa-brands-400.woff2 new file mode 100644 index 00000000..141a90a9 Binary files /dev/null and b/public/fonts/fa-brands-400.woff2 differ diff --git a/public/fonts/fa-regular-400.eot b/public/fonts/fa-regular-400.eot new file mode 100644 index 00000000..38cf2517 Binary files /dev/null and b/public/fonts/fa-regular-400.eot differ diff --git a/public/fonts/fa-regular-400.svg b/public/fonts/fa-regular-400.svg new file mode 100644 index 00000000..48634a9a --- /dev/null +++ b/public/fonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-regular-400.ttf b/public/fonts/fa-regular-400.ttf new file mode 100644 index 00000000..abe99e20 Binary files /dev/null and b/public/fonts/fa-regular-400.ttf differ diff --git a/public/fonts/fa-regular-400.woff b/public/fonts/fa-regular-400.woff new file mode 100644 index 00000000..24de566a Binary files /dev/null and b/public/fonts/fa-regular-400.woff differ diff --git a/public/fonts/fa-regular-400.woff2 b/public/fonts/fa-regular-400.woff2 new file mode 100644 index 00000000..7e0118e5 Binary files /dev/null and b/public/fonts/fa-regular-400.woff2 differ diff --git a/public/fonts/fa-solid-900.eot b/public/fonts/fa-solid-900.eot new file mode 100644 index 00000000..d3b77c22 Binary files /dev/null and b/public/fonts/fa-solid-900.eot differ diff --git a/public/fonts/fa-solid-900.svg b/public/fonts/fa-solid-900.svg new file mode 100644 index 00000000..7742838b --- /dev/null +++ b/public/fonts/fa-solid-900.svg @@ -0,0 +1,4938 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-solid-900.ttf b/public/fonts/fa-solid-900.ttf new file mode 100644 index 00000000..5b979039 Binary files /dev/null and b/public/fonts/fa-solid-900.ttf differ diff --git a/public/fonts/fa-solid-900.woff b/public/fonts/fa-solid-900.woff new file mode 100644 index 00000000..beec7917 Binary files /dev/null and b/public/fonts/fa-solid-900.woff differ diff --git a/public/fonts/fa-solid-900.woff2 b/public/fonts/fa-solid-900.woff2 new file mode 100644 index 00000000..978a681a Binary files /dev/null and b/public/fonts/fa-solid-900.woff2 differ diff --git a/tests/event_topic_catalog.rs b/tests/event_topic_catalog.rs index 42b15406..a06d6f2f 100644 --- a/tests/event_topic_catalog.rs +++ b/tests/event_topic_catalog.rs @@ -9,7 +9,7 @@ //! If any test in this file fails, the corresponding row in //! `docs/EVENT_TOPICS.md` must be updated to reflect the new topic string. -use soroban_sdk::{Env, Symbol, Vec as SorobanVec}; +use soroban_sdk::{Env, Symbol}; // --------------------------------------------------------------------------- // Vault contract topics