Skip to main content

rustev_runtime/
spend.rs

1//! Synchronous, host-supplied accounting for live attempts (spec 043).
2
3use std::any::Any;
4
5use rustev_contract::{
6    definition::Operation,
7    ids::ArtifactId,
8    run::{Charge, CostBound},
9};
10
11/// Host declaration of the binding's cost units.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum SpendUnit {
14    NanoUsd,
15    None,
16}
17
18/// An exact binding and its host-declared cost unit.
19#[derive(Debug, Clone)]
20pub struct SpendBinding {
21    pub backend_id: String,
22    pub artifact: ArtifactId,
23    pub cost_unit: SpendUnit,
24}
25
26#[derive(Debug, Clone)]
27pub struct SpendAttempt {
28    pub decision_id: String,
29    pub attempt_id: String,
30    pub backend_id: String,
31    pub target: u32,
32    pub artifact: ArtifactId,
33    pub operation: Operation,
34    pub bound: CostBound,
35    pub cost_unit: SpendUnit,
36}
37
38/// In-process accounting state, consumed exactly once by its gate.
39/// This is never authorization for an action executor.
40pub type SpendTicket = Box<dyn Any + Send>;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct SpendRefusal {
44    pub code: String,
45}
46
47impl SpendRefusal {
48    pub fn new(code: impl Into<String>) -> Self {
49        Self { code: code.into() }
50    }
51}
52
53/// A reservation must be durable before returning success. Settlement
54/// failures must retain liability. Implementations must not panic.
55pub trait SpendGate: Send + Sync {
56    fn reserve(&self, attempt: &SpendAttempt) -> Result<SpendTicket, SpendRefusal>;
57    /// Source costs are monetary units. Older gates refuse source attempts.
58    fn reserve_acquisition(
59        &self,
60        _attempt: &AcquisitionSpendAttempt,
61    ) -> Result<SpendTicket, SpendRefusal> {
62        Err(SpendRefusal::new("acquisition-unsupported"))
63    }
64    fn settle(&self, ticket: SpendTicket, charge: &Charge);
65    fn release(&self, ticket: SpendTicket);
66}
67
68/// Gate and explicit binding declarations for one live decision.
69#[derive(Clone, Copy)]
70pub struct LiveSpend<'a> {
71    pub gate: &'a dyn SpendGate,
72    pub bindings: &'a [SpendBinding],
73}
74
75/// Source posture and retained pages for the source-aware live entry point.
76#[derive(Clone, Copy, Default)]
77pub struct LiveSources<'a> {
78    pub acquisitions: &'a [LiveAcquisition],
79    pub pages: &'a [rustev_contract::acquisition::AcquisitionPage],
80}
81
82/// Explicit source posture for a gated decision. Identities are descriptor ids.
83#[derive(Debug, Clone)]
84pub struct LiveAcquisition {
85    pub source: rustev_contract::ids::SourceId,
86    pub posture: AcquisitionPosture,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum AcquisitionPosture {
91    Captured,
92    Live,
93}
94
95/// Source attempts have their own identity, never a fabricated backend artifact.
96#[derive(Debug, Clone)]
97pub struct AcquisitionSpendAttempt {
98    pub decision_id: String,
99    pub attempt_id: String,
100    pub source: rustev_contract::ids::SourceId,
101    pub operation: String,
102    pub request: rustev_contract::ids::AcquisitionRequestId,
103    pub bound: CostBound,
104}
105
106pub(crate) struct HeldSpend<'a> {
107    gate: &'a dyn SpendGate,
108    ticket: Option<SpendTicket>,
109}
110
111impl<'a> HeldSpend<'a> {
112    pub(crate) fn new(gate: &'a dyn SpendGate, ticket: SpendTicket) -> Self {
113        Self {
114            gate,
115            ticket: Some(ticket),
116        }
117    }
118
119    pub(crate) fn release(mut self) {
120        self.gate.release(self.ticket.take().expect("held ticket"));
121    }
122
123    pub(crate) fn settle(mut self, charge: &Charge) {
124        self.gate
125            .settle(self.ticket.take().expect("held ticket"), charge);
126    }
127}
128
129impl Drop for HeldSpend<'_> {
130    fn drop(&mut self) {
131        if let Some(ticket) = self.ticket.take() {
132            self.gate.settle(ticket, &Charge::Unknown);
133        }
134    }
135}