Skip to main content

roshi_interface/
oracle.rs

1use wincode::{SchemaRead, SchemaWrite};
2
3/// Discriminator for oracle implementations.
4#[repr(u8)]
5#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
6#[wincode(tag_encoding = "u8")]
7pub enum OracleKind {
8    #[wincode(tag = 0)]
9    Switchboard = 0,
10    #[wincode(tag = 1)]
11    Pyth = 1,
12}
13
14impl OracleKind {
15    pub const fn as_u8(self) -> u8 {
16        self as u8
17    }
18
19    pub const fn from_u8(kind: u8) -> Option<Self> {
20        match kind {
21            0 => Some(Self::Switchboard),
22            1 => Some(Self::Pyth),
23            _ => None,
24        }
25    }
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct InvalidOracleKind;
30
31/// Switchboard On-Demand oracle configuration stored with the asset it prices.
32///
33/// `price_decimals` is the scale of the raw oracle price. A price of `123`
34/// with `price_decimals = 2` represents `1.23`.
35#[derive(
36    Clone, Copy, Debug, Default, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead,
37)]
38#[wincode(assert_zero_copy)]
39#[repr(C)]
40pub struct SwitchboardOracleConfig {
41    pub quote_account: [u8; 32],
42    pub queue_account: [u8; 32],
43    pub feed_id: [u8; 32],
44    pub max_age_slots: u64,
45    pub price_decimals: u8,
46    _padding: [u8; 7],
47}
48
49impl SwitchboardOracleConfig {
50    pub const fn new(
51        quote_account: [u8; 32],
52        queue_account: [u8; 32],
53        feed_id: [u8; 32],
54        price_decimals: u8,
55        max_age_slots: u64,
56    ) -> Self {
57        Self {
58            quote_account,
59            queue_account,
60            feed_id,
61            max_age_slots,
62            price_decimals,
63            _padding: [0; 7],
64        }
65    }
66}
67
68/// Pyth pull-oracle configuration stored with the asset it prices.
69///
70/// `feed_id` is the 32-byte Pyth price feed id expected inside the submitted
71/// price update account. `price_decimals` is the scale Roshi exposes through
72/// `OraclePrice`; for example, a Pyth price of `123456789 * 10^-8` with
73/// `price_decimals = 8` is returned as `123456789`.
74///
75/// `max_confidence_bps = 0` disables the confidence-width guardrail.
76#[derive(
77    Clone, Copy, Debug, Default, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead,
78)]
79#[wincode(assert_zero_copy)]
80#[repr(C)]
81pub struct PythOracleConfig {
82    pub feed_id: [u8; 32],
83    pub max_age_seconds: u64,
84    pub max_confidence_bps: u16,
85    pub price_decimals: u8,
86    _padding: [u8; 5],
87}
88
89impl PythOracleConfig {
90    pub const fn new(
91        feed_id: [u8; 32],
92        price_decimals: u8,
93        max_age_seconds: u64,
94        max_confidence_bps: u16,
95    ) -> Self {
96        Self {
97            feed_id,
98            max_age_seconds,
99            max_confidence_bps,
100            price_decimals,
101            _padding: [0; 5],
102        }
103    }
104}
105
106/// Oracle configuration stored by vault and asset accounts.
107///
108/// The serialized shape includes every supported oracle implementation from
109/// the start. Switching implementations only changes `kind`, so account data
110/// size remains stable.
111#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
112#[wincode(assert_zero_copy)]
113#[repr(C)]
114pub struct OracleConfig {
115    pub switchboard: SwitchboardOracleConfig,
116    pub pyth: PythOracleConfig,
117    kind: u8,
118    _padding: [u8; 7],
119}
120
121impl OracleConfig {
122    pub const fn raw_kind(&self) -> u8 {
123        self.kind
124    }
125
126    pub const fn kind(&self) -> Result<OracleKind, InvalidOracleKind> {
127        match OracleKind::from_u8(self.kind) {
128            Some(kind) => Ok(kind),
129            None => Err(InvalidOracleKind),
130        }
131    }
132
133    pub const fn validate(&self) -> Result<(), InvalidOracleKind> {
134        match self.kind() {
135            Ok(_) => Ok(()),
136            Err(error) => Err(error),
137        }
138    }
139
140    pub const fn switchboard(config: SwitchboardOracleConfig) -> Self {
141        Self {
142            switchboard: config,
143            pyth: PythOracleConfig {
144                feed_id: [0; 32],
145                max_age_seconds: 0,
146                max_confidence_bps: 0,
147                price_decimals: 0,
148                _padding: [0; 5],
149            },
150            kind: OracleKind::Switchboard.as_u8(),
151            _padding: [0; 7],
152        }
153    }
154
155    pub const fn pyth(config: PythOracleConfig) -> Self {
156        Self {
157            switchboard: SwitchboardOracleConfig {
158                quote_account: [0; 32],
159                queue_account: [0; 32],
160                feed_id: [0; 32],
161                max_age_slots: 0,
162                price_decimals: 0,
163                _padding: [0; 7],
164            },
165            pyth: config,
166            kind: OracleKind::Pyth.as_u8(),
167            _padding: [0; 7],
168        }
169    }
170
171    pub const fn with_configs(
172        kind: OracleKind,
173        switchboard: SwitchboardOracleConfig,
174        pyth: PythOracleConfig,
175    ) -> Self {
176        Self {
177            switchboard,
178            pyth,
179            kind: kind.as_u8(),
180            _padding: [0; 7],
181        }
182    }
183}
184
185impl Default for OracleConfig {
186    fn default() -> Self {
187        Self::switchboard(SwitchboardOracleConfig::default())
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use wincode::{config::DefaultConfig, serialize, SchemaRead, SchemaWrite, TypeMeta};
195
196    fn assert_zero_copy<T>()
197    where
198        T: wincode::ZeroCopy,
199        T: for<'de> SchemaRead<'de, DefaultConfig> + SchemaWrite<DefaultConfig>,
200    {
201        assert_eq!(
202            <T as SchemaRead<'_, DefaultConfig>>::TYPE_META,
203            TypeMeta::Static {
204                size: core::mem::size_of::<T>(),
205                zero_copy: true,
206            }
207        );
208        assert_eq!(
209            <T as SchemaWrite<DefaultConfig>>::TYPE_META,
210            TypeMeta::Static {
211                size: core::mem::size_of::<T>(),
212                zero_copy: true,
213            }
214        );
215    }
216
217    #[test]
218    fn oracle_config_size_is_fixed_across_implementations() {
219        let switchboard = OracleConfig::switchboard(SwitchboardOracleConfig::new(
220            [1; 32], [2; 32], [3; 32], 6, 100,
221        ));
222        let pyth = OracleConfig::pyth(PythOracleConfig::new([4; 32], 8, 30, 250));
223
224        assert_eq!(
225            serialize(&switchboard).unwrap().len(),
226            serialize(&pyth).unwrap().len()
227        );
228        assert_eq!(switchboard.kind(), Ok(OracleKind::Switchboard));
229        assert_eq!(pyth.kind(), Ok(OracleKind::Pyth));
230    }
231
232    #[test]
233    fn with_configs_keeps_inactive_config_available() {
234        let switchboard_config = SwitchboardOracleConfig::new([1; 32], [2; 32], [3; 32], 6, 100);
235        let pyth_config = PythOracleConfig::new([4; 32], 8, 30, 250);
236
237        let config = OracleConfig::with_configs(OracleKind::Pyth, switchboard_config, pyth_config);
238
239        assert_eq!(config.kind(), Ok(OracleKind::Pyth));
240        assert_eq!(config.switchboard, switchboard_config);
241        assert_eq!(config.pyth, pyth_config);
242    }
243
244    #[test]
245    fn oracle_configs_are_zero_copy() {
246        assert_zero_copy::<SwitchboardOracleConfig>();
247        assert_zero_copy::<PythOracleConfig>();
248        assert_zero_copy::<OracleConfig>();
249        assert_eq!(core::mem::size_of::<SwitchboardOracleConfig>(), 112);
250        assert_eq!(core::mem::size_of::<PythOracleConfig>(), 48);
251        assert_eq!(core::mem::size_of::<OracleConfig>(), 168);
252        assert_eq!(
253            serialize(&OracleConfig::default()).unwrap().len(),
254            core::mem::size_of::<OracleConfig>()
255        );
256    }
257
258    #[test]
259    fn oracle_config_rejects_invalid_kind() {
260        let config = OracleConfig {
261            kind: 255,
262            ..OracleConfig::default()
263        };
264
265        assert_eq!(config.kind(), Err(InvalidOracleKind));
266        assert_eq!(config.validate(), Err(InvalidOracleKind));
267    }
268}