Skip to main content

acta_maker_sdk/types/
ids.rs

1use std::fmt::Display;
2
3use serde::{Deserialize, Serialize};
4
5define_bytes32_newtype!(OrderId);
6
7define_numeric_newtype!(
8    /// Strike price, 1e9 fixed-point per one unit of the underlying
9    /// ([`PRICE_SCALE`](crate::PRICE_SCALE)), independent of mint decimals.
10    Strike,
11    u64
12);
13define_numeric_newtype!(
14    /// Gross premium per one unit of the underlying, 1e9 fixed-point
15    /// ([`PRICE_SCALE`](crate::PRICE_SCALE)), independent of mint decimals.
16    Price,
17    u64
18);
19define_numeric_newtype!(
20    /// Order size in the underlying mint's atomic units.
21    Quantity,
22    u64
23);
24
25impl Price {
26    /// Subtract a floor-rounded per-unit fee, saturating at zero.
27    #[must_use]
28    pub fn after_fee_bps(self, bps: u16) -> Self {
29        let fee = u128::from(self.0) * u128::from(bps) / 10_000;
30        match u64::try_from(fee) {
31            Ok(fee) => Self::new(self.0.saturating_sub(fee)),
32            Err(_) => Self::new(0),
33        }
34    }
35}
36
37define_numeric_newtype!(Nonce, u64);
38define_numeric_newtype!(RfqVersion, u64);
39define_numeric_newtype!(OrderVersion, u64);
40
41impl OrderVersion {
42    /// Order exists and has been accepted/locked by the taker.
43    pub const ACCEPTED: Self = Self::new(1);
44    /// The signed transaction has been submitted for execution.
45    pub const SUBMITTED: Self = Self::new(2);
46    /// A locally final failure or expiry. A later chain confirmation may supersede failure.
47    pub const FAILED: Self = Self::new(3);
48    /// An authoritative on-chain confirmation.
49    pub const CONFIRMED: Self = Self::new(4);
50    /// The server has no authoritative record for the requested order ID.
51    pub const NOT_FOUND: Self = Self::new(0);
52}
53define_numeric_newtype!(Slot, u64);
54define_numeric_newtype!(ChainId, u64);
55define_numeric_newtype!(DurationSeconds, u64);
56define_numeric_newtype!(Volume, u64);
57define_numeric_newtype!(Balance, u64);
58
59define_numeric_newtype!(QuoteCount, u32);
60define_numeric_newtype!(TradeCount, u32);
61define_numeric_newtype!(TimeoutSeconds, u32);
62
63#[derive(
64    Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
65)]
66pub struct Decimals(pub u8);
67
68impl Decimals {
69    #[must_use]
70    pub const fn new(value: u8) -> Self {
71        Self(value)
72    }
73
74    #[must_use]
75    pub const fn value(self) -> u8 {
76        self.0
77    }
78}
79
80impl Display for Decimals {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        write!(f, "{}", self.0)
83    }
84}
85
86impl From<u8> for Decimals {
87    fn from(value: u8) -> Self {
88        Self(value)
89    }
90}
91
92impl From<Decimals> for u8 {
93    fn from(value: Decimals) -> Self {
94        value.0
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::OrderVersion;
101
102    #[test]
103    fn order_version_ranks_match_wire_lifecycle_precedence() {
104        assert_eq!(OrderVersion::NOT_FOUND.value(), 0);
105        assert!(OrderVersion::ACCEPTED < OrderVersion::SUBMITTED);
106        assert!(OrderVersion::SUBMITTED < OrderVersion::FAILED);
107        assert!(OrderVersion::FAILED < OrderVersion::CONFIRMED);
108    }
109}
110
111/// An address-valued identity with a cached canonical representation.
112macro_rules! define_address_id {
113    ($name:ident) => {
114        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
115        #[serde(from = "crate::Address32", into = "crate::Address32")]
116        pub struct $name {
117            address: crate::Address32,
118            base58: String,
119        }
120        impl $name {
121            #[must_use]
122            pub fn new(address: crate::Address32) -> Self {
123                Self {
124                    address,
125                    base58: address.to_base58(),
126                }
127            }
128            #[must_use]
129            pub const fn address(&self) -> crate::Address32 {
130                self.address
131            }
132            #[must_use]
133            pub fn as_str(&self) -> &str {
134                &self.base58
135            }
136        }
137        impl From<crate::Address32> for $name {
138            fn from(value: crate::Address32) -> Self {
139                Self::new(value)
140            }
141        }
142        impl From<$name> for crate::Address32 {
143            fn from(value: $name) -> Self {
144                value.address
145            }
146        }
147        impl AsRef<str> for $name {
148            fn as_ref(&self) -> &str {
149                self.as_str()
150            }
151        }
152        impl std::fmt::Display for $name {
153            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154                f.write_str(self.as_str())
155            }
156        }
157        impl std::ops::Deref for $name {
158            type Target = str;
159            fn deref(&self) -> &Self::Target {
160                self.as_str()
161            }
162        }
163    };
164}
165
166define_address_id!(MarketId);
167define_address_id!(UserId);
168
169#[derive(
170    Debug,
171    Clone,
172    Copy,
173    PartialEq,
174    Eq,
175    Hash,
176    Serialize,
177    Deserialize,
178    strum::IntoStaticStr,
179    strum::EnumString,
180)]
181#[serde(rename_all = "snake_case")]
182#[strum(serialize_all = "snake_case")]
183pub enum PositionType {
184    CoveredCall,
185    CashSecuredPut,
186    CallSpread,
187    PutSpread,
188    CashPut,
189}
190
191impl Display for PositionType {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        f.write_str(self.into())
194    }
195}
196
197impl PositionType {
198    pub const fn is_spread(self) -> bool {
199        matches!(self, Self::CallSpread | Self::PutSpread | Self::CashPut)
200    }
201    pub const fn is_put(self) -> bool {
202        matches!(self, Self::CashSecuredPut | Self::PutSpread | Self::CashPut)
203    }
204}
205
206/// The complete, hash-bound option terms carried by an RFQ, quote, position, or trade.
207///
208/// The tagged form prevents a physical option from carrying a second strike and
209/// prevents a spread from losing either strike during a wire round-trip.
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
211#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
212pub enum OptionTerms {
213    /// Cash-settled put: wire strike K, execution legs (0, K).
214    CashPut {
215        strike: Strike,
216    },
217    CoveredCall {
218        strike: Strike,
219    },
220    CashSecuredPut {
221        strike: Strike,
222    },
223    CallSpread {
224        lower_strike: Strike,
225        upper_strike: Strike,
226    },
227    PutSpread {
228        lower_strike: Strike,
229        upper_strike: Strike,
230    },
231}
232
233impl OptionTerms {
234    pub const fn is_spread(self) -> bool {
235        self.position_type().is_spread()
236    }
237    /// Replace the physical strike (or spread lower strike), retaining the product and K2.
238    #[must_use]
239    pub const fn with_strike(self, strike: Strike) -> Self {
240        match self {
241            Self::CashPut { .. } => Self::CashPut { strike },
242            Self::CoveredCall { .. } => Self::CoveredCall { strike },
243            Self::CashSecuredPut { .. } => Self::CashSecuredPut { strike },
244            Self::CallSpread { upper_strike, .. } => Self::CallSpread {
245                lower_strike: strike,
246                upper_strike,
247            },
248            Self::PutSpread { upper_strike, .. } => Self::PutSpread {
249                lower_strike: strike,
250                upper_strike,
251            },
252        }
253    }
254
255    #[must_use]
256    pub const fn position_type(self) -> PositionType {
257        match self {
258            Self::CashPut { .. } => PositionType::CashPut,
259            Self::CoveredCall { .. } => PositionType::CoveredCall,
260            Self::CashSecuredPut { .. } => PositionType::CashSecuredPut,
261            Self::CallSpread { .. } => PositionType::CallSpread,
262            Self::PutSpread { .. } => PositionType::PutSpread,
263        }
264    }
265
266    #[must_use]
267    pub const fn strike(self) -> Strike {
268        match self {
269            Self::CashPut { strike } => strike,
270            Self::CoveredCall { strike } | Self::CashSecuredPut { strike } => strike,
271            Self::CallSpread { lower_strike, .. } | Self::PutSpread { lower_strike, .. } => {
272                lower_strike
273            }
274        }
275    }
276
277    #[must_use]
278    pub const fn upper_strike(self) -> Option<Strike> {
279        match self {
280            Self::CashPut { strike } => Some(strike),
281            Self::CoveredCall { .. } | Self::CashSecuredPut { .. } => None,
282            Self::CallSpread { upper_strike, .. } | Self::PutSpread { upper_strike, .. } => {
283                Some(upper_strike)
284            }
285        }
286    }
287
288    /// Convert public economic terms to the contract's four-product representation.
289    #[must_use]
290    pub const fn to_chain(self) -> ChainTerms {
291        match self {
292            Self::CoveredCall { strike } => ChainTerms::CoveredCall { strike },
293            Self::CashSecuredPut { strike } => ChainTerms::CashSecuredPut { strike },
294            Self::CallSpread {
295                lower_strike,
296                upper_strike,
297            } => ChainTerms::CallSpread {
298                lower_strike,
299                upper_strike,
300            },
301            Self::PutSpread {
302                lower_strike,
303                upper_strike,
304            } => ChainTerms::PutSpread {
305                lower_strike,
306                upper_strike,
307            },
308            Self::CashPut { strike } => ChainTerms::PutSpread {
309                lower_strike: Strike(0),
310                upper_strike: strike,
311            },
312        }
313    }
314
315    /// The sole normalization of a zero-lower chain put spread into a cash put.
316    #[must_use]
317    pub const fn from_chain(terms: ChainTerms) -> Self {
318        match terms {
319            ChainTerms::CoveredCall { strike } => Self::CoveredCall { strike },
320            ChainTerms::CashSecuredPut { strike } => Self::CashSecuredPut { strike },
321            ChainTerms::CallSpread {
322                lower_strike,
323                upper_strike,
324            } => Self::CallSpread {
325                lower_strike,
326                upper_strike,
327            },
328            ChainTerms::PutSpread {
329                lower_strike: Strike(0),
330                upper_strike,
331            } => Self::CashPut {
332                strike: upper_strike,
333            },
334            ChainTerms::PutSpread {
335                lower_strike,
336                upper_strike,
337            } => Self::PutSpread {
338                lower_strike,
339                upper_strike,
340            },
341        }
342    }
343
344    pub fn from_chain_parts(
345        product: ChainPositionType,
346        strike: Strike,
347        upper: Option<Strike>,
348    ) -> Result<Self, crate::spreads::SpreadError> {
349        ChainTerms::from_parts(product, strike, upper).map(Self::from_chain)
350    }
351}
352
353/// Contract discriminants only. Public products have no numeric conversion.
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
355#[serde(rename_all = "snake_case")]
356#[repr(u8)]
357pub enum ChainPositionType {
358    CoveredCall = 0,
359    CashSecuredPut = 1,
360    CallSpread = 2,
361    PutSpread = 3,
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
365#[error("invalid chain position type value: {0}")]
366pub struct ChainPositionTypeParseError(pub u8);
367
368impl TryFrom<u8> for ChainPositionType {
369    type Error = ChainPositionTypeParseError;
370    fn try_from(value: u8) -> Result<Self, Self::Error> {
371        match value {
372            0 => Ok(Self::CoveredCall),
373            1 => Ok(Self::CashSecuredPut),
374            2 => Ok(Self::CallSpread),
375            3 => Ok(Self::PutSpread),
376            _ => Err(ChainPositionTypeParseError(value)),
377        }
378    }
379}
380impl From<ChainPositionType> for u8 {
381    fn from(value: ChainPositionType) -> Self {
382        value as Self
383    }
384}
385impl ChainPositionType {
386    #[must_use]
387    pub const fn is_spread(self) -> bool {
388        matches!(self, Self::CallSpread | Self::PutSpread)
389    }
390    #[must_use]
391    pub const fn is_put(self) -> bool {
392        matches!(self, Self::CashSecuredPut | Self::PutSpread)
393    }
394    #[must_use]
395    pub const fn uses_quote_collateral(self) -> bool {
396        !matches!(self, Self::CoveredCall)
397    }
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub enum ChainTerms {
402    CoveredCall {
403        strike: Strike,
404    },
405    CashSecuredPut {
406        strike: Strike,
407    },
408    CallSpread {
409        lower_strike: Strike,
410        upper_strike: Strike,
411    },
412    PutSpread {
413        lower_strike: Strike,
414        upper_strike: Strike,
415    },
416}
417impl ChainTerms {
418    pub fn from_parts(
419        product: ChainPositionType,
420        strike: Strike,
421        upper: Option<Strike>,
422    ) -> Result<Self, crate::spreads::SpreadError> {
423        match (product, upper) {
424            (ChainPositionType::CoveredCall, None) if strike.0 > 0 => {
425                Ok(Self::CoveredCall { strike })
426            }
427            (ChainPositionType::CashSecuredPut, None) if strike.0 > 0 => {
428                Ok(Self::CashSecuredPut { strike })
429            }
430            (ChainPositionType::CallSpread, Some(upper_strike))
431                if strike.0 > 0 && upper_strike > strike =>
432            {
433                Ok(Self::CallSpread {
434                    lower_strike: strike,
435                    upper_strike,
436                })
437            }
438            (ChainPositionType::PutSpread, Some(upper_strike)) if upper_strike > strike => {
439                Ok(Self::PutSpread {
440                    lower_strike: strike,
441                    upper_strike,
442                })
443            }
444            _ => Err(crate::spreads::SpreadError::InvalidStrikes),
445        }
446    }
447    #[must_use]
448    pub const fn position_type(self) -> ChainPositionType {
449        match self {
450            Self::CoveredCall { .. } => ChainPositionType::CoveredCall,
451            Self::CashSecuredPut { .. } => ChainPositionType::CashSecuredPut,
452            Self::CallSpread { .. } => ChainPositionType::CallSpread,
453            Self::PutSpread { .. } => ChainPositionType::PutSpread,
454        }
455    }
456    #[must_use]
457    pub const fn strike(self) -> Strike {
458        match self {
459            Self::CoveredCall { strike } | Self::CashSecuredPut { strike } => strike,
460            Self::CallSpread { lower_strike, .. } | Self::PutSpread { lower_strike, .. } => {
461                lower_strike
462            }
463        }
464    }
465    #[must_use]
466    pub const fn upper_strike(self) -> Option<Strike> {
467        match self {
468            Self::CoveredCall { .. } | Self::CashSecuredPut { .. } => None,
469            Self::CallSpread { upper_strike, .. } | Self::PutSpread { upper_strike, .. } => {
470                Some(upper_strike)
471            }
472        }
473    }
474}
475
476/// Taker direction committed in the signed order preimage.
477///
478/// The venue currently admits only [`Self::Sell`]. `Buy` remains representable
479/// so the wire and preimage layout do not need another migration when economics
480/// for taker buys are added.
481#[derive(
482    Debug,
483    Clone,
484    Copy,
485    PartialEq,
486    Eq,
487    Hash,
488    Serialize,
489    Deserialize,
490    strum::IntoStaticStr,
491    strum::EnumString,
492)]
493#[serde(rename_all = "snake_case")]
494#[strum(serialize_all = "snake_case")]
495pub enum TakerSide {
496    Sell,
497    Buy,
498}
499
500impl TakerSide {
501    #[must_use]
502    pub const fn is_taker_buy(self) -> bool {
503        matches!(self, Self::Buy)
504    }
505}
506/// Last applied lifecycle event in canonical chain order.
507#[derive(
508    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
509)]
510#[serde(deny_unknown_fields)]
511pub struct PositionRevision {
512    pub slot: u64,
513    pub transaction_index: u32,
514    pub instruction_index: u16,
515}