Skip to content

Commit ab85cec

Browse files
Merge pull request #544 from JamesEjembi/james/prs-429-428-427-426
feat: canonical decision payloads, settlement self-containment, version-negotiation endpoint, address auth labels
2 parents bf19a39 + 4ffe7d8 commit ab85cec

5 files changed

Lines changed: 386 additions & 71 deletions

File tree

apexchainx_calculator/src/calculation.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,11 +478,11 @@ fn publish_sla_event(env: &Env, severity: Symbol, result: &SLAResult) {
478478
(
479479
result.outage_id.clone(),
480480
result.status.clone(),
481-
result.payment_type.clone(),
482-
result.rating.clone(),
483481
result.mttr_minutes,
484482
result.threshold_minutes,
485483
result.amount,
484+
result.payment_type.clone(),
485+
result.rating.clone(),
486486
result.config_version_hash,
487487
result.recorded_at,
488488
),
@@ -495,8 +495,11 @@ fn publish_settlement_intent_event(env: &Env, severity: Symbol, result: &SLAResu
495495
(
496496
result.outage_id.clone(),
497497
result.status.clone(),
498-
result.payment_type.clone(),
498+
result.mttr_minutes,
499+
result.threshold_minutes,
499500
result.amount,
501+
result.payment_type.clone(),
502+
result.rating.clone(),
500503
result.config_version_hash,
501504
result.recorded_at,
502505
),

apexchainx_calculator/src/event_schema.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,31 @@
1313
//!
1414
//! # Event Catalog
1515
//!
16+
//! The three decision-carrying events (`sla_calc`, `set_int`, `dup_input`)
17+
//! share a single canonical payload field order — the `SLAResult` struct
18+
//! order — so indexers parse one layout regardless of which decision event
19+
//! they consume. Any divergence between them is a bug (#429). The canonical
20+
//! order is:
21+
//!
22+
//! (outage_id, status, mttr_minutes, threshold_minutes, amount,
23+
//! payment_type, rating, config_version_hash, recorded_at)
24+
//!
1625
//! ## sla_calc (`sla_calc`)
1726
//! Emitted on every successful `calculate_sla` call.
1827
//! - topic[2]: severity Symbol
19-
//! - payload: (outage_id: Symbol, status: Symbol, payment_type: Symbol,
20-
//! rating: Symbol, mttr_minutes: u32, threshold_minutes: u32,
21-
//! amount: i128, config_version_hash: u64, recorded_at: u64)
28+
//! - payload: (outage_id: Symbol, status: Symbol, mttr_minutes: u32,
29+
//! threshold_minutes: u32, amount: i128, payment_type: Symbol,
30+
//! rating: Symbol, config_version_hash: u64, recorded_at: u64)
2231
//!
2332
//! ## set_int (`set_int`)
2433
//! Settlement intent emitted alongside sla_calc for backend reconciliation.
34+
//! Carries the full decision (including `mttr_minutes`, `threshold_minutes`,
35+
//! and `rating`) so a settlement-only consumer can reconstruct the SLA
36+
//! decision without a follow-up read (#428).
2537
//! - topic[2]: severity Symbol
26-
//! - payload: (outage_id: Symbol, status: Symbol, payment_type: Symbol,
27-
//! amount: i128, config_version_hash: u64, recorded_at: u64)
38+
//! - payload: (outage_id: Symbol, status: Symbol, mttr_minutes: u32,
39+
//! threshold_minutes: u32, amount: i128, payment_type: Symbol,
40+
//! rating: Symbol, config_version_hash: u64, recorded_at: u64)
2841
//!
2942
//! ## dup_input (`dup_input`)
3043
//! Emitted when `calculate_sla` rejects a conflicting duplicate `outage_id`

apexchainx_calculator/src/lib.rs

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,16 @@ pub use crate::config_metadata::LAST_CFG_UPDATE_KEY;
285285
//
286286
// ===== Event Payload Schemas =====
287287
//
288-
// sla_calc → (outage_id: Symbol, status: Symbol, payment_type: Symbol,
289-
// rating: Symbol, mttr_minutes: u32, threshold_minutes: u32,
290-
// amount: i128, config_version_hash: u64, recorded_at: u64)
288+
// The three decision-carrying events (sla_calc, set_int, dup_input) share a
289+
// single canonical field order — the SLAResult struct order — so indexers
290+
// parse one layout regardless of which decision event they consume (#429):
291+
//
292+
// decision → (outage_id, status, mttr_minutes, threshold_minutes, amount,
293+
// payment_type, rating, config_version_hash, recorded_at)
294+
//
295+
// sla_calc → (outage_id: Symbol, status: Symbol, mttr_minutes: u32,
296+
// threshold_minutes: u32, amount: i128, payment_type: Symbol,
297+
// rating: Symbol, config_version_hash: u64, recorded_at: u64)
291298
// context: severity Symbol
292299
//
293300
// cfg_upd → (threshold_minutes: u32, penalty_per_minute: i128,
@@ -333,11 +340,9 @@ pub use crate::config_metadata::LAST_CFG_UPDATE_KEY;
333340
// op_can → ()
334341
// context: caller Address
335342
//
336-
// op_sup → (superseded_operator: Address, new_operator: Address)
337-
// context: caller Address
338-
//
339-
// set_int → (outage_id: Symbol, status: Symbol, payment_type: Symbol,
340-
// amount: i128, config_version_hash: u64, recorded_at: u64)
343+
// set_int → (outage_id: Symbol, status: Symbol, mttr_minutes: u32,
344+
// threshold_minutes: u32, amount: i128, payment_type: Symbol,
345+
// rating: Symbol, config_version_hash: u64, recorded_at: u64)
341346
// context: severity Symbol
342347
//
343348
// dup_input → (outage_id: Symbol, status: Symbol, mttr_minutes: u32,
@@ -359,8 +364,13 @@ pub(crate) const EVENT_SLA_CALC: Symbol = symbol_short!("sla_calc");
359364

360365
/// Emitted alongside sla_calc for settlement intent reconciliation.
361366
///
362-
/// Compatibility decision: settlement intent fields are ordered by settlement
363-
/// priority (id, status, payment, amount, hash, timestamp). Field additions
367+
/// Carries the full SLA decision (including `mttr_minutes`,
368+
/// `threshold_minutes`, and `rating`) so a consumer processing only the
369+
/// settlement stream can reconstruct the decision without a follow-up read.
370+
///
371+
/// Compatibility decision: shares the canonical decision field order
372+
/// (`outage_id, status, mttr_minutes, threshold_minutes, amount,
373+
/// payment_type, rating, config_version_hash, recorded_at`). Field additions
364374
/// go at the end; any reorder or removal requires a version bump.
365375
pub(crate) const EVENT_SETTLE_INTENT: Symbol = symbol_short!("set_int");
366376

@@ -854,8 +864,9 @@ pub struct PublicApiMethod {
854864
/// Auth classification. Values:
855865
/// - `"admin"` – caller must hold the admin role.
856866
/// - `"operator"` – caller must hold the operator role.
857-
/// - `"multi"` – two or more parties must authorize (e.g. `initialize`
858-
/// requires BOTH admin and operator signatures) (#425).
867+
/// - `"addr"` – only a specific stored address may call (the pending
868+
/// proposal slot holder must sign, e.g. `accept_admin`/`accept_operator`)
869+
/// (#426).
859870
/// - `"none"` – no authorization gate (read-only / public).
860871
pub auth: Symbol,
861872
/// The primary event name emitted by this method, or `Symbol::new(env, "")` if none.
@@ -2170,8 +2181,9 @@ impl SLACalculatorContract {
21702181
/// Each `PublicApiMethod` contains:
21712182
/// - `name`: the contract method name (e.g. "calculate_sla")
21722183
/// - `mutates`: `true` if the method modifies storage
2173-
/// - `auth`: auth classification — `"admin"`, `"operator"`, `"multi"`
2174-
/// (multiple parties, e.g. `initialize`), or `"none"`.
2184+
/// - `auth`: auth classification — `"admin"`, `"operator"`, `"addr"`
2185+
/// (a specific pending address, e.g. `accept_admin`/`accept_operator`),
2186+
/// or `"none"`.
21752187
/// - `event`: the primary event name emitted, or empty if none
21762188
///
21772189
/// # Errors
@@ -2210,10 +2222,12 @@ impl SLACalculatorContract {
22102222
// All public methods added in alphabetical order for deterministic output.
22112223
// Lifecycle:
22122224
// Note: accept_admin/accept_operator are called by the proposed address
2213-
// (not the current role holder), so auth is "none" — only an address
2214-
// equality check against the pending slot is performed.
2215-
methods.push_back(method("accept_admin", true, "none", "adm_acc"));
2216-
methods.push_back(method("accept_operator", true, "none", "op_acc"));
2225+
// (not the current role holder). They still call `caller.require_auth()`
2226+
// and enforce that the caller equals the pending address, so they are
2227+
// NOT "none" — they are address-scoped ("addr"— the pending slot holder
2228+
// must sign) (#426).
2229+
methods.push_back(method("accept_admin", true, "addr", "adm_acc"));
2230+
methods.push_back(method("accept_operator", true, "addr", "op_acc"));
22172231
// Calculation:
22182232
methods.push_back(method("calculate_sla", true, "operator", "sla_calc"));
22192233
methods.push_back(method("calculate_sla_view", false, "none", ""));
@@ -2256,6 +2270,7 @@ impl SLACalculatorContract {
22562270
methods.push_back(method("get_storage_footprint_estimate", false, "none", ""));
22572271
methods.push_back(method("get_storage_version", false, "none", ""));
22582272
methods.push_back(method("get_version_info", false, "none", ""));
2273+
methods.push_back(method("get_version_negotiation_info", false, "none", ""));
22592274
// Health:
22602275
methods.push_back(method("healthcheck", false, "none", ""));
22612276
// Init:
@@ -3267,28 +3282,38 @@ impl SLACalculatorContract {
32673282
}
32683283

32693284
fn publish_sla_event(env: &Env, severity: Symbol, result: &SLAResult) {
3285+
// Canonical decision field order (#429): shares the SLAResult struct
3286+
// order with set_int and dup_input so indexers parse one layout.
32703287
env.events().publish(
32713288
(EVENT_SLA_CALC, EVENT_VERSION, severity),
32723289
(
32733290
result.outage_id.clone(),
32743291
result.status.clone(),
3275-
result.payment_type.clone(),
3276-
result.rating.clone(),
32773292
result.mttr_minutes,
32783293
result.threshold_minutes,
32793294
result.amount,
3295+
result.payment_type.clone(),
3296+
result.rating.clone(),
3297+
result.config_version_hash,
3298+
result.recorded_at,
32803299
),
32813300
);
32823301
}
32833302

32843303
fn publish_settlement_intent_event(env: &Env, severity: Symbol, result: &SLAResult) {
3304+
// Canonical decision field order (#429) carrying the full decision
3305+
// (mttr_minutes, threshold_minutes, rating) so a settlement-only
3306+
// consumer can reconstruct the SLA decision (#428).
32853307
env.events().publish(
32863308
(EVENT_SETTLE_INTENT, EVENT_VERSION, severity),
32873309
(
32883310
result.outage_id.clone(),
32893311
result.status.clone(),
3290-
result.payment_type.clone(),
3312+
result.mttr_minutes,
3313+
result.threshold_minutes,
32913314
result.amount,
3315+
result.payment_type.clone(),
3316+
result.rating.clone(),
32923317
result.config_version_hash,
32933318
result.recorded_at,
32943319
),
@@ -3755,6 +3780,45 @@ impl SLACalculatorContract {
37553780
})
37563781
}
37573782

3783+
// -------------------------------------------------------------------
3784+
// SC-W5-078 – Version negotiation endpoint for multi-contract handshake
3785+
// -------------------------------------------------------------------
3786+
3787+
/// Returns the `VersionNegotiationInfo` for this contract, exposing the
3788+
/// version-negotiation protocol data (`protocol_version`,
3789+
/// `min_compatible_protocol`, storage version, pause & migration state)
3790+
/// over a live contract method.
3791+
///
3792+
/// This makes the multi-contract handshake documented in
3793+
/// `version_negotiation.rs` and `docs/VERSION_NEGOTIATION_CONTRIBUTOR_GUIDE.md`
3794+
/// actually runnable: a coordinator/backend calls this on each peer to
3795+
/// obtain the `protocol_version`/`min_compatible_protocol` it needs to
3796+
/// feed `negotiate_contract_versions` off-chain (or via a cross-contract
3797+
/// coordinator), instead of the data living only in dead code (#427).
3798+
///
3799+
/// Like `get_version_info`, this intentionally bypasses `check_version`
3800+
/// so it remains callable even in a pre-migration or pre-init state.
3801+
///
3802+
/// # Returns
3803+
/// The `VersionNegotiationInfo` for this contract (empty peers: a
3804+
/// coordinator can then run the negotiation rules against a peer list it
3805+
/// assembles from these responses).
3806+
pub fn get_version_negotiation_info(
3807+
env: Env,
3808+
) -> Result<crate::version_negotiation::VersionNegotiationInfo, SLAError> {
3809+
let stored_version: u32 = env
3810+
.storage()
3811+
.instance()
3812+
.get(&STORAGE_VERSION_KEY)
3813+
.ok_or(SLAError::NotInitialized)?;
3814+
let is_paused: bool = env.storage().instance().get(&PAUSED_KEY).unwrap_or(false);
3815+
Ok(crate::version_negotiation::build_negotiation_info(
3816+
stored_version,
3817+
STORAGE_VERSION,
3818+
is_paused,
3819+
))
3820+
}
3821+
37583822
// -------------------------------------------------------------------
37593823
// #218 – Read-only healthcheck path for backend startup readiness
37603824
// -------------------------------------------------------------------

0 commit comments

Comments
 (0)