1use axum::{extract::State, http::StatusCode};
4use hypercall_engine::command::{
5 AccruePmSettlementInterestCommand, EngineCommand, JournalPmRecoveryPlanCommand,
6 MarkPmRecoveryActionSubmittedCommand, PmRecoveryActionResult, PmRecoveryExternalKind,
7 PmRecoveryPlanCommand, ResolvePmRecoveryActionCommand, SetPmSettlementPoolConfigCommand,
8};
9use hypercall_margin::portfolio::PmSettlementPoolConfig;
10use hypercall_runtime_api::{error::ApiError, sonic_json::SonicJson};
11use hypercall_types::{api_models::ApiResponse, WalletAddress};
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14
15use crate::state::{AdminState, ENGINE_RESPONSE_TIMEOUT};
16
17#[derive(Debug, Deserialize)]
18pub struct SetPmSettlementPoolConfigRequest {
19 pub request_id: uuid::Uuid,
20 pub input_digest: String,
21 pub underlying: String,
22 pub config_version: u32,
23 pub config: PmSettlementPoolConfig,
24 pub timestamp_ms: u64,
25}
26
27#[derive(Debug, Deserialize)]
28pub struct AccruePmSettlementInterestRequest {
29 pub request_id: uuid::Uuid,
30 pub input_digest: String,
31 pub wallet: WalletAddress,
32 pub underlying: String,
33 pub to_ms: i64,
34 pub timestamp_ms: u64,
35}
36
37#[derive(Debug, Deserialize)]
38pub struct JournalPmRecoveryPlanRequest {
39 pub request_id: uuid::Uuid,
40 pub input_digest: String,
41 pub plan: PmRecoveryPlanCommand,
42 pub timestamp_ms: u64,
43}
44
45#[derive(Debug, Deserialize)]
46pub struct MarkPmRecoveryActionSubmittedRequest {
47 pub request_id: uuid::Uuid,
48 pub input_digest: String,
49 pub wallet: WalletAddress,
50 pub plan_id: String,
51 pub action_index: u32,
52 pub attempt: u32,
53 pub external_id: String,
54 pub external_kind: PmRecoveryExternalKind,
55 pub timestamp_ms: u64,
56}
57
58#[derive(Debug, Deserialize)]
59pub struct ResolvePmRecoveryActionRequest {
60 pub request_id: uuid::Uuid,
61 pub input_digest: String,
62 pub wallet: WalletAddress,
63 pub plan_id: String,
64 pub action_index: u32,
65 pub attempt: u32,
66 pub result: PmRecoveryActionResult,
67 pub recovered_usdc: Decimal,
68 pub liability_reduction_usdc: Decimal,
69 pub result_external_id: Option<String>,
70 pub timestamp_ms: u64,
71}
72
73#[derive(Debug, Serialize)]
74pub struct PmRecoveryProjectionResponse {
75 pub plans: Vec<hypercall_db::PmRecoveryPlanProjection>,
76 pub actions: Vec<hypercall_db::PmRecoveryActionProjection>,
77}
78
79#[derive(Debug, Serialize)]
80pub struct PmSettlementGateStateResponse {
81 pub portfolio_margin_pool_enabled: bool,
82 pub allowlist_count: usize,
83 pub allowlist_wallets: Vec<WalletAddress>,
84 pub pools: Vec<PmSettlementGatePoolState>,
85 pub account_blockers: PmSettlementAccountBlockers,
86 pub recovery_actions: PmSettlementRecoveryActionGateState,
87 pub projection_freshness: PmSettlementProjectionFreshness,
88 pub expansion_ready: PmSettlementExpansionReadiness,
89}
90
91#[derive(Debug, Serialize)]
92pub struct PmSettlementGatePoolState {
93 pub underlying: String,
94 pub pool_available_usdc: Decimal,
95 pub pool_target_usdc: Decimal,
96 pub pool_capacity_usdc: Decimal,
97 pub pool_utilization: Option<Decimal>,
98 pub active_timing_bridge_usdc: Decimal,
99 pub active_settlement_debt_usdc: Decimal,
100 pub normal_utilization_cap: Option<Decimal>,
101 pub crisis_utilization_cap: Option<Decimal>,
102 pub below_target: bool,
103 pub utilization_unavailable: bool,
104 pub above_crisis_cap: bool,
105 pub projection_seq: i64,
106 pub last_engine_command_id: i64,
107}
108
109#[derive(Debug, Default, Serialize)]
110pub struct PmSettlementAccountBlockers {
111 pub total_accounts: usize,
112 pub bridged_accounts: usize,
113 pub debt_accounts: usize,
114 pub overdue_bridge_accounts: usize,
115 pub active_recovery_accounts: usize,
116 pub active_bridge_usdc: Decimal,
117 pub active_debt_usdc: Decimal,
118}
119
120#[derive(Debug, Default, Serialize)]
121pub struct PmSettlementRecoveryActionGateState {
122 pub total_actions: usize,
123 pub planned_actions: usize,
124 pub submitted_actions: usize,
125 pub terminal_actions: usize,
126}
127
128#[derive(Debug, Default, Serialize)]
129pub struct PmSettlementProjectionFreshness {
130 pub projection_rows: usize,
131 pub max_projection_seq: i64,
132 pub max_engine_command_id: i64,
133}
134
135#[derive(Debug, Serialize)]
136pub struct PmSettlementExpansionReadiness {
137 pub allowlist_configured: bool,
138 pub pool_facts_available: bool,
139 pub projections_present: bool,
140 pub no_pool_below_target: bool,
141 pub no_pool_above_crisis_cap: bool,
142 pub no_account_debt: bool,
143 pub no_active_recovery_accounts: bool,
144 pub no_overdue_bridge: bool,
145 pub no_planned_recovery_actions: bool,
146 pub no_submitted_recovery_actions: bool,
147 pub ready_for_single_wallet_smoke: bool,
148 pub ready_for_allowlist_expansion: bool,
149}
150
151fn ensure_pm_settlement_pool_enabled(state: &AdminState) -> Result<(), ApiError> {
152 if state.runtime_config.portfolio_margin_pool_enabled {
153 Ok(())
154 } else {
155 Err(ApiError::new(
156 StatusCode::SERVICE_UNAVAILABLE,
157 "portfolio_margin_pool_disabled",
158 "portfolio_margin_pool_enabled is false; PM settlement pool mutations are disabled",
159 ))
160 }
161}
162
163fn ensure_pm_settlement_wallet_allowed(
164 state: &AdminState,
165 wallet: &WalletAddress,
166) -> Result<(), ApiError> {
167 if state
168 .runtime_config
169 .portfolio_margin_settlement_allowlist
170 .contains(wallet)
171 {
172 Ok(())
173 } else {
174 Err(ApiError::new(
175 StatusCode::FORBIDDEN,
176 "portfolio_margin_wallet_not_allowlisted",
177 "wallet is not allowlisted for PM settlement pool recovery commands",
178 ))
179 }
180}
181
182async fn submit_pm_settlement_command(
183 state: &AdminState,
184 command: EngineCommand,
185) -> Result<(), ApiError> {
186 let sender = state
187 .pm_settlement_admin_sender
188 .as_ref()
189 .ok_or_else(|| ApiError::internal_error("PM settlement admin channel is not configured"))?;
190 let (tx, rx) = tokio::sync::oneshot::channel();
191 sender
192 .send(hypercall_runtime_api::PmSettlementAdminRequest {
193 command,
194 applied_tx: tx,
195 })
196 .await
197 .map_err(|_| ApiError::internal_error("PM settlement admin channel is closed"))?;
198 tokio::time::timeout(ENGINE_RESPONSE_TIMEOUT, rx)
199 .await
200 .map_err(|_| ApiError::gateway_timeout("PM settlement command timed out"))?
201 .map_err(|_| ApiError::internal_error("PM settlement command acknowledgement dropped"))?
202 .map_err(ApiError::bad_request)
203}
204
205pub async fn set_pm_settlement_pool_config(
206 State(state): State<AdminState>,
207 SonicJson(request): SonicJson<SetPmSettlementPoolConfigRequest>,
208) -> Result<SonicJson<ApiResponse<String>>, ApiError> {
209 ensure_pm_settlement_pool_enabled(&state)?;
210 submit_pm_settlement_command(
211 &state,
212 EngineCommand::SetPmSettlementPoolConfig(SetPmSettlementPoolConfigCommand {
213 request_id: request.request_id,
214 input_digest: request.input_digest,
215 underlying: request.underlying,
216 config_version: request.config_version,
217 config: request.config,
218 timestamp_ms: request.timestamp_ms,
219 }),
220 )
221 .await?;
222 Ok(SonicJson(ApiResponse::success(
223 "PM settlement pool config command accepted".to_string(),
224 )))
225}
226
227pub async fn accrue_pm_settlement_interest(
228 State(state): State<AdminState>,
229 SonicJson(request): SonicJson<AccruePmSettlementInterestRequest>,
230) -> Result<SonicJson<ApiResponse<String>>, ApiError> {
231 ensure_pm_settlement_pool_enabled(&state)?;
232 ensure_pm_settlement_wallet_allowed(&state, &request.wallet)?;
233 submit_pm_settlement_command(
234 &state,
235 EngineCommand::AccruePmSettlementInterest(AccruePmSettlementInterestCommand {
236 request_id: request.request_id,
237 input_digest: request.input_digest,
238 wallet: request.wallet,
239 underlying: request.underlying,
240 to_ms: request.to_ms,
241 timestamp_ms: request.timestamp_ms,
242 }),
243 )
244 .await?;
245 Ok(SonicJson(ApiResponse::success(
246 "PM settlement interest accrual command accepted".to_string(),
247 )))
248}
249
250pub async fn journal_pm_recovery_plan(
251 State(state): State<AdminState>,
252 SonicJson(request): SonicJson<JournalPmRecoveryPlanRequest>,
253) -> Result<SonicJson<ApiResponse<String>>, ApiError> {
254 ensure_pm_settlement_pool_enabled(&state)?;
255 ensure_pm_settlement_wallet_allowed(&state, &request.plan.wallet)?;
256 submit_pm_settlement_command(
257 &state,
258 EngineCommand::JournalPmRecoveryPlan(JournalPmRecoveryPlanCommand {
259 request_id: request.request_id,
260 input_digest: request.input_digest,
261 plan: request.plan,
262 timestamp_ms: request.timestamp_ms,
263 }),
264 )
265 .await?;
266 Ok(SonicJson(ApiResponse::success(
267 "PM recovery plan journal command accepted".to_string(),
268 )))
269}
270
271pub async fn mark_pm_recovery_action_submitted(
272 State(state): State<AdminState>,
273 SonicJson(request): SonicJson<MarkPmRecoveryActionSubmittedRequest>,
274) -> Result<SonicJson<ApiResponse<String>>, ApiError> {
275 ensure_pm_settlement_pool_enabled(&state)?;
276 ensure_pm_settlement_wallet_allowed(&state, &request.wallet)?;
277 submit_pm_settlement_command(
278 &state,
279 EngineCommand::MarkPmRecoveryActionSubmitted(MarkPmRecoveryActionSubmittedCommand {
280 request_id: request.request_id,
281 input_digest: request.input_digest,
282 wallet: request.wallet,
283 plan_id: request.plan_id,
284 action_index: request.action_index,
285 attempt: request.attempt,
286 external_id: request.external_id,
287 external_kind: request.external_kind,
288 timestamp_ms: request.timestamp_ms,
289 }),
290 )
291 .await?;
292 Ok(SonicJson(ApiResponse::success(
293 "PM recovery action submission command accepted".to_string(),
294 )))
295}
296
297pub async fn resolve_pm_recovery_action(
298 State(state): State<AdminState>,
299 SonicJson(request): SonicJson<ResolvePmRecoveryActionRequest>,
300) -> Result<SonicJson<ApiResponse<String>>, ApiError> {
301 ensure_pm_settlement_pool_enabled(&state)?;
302 ensure_pm_settlement_wallet_allowed(&state, &request.wallet)?;
303 submit_pm_settlement_command(
304 &state,
305 EngineCommand::ResolvePmRecoveryAction(ResolvePmRecoveryActionCommand {
306 request_id: request.request_id,
307 input_digest: request.input_digest,
308 wallet: request.wallet,
309 plan_id: request.plan_id,
310 action_index: request.action_index,
311 attempt: request.attempt,
312 result: request.result,
313 recovered_usdc: request.recovered_usdc,
314 liability_reduction_usdc: request.liability_reduction_usdc,
315 result_external_id: request.result_external_id,
316 timestamp_ms: request.timestamp_ms,
317 }),
318 )
319 .await?;
320 Ok(SonicJson(ApiResponse::success(
321 "PM recovery action resolution command accepted".to_string(),
322 )))
323}
324
325pub async fn get_pm_settlement_pools(
326 State(state): State<AdminState>,
327) -> Result<SonicJson<ApiResponse<Vec<hypercall_db::PmSettlementPoolProjection>>>, ApiError> {
328 ensure_pm_settlement_pool_enabled(&state)?;
329 let rows = state.db.list_pm_settlement_pools().await.map_err(|error| {
330 tracing::error!("Failed to read PM settlement pool projections: {}", error);
331 ApiError::internal_error("Failed to read PM settlement pool projections")
332 })?;
333 Ok(SonicJson(ApiResponse::success(rows)))
334}
335
336pub async fn get_pm_settlement_accounts(
337 State(state): State<AdminState>,
338) -> Result<SonicJson<ApiResponse<Vec<hypercall_db::PmSettlementAccountProjection>>>, ApiError> {
339 ensure_pm_settlement_pool_enabled(&state)?;
340 let rows = state
341 .db
342 .list_pm_settlement_accounts()
343 .await
344 .map_err(|error| {
345 tracing::error!(
346 "Failed to read PM settlement account projections: {}",
347 error
348 );
349 ApiError::internal_error("Failed to read PM settlement account projections")
350 })?;
351 Ok(SonicJson(ApiResponse::success(rows)))
352}
353
354pub async fn get_pm_settlement_events(
355 State(state): State<AdminState>,
356) -> Result<SonicJson<ApiResponse<Vec<hypercall_db::PmSettlementEventProjection>>>, ApiError> {
357 ensure_pm_settlement_pool_enabled(&state)?;
358 let rows = state
359 .db
360 .list_pm_settlement_events()
361 .await
362 .map_err(|error| {
363 tracing::error!("Failed to read PM settlement event projections: {}", error);
364 ApiError::internal_error("Failed to read PM settlement event projections")
365 })?;
366 Ok(SonicJson(ApiResponse::success(rows)))
367}
368
369pub async fn get_pm_settlement_interest_events(
370 State(state): State<AdminState>,
371) -> Result<SonicJson<ApiResponse<Vec<hypercall_db::PmSettlementInterestEventProjection>>>, ApiError>
372{
373 ensure_pm_settlement_pool_enabled(&state)?;
374 let rows = state
375 .db
376 .list_pm_settlement_interest_events()
377 .await
378 .map_err(|error| {
379 tracing::error!(
380 "Failed to read PM settlement interest event projections: {}",
381 error
382 );
383 ApiError::internal_error("Failed to read PM settlement interest event projections")
384 })?;
385 Ok(SonicJson(ApiResponse::success(rows)))
386}
387
388pub async fn get_pm_settlement_repayment_events(
389 State(state): State<AdminState>,
390) -> Result<SonicJson<ApiResponse<Vec<hypercall_db::PmSettlementRepaymentEventProjection>>>, ApiError>
391{
392 ensure_pm_settlement_pool_enabled(&state)?;
393 let rows = state
394 .db
395 .list_pm_settlement_repayment_events()
396 .await
397 .map_err(|error| {
398 tracing::error!(
399 "Failed to read PM settlement repayment event projections: {}",
400 error
401 );
402 ApiError::internal_error("Failed to read PM settlement repayment event projections")
403 })?;
404 Ok(SonicJson(ApiResponse::success(rows)))
405}
406
407pub async fn get_pm_recovery_projections(
408 State(state): State<AdminState>,
409) -> Result<SonicJson<ApiResponse<PmRecoveryProjectionResponse>>, ApiError> {
410 ensure_pm_settlement_pool_enabled(&state)?;
411 let plans = state.db.list_pm_recovery_plans().await.map_err(|error| {
412 tracing::error!("Failed to read PM recovery plan projections: {}", error);
413 ApiError::internal_error("Failed to read PM recovery plan projections")
414 })?;
415 let actions = state.db.list_pm_recovery_actions().await.map_err(|error| {
416 tracing::error!("Failed to read PM recovery action projections: {}", error);
417 ApiError::internal_error("Failed to read PM recovery action projections")
418 })?;
419 Ok(SonicJson(ApiResponse::success(
420 PmRecoveryProjectionResponse { plans, actions },
421 )))
422}
423
424pub async fn get_pm_settlement_gate_state(
425 State(state): State<AdminState>,
426) -> Result<SonicJson<ApiResponse<PmSettlementGateStateResponse>>, ApiError> {
427 ensure_pm_settlement_pool_enabled(&state)?;
428
429 let pools = state.db.list_pm_settlement_pools().await.map_err(|error| {
430 tracing::error!(
431 "Failed to read PM settlement pool projections for gate state: {}",
432 error
433 );
434 ApiError::internal_error("Failed to read PM settlement pool projections")
435 })?;
436 let pool_gate_counts = state
437 .db
438 .pm_settlement_pool_gate_counts()
439 .await
440 .map_err(|error| {
441 tracing::error!("Failed to read PM settlement pool gate counts: {}", error);
442 ApiError::internal_error("Failed to read PM settlement pool gate counts")
443 })?;
444 let accounts = state
445 .db
446 .list_pm_settlement_accounts()
447 .await
448 .map_err(|error| {
449 tracing::error!(
450 "Failed to read PM settlement account projections for gate state: {}",
451 error
452 );
453 ApiError::internal_error("Failed to read PM settlement account projections")
454 })?;
455 let recovery_plans = state.db.list_pm_recovery_plans().await.map_err(|error| {
456 tracing::error!(
457 "Failed to read PM recovery plan projections for gate state: {}",
458 error
459 );
460 ApiError::internal_error("Failed to read PM recovery plan projections")
461 })?;
462 let recovery_action_rows = state.db.list_pm_recovery_actions().await.map_err(|error| {
463 tracing::error!(
464 "Failed to read PM recovery action projections for gate state: {}",
465 error
466 );
467 ApiError::internal_error("Failed to read PM recovery action projections")
468 })?;
469 let now_ms = chrono::Utc::now().timestamp_millis();
470 let account_gate_counts = state
471 .db
472 .pm_settlement_account_gate_counts(now_ms)
473 .await
474 .map_err(|error| {
475 tracing::error!(
476 "Failed to read PM settlement account gate counts: {}",
477 error
478 );
479 ApiError::internal_error("Failed to read PM settlement account gate counts")
480 })?;
481 let recovery_action_gate_counts =
482 state
483 .db
484 .pm_recovery_action_gate_counts()
485 .await
486 .map_err(|error| {
487 tracing::error!("Failed to read PM recovery action gate counts: {}", error);
488 ApiError::internal_error("Failed to read PM recovery action gate counts")
489 })?;
490
491 let gate_pools = pools
492 .iter()
493 .map(|pool| {
494 let below_target = pool.pool_available_usdc < pool.pool_target_usdc;
495 let utilization_unavailable = pool.pool_utilization.is_none();
496 let above_crisis_cap = match (pool.pool_utilization, pool.crisis_utilization_cap) {
497 (Some(utilization), Some(cap)) => utilization > cap,
498 _ => false,
499 };
500 PmSettlementGatePoolState {
501 underlying: pool.underlying.clone(),
502 pool_available_usdc: pool.pool_available_usdc,
503 pool_target_usdc: pool.pool_target_usdc,
504 pool_capacity_usdc: pool.pool_capacity_usdc,
505 pool_utilization: pool.pool_utilization,
506 active_timing_bridge_usdc: pool.active_timing_bridge_usdc,
507 active_settlement_debt_usdc: pool.active_settlement_debt_usdc,
508 normal_utilization_cap: pool.normal_utilization_cap,
509 crisis_utilization_cap: pool.crisis_utilization_cap,
510 below_target,
511 utilization_unavailable,
512 above_crisis_cap,
513 projection_seq: pool.projection_seq,
514 last_engine_command_id: pool.last_engine_command_id,
515 }
516 })
517 .collect::<Vec<_>>();
518
519 let account_blockers = pm_settlement_account_blockers(account_gate_counts);
520 let recovery_actions = pm_settlement_recovery_action_gate_state(recovery_action_gate_counts);
521 let projection_freshness = pm_settlement_projection_freshness(
522 &pools,
523 &accounts,
524 &recovery_plans,
525 &recovery_action_rows,
526 );
527 let pool_facts_available = pool_gate_counts.total_pools > 0
528 && pool_gate_counts.missing_utilization_pools == 0
529 && pool_gate_counts.missing_crisis_cap_pools == 0;
530 let no_pool_below_target = pool_gate_counts.below_target_pools == 0;
531 let no_pool_above_crisis_cap = pool_gate_counts.above_crisis_cap_pools == 0;
532 let no_account_debt = account_blockers.debt_accounts == 0;
533 let no_active_recovery_accounts = account_blockers.active_recovery_accounts == 0;
534 let no_overdue_bridge = account_blockers.overdue_bridge_accounts == 0;
535 let no_planned_recovery_actions = recovery_actions.planned_actions == 0;
536 let no_submitted_recovery_actions = recovery_actions.submitted_actions == 0;
537 let allowlist_configured = !state
538 .runtime_config
539 .portfolio_margin_settlement_allowlist
540 .is_empty();
541 let projections_present = projection_freshness.projection_rows > 0;
542 let ready_for_single_wallet_smoke =
543 allowlist_configured && pool_facts_available && projections_present;
544 let ready_for_allowlist_expansion = ready_for_single_wallet_smoke
545 && no_pool_below_target
546 && no_pool_above_crisis_cap
547 && no_account_debt
548 && no_active_recovery_accounts
549 && no_overdue_bridge
550 && no_planned_recovery_actions
551 && no_submitted_recovery_actions;
552
553 Ok(SonicJson(ApiResponse::success(
554 PmSettlementGateStateResponse {
555 portfolio_margin_pool_enabled: state.runtime_config.portfolio_margin_pool_enabled,
556 allowlist_count: state
557 .runtime_config
558 .portfolio_margin_settlement_allowlist
559 .len(),
560 allowlist_wallets: state
561 .runtime_config
562 .portfolio_margin_settlement_allowlist
563 .clone(),
564 pools: gate_pools,
565 account_blockers,
566 recovery_actions,
567 projection_freshness,
568 expansion_ready: PmSettlementExpansionReadiness {
569 allowlist_configured,
570 pool_facts_available,
571 projections_present,
572 no_pool_below_target,
573 no_pool_above_crisis_cap,
574 no_account_debt,
575 no_active_recovery_accounts,
576 no_overdue_bridge,
577 no_planned_recovery_actions,
578 no_submitted_recovery_actions,
579 ready_for_single_wallet_smoke,
580 ready_for_allowlist_expansion,
581 },
582 },
583 )))
584}
585
586fn count_to_usize(count: i64) -> usize {
587 usize::try_from(count).expect("Postgres COUNT returned an invalid count")
588}
589
590fn pm_settlement_account_blockers(
591 counts: hypercall_db::PmSettlementAccountGateCounts,
592) -> PmSettlementAccountBlockers {
593 PmSettlementAccountBlockers {
594 total_accounts: count_to_usize(counts.total_accounts),
595 bridged_accounts: count_to_usize(counts.bridged_accounts),
596 debt_accounts: count_to_usize(counts.debt_accounts),
597 overdue_bridge_accounts: count_to_usize(counts.overdue_bridge_accounts),
598 active_recovery_accounts: count_to_usize(counts.active_recovery_accounts),
599 active_bridge_usdc: counts.active_bridge_usdc,
600 active_debt_usdc: counts.active_debt_usdc,
601 }
602}
603
604fn pm_settlement_recovery_action_gate_state(
605 counts: hypercall_db::PmRecoveryActionGateCounts,
606) -> PmSettlementRecoveryActionGateState {
607 PmSettlementRecoveryActionGateState {
608 total_actions: count_to_usize(counts.total_actions),
609 planned_actions: count_to_usize(counts.planned_actions),
610 submitted_actions: count_to_usize(counts.submitted_actions),
611 terminal_actions: count_to_usize(counts.terminal_actions),
612 }
613}
614
615fn pm_settlement_projection_freshness(
616 pools: &[hypercall_db::PmSettlementPoolProjection],
617 accounts: &[hypercall_db::PmSettlementAccountProjection],
618 plans: &[hypercall_db::PmRecoveryPlanProjection],
619 actions: &[hypercall_db::PmRecoveryActionProjection],
620) -> PmSettlementProjectionFreshness {
621 let projection_rows = pools.len() + accounts.len() + plans.len() + actions.len();
622 let max_projection_seq = pools
623 .iter()
624 .map(|row| row.projection_seq)
625 .chain(accounts.iter().map(|row| row.projection_seq))
626 .chain(plans.iter().map(|row| row.projection_seq))
627 .chain(actions.iter().map(|row| row.projection_seq))
628 .max()
629 .unwrap_or_default();
630 let max_engine_command_id = pools
631 .iter()
632 .map(|row| row.last_engine_command_id)
633 .chain(accounts.iter().map(|row| row.last_engine_command_id))
634 .chain(plans.iter().map(|row| row.engine_command_id))
635 .chain(actions.iter().map(|row| row.engine_command_id))
636 .max()
637 .unwrap_or_default();
638
639 PmSettlementProjectionFreshness {
640 projection_rows,
641 max_projection_seq,
642 max_engine_command_id,
643 }
644}