1use wincode::{SchemaRead, SchemaWrite};
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub struct OraclePrice {
7 pub value: u128,
8 pub decimals: u8,
9}
10
11impl OraclePrice {
12 pub const UNIT: Self = Self {
16 value: 1,
17 decimals: 0,
18 };
19}
20
21#[repr(u8)]
23#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
24#[wincode(tag_encoding = "u8")]
25pub enum OracleKind {
26 #[wincode(tag = 0)]
27 Switchboard = 0,
28 #[wincode(tag = 1)]
29 Pyth = 1,
30}
31
32impl OracleKind {
33 pub const fn as_u8(self) -> u8 {
34 self as u8
35 }
36
37 pub const fn from_u8(kind: u8) -> Option<Self> {
38 match kind {
39 0 => Some(Self::Switchboard),
40 1 => Some(Self::Pyth),
41 _ => None,
42 }
43 }
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct InvalidOracleConfig;
48
49#[derive(
54 Clone, Copy, Debug, Default, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead,
55)]
56#[wincode(assert_zero_copy)]
57#[repr(C)]
58pub struct SwitchboardOracleConfig {
59 pub quote_account: [u8; 32],
60 pub queue_account: [u8; 32],
61 pub feed_id: [u8; 32],
62 pub max_age_slots: u64,
63 pub price_decimals: u8,
64 _padding: [u8; 7],
65}
66
67impl SwitchboardOracleConfig {
68 pub const fn new(
69 quote_account: [u8; 32],
70 queue_account: [u8; 32],
71 feed_id: [u8; 32],
72 price_decimals: u8,
73 max_age_slots: u64,
74 ) -> Self {
75 Self {
76 quote_account,
77 queue_account,
78 feed_id,
79 max_age_slots,
80 price_decimals,
81 _padding: [0; 7],
82 }
83 }
84}
85
86#[derive(
101 Clone, Copy, Debug, Default, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead,
102)]
103#[wincode(assert_zero_copy)]
104#[repr(C)]
105pub struct PythOracleConfig {
106 pub feed_id: [u8; 32],
107 pub price_update_account: [u8; 32],
108 pub max_age_seconds: u64,
109 pub max_confidence_bps: u16,
110 pub price_decimals: u8,
111 _padding: [u8; 5],
112}
113
114impl PythOracleConfig {
115 pub const fn new(
116 feed_id: [u8; 32],
117 price_decimals: u8,
118 max_age_seconds: u64,
119 max_confidence_bps: u16,
120 ) -> Self {
121 Self {
122 feed_id,
123 price_update_account: [0; 32],
124 max_age_seconds,
125 max_confidence_bps,
126 price_decimals,
127 _padding: [0; 5],
128 }
129 }
130
131 pub const fn pin_price_update_account(mut self, price_update_account: [u8; 32]) -> Self {
135 self.price_update_account = price_update_account;
136 self
137 }
138
139 pub fn pinned_price_update_account(&self) -> Option<[u8; 32]> {
142 if self.price_update_account == [0; 32] {
143 return None;
144 }
145
146 Some(self.price_update_account)
147 }
148}
149
150#[derive(Clone, Copy, Debug, Eq, PartialEq, codama_macros::CodamaType, SchemaWrite, SchemaRead)]
156#[wincode(assert_zero_copy)]
157#[repr(C)]
158pub struct OracleConfig {
159 pub switchboard: SwitchboardOracleConfig,
160 pub pyth: PythOracleConfig,
161 kind: u8,
162 _padding: [u8; 7],
163}
164
165impl OracleConfig {
166 pub const fn raw_kind(&self) -> u8 {
167 self.kind
168 }
169
170 pub const fn kind(&self) -> Result<OracleKind, InvalidOracleConfig> {
171 match OracleKind::from_u8(self.kind) {
172 Some(kind) => Ok(kind),
173 None => Err(InvalidOracleConfig),
174 }
175 }
176
177 pub const fn validate(&self) -> Result<(), InvalidOracleConfig> {
178 match self.kind() {
179 Ok(OracleKind::Pyth) => {
184 if self.pyth.max_confidence_bps == 0 {
185 return Err(InvalidOracleConfig);
186 }
187 Ok(())
188 }
189 Ok(OracleKind::Switchboard) => Ok(()),
190 Err(error) => Err(error),
191 }
192 }
193
194 pub const fn switchboard(config: SwitchboardOracleConfig) -> Self {
195 Self {
196 switchboard: config,
197 pyth: PythOracleConfig {
198 feed_id: [0; 32],
199 price_update_account: [0; 32],
200 max_age_seconds: 0,
201 max_confidence_bps: 0,
202 price_decimals: 0,
203 _padding: [0; 5],
204 },
205 kind: OracleKind::Switchboard.as_u8(),
206 _padding: [0; 7],
207 }
208 }
209
210 pub const fn pyth(config: PythOracleConfig) -> Self {
211 Self {
212 switchboard: SwitchboardOracleConfig {
213 quote_account: [0; 32],
214 queue_account: [0; 32],
215 feed_id: [0; 32],
216 max_age_slots: 0,
217 price_decimals: 0,
218 _padding: [0; 7],
219 },
220 pyth: config,
221 kind: OracleKind::Pyth.as_u8(),
222 _padding: [0; 7],
223 }
224 }
225
226 pub const fn with_configs(
227 kind: OracleKind,
228 switchboard: SwitchboardOracleConfig,
229 pyth: PythOracleConfig,
230 ) -> Self {
231 Self {
232 switchboard,
233 pyth,
234 kind: kind.as_u8(),
235 _padding: [0; 7],
236 }
237 }
238}
239
240impl Default for OracleConfig {
241 fn default() -> Self {
242 Self::switchboard(SwitchboardOracleConfig::default())
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use wincode::{config::DefaultConfig, serialize, SchemaRead, SchemaWrite, TypeMeta};
250
251 fn assert_zero_copy<T>()
252 where
253 T: wincode::ZeroCopy,
254 T: for<'de> SchemaRead<'de, DefaultConfig> + SchemaWrite<DefaultConfig>,
255 {
256 assert_eq!(
257 <T as SchemaRead<'_, DefaultConfig>>::TYPE_META,
258 TypeMeta::Static {
259 size: core::mem::size_of::<T>(),
260 zero_copy: true,
261 }
262 );
263 assert_eq!(
264 <T as SchemaWrite<DefaultConfig>>::TYPE_META,
265 TypeMeta::Static {
266 size: core::mem::size_of::<T>(),
267 zero_copy: true,
268 }
269 );
270 }
271
272 #[test]
273 fn oracle_config_size_is_fixed_across_implementations() {
274 let switchboard = OracleConfig::switchboard(SwitchboardOracleConfig::new(
275 [1; 32], [2; 32], [3; 32], 6, 100,
276 ));
277 let pyth = OracleConfig::pyth(PythOracleConfig::new([4; 32], 8, 30, 250));
278
279 assert_eq!(
280 serialize(&switchboard).unwrap().len(),
281 serialize(&pyth).unwrap().len()
282 );
283 assert_eq!(switchboard.kind(), Ok(OracleKind::Switchboard));
284 assert_eq!(pyth.kind(), Ok(OracleKind::Pyth));
285 }
286
287 #[test]
288 fn with_configs_keeps_inactive_config_available() {
289 let switchboard_config = SwitchboardOracleConfig::new([1; 32], [2; 32], [3; 32], 6, 100);
290 let pyth_config = PythOracleConfig::new([4; 32], 8, 30, 250);
291
292 let config = OracleConfig::with_configs(OracleKind::Pyth, switchboard_config, pyth_config);
293
294 assert_eq!(config.kind(), Ok(OracleKind::Pyth));
295 assert_eq!(config.switchboard, switchboard_config);
296 assert_eq!(config.pyth, pyth_config);
297 }
298
299 #[test]
300 fn oracle_configs_are_zero_copy() {
301 assert_zero_copy::<SwitchboardOracleConfig>();
302 assert_zero_copy::<PythOracleConfig>();
303 assert_zero_copy::<OracleConfig>();
304 assert_eq!(core::mem::size_of::<SwitchboardOracleConfig>(), 112);
305 assert_eq!(core::mem::size_of::<PythOracleConfig>(), 80);
306 assert_eq!(core::mem::size_of::<OracleConfig>(), 200);
307 assert_eq!(
308 serialize(&OracleConfig::default()).unwrap().len(),
309 core::mem::size_of::<OracleConfig>()
310 );
311 }
312
313 #[test]
314 fn pyth_price_update_pin_defaults_off_and_round_trips() {
315 let unpinned = PythOracleConfig::new([4; 32], 8, 30, 250);
316 assert_eq!(unpinned.pinned_price_update_account(), None);
317
318 let pinned = unpinned.pin_price_update_account([5; 32]);
319 assert_eq!(pinned.pinned_price_update_account(), Some([5; 32]));
320 }
321
322 #[test]
323 fn validate_requires_confidence_bound_on_active_pyth_leg() {
324 let unbounded = OracleConfig::pyth(PythOracleConfig::new([4; 32], 8, 30, 0));
325 assert_eq!(unbounded.validate(), Err(InvalidOracleConfig));
326
327 let bounded = OracleConfig::pyth(PythOracleConfig::new([4; 32], 8, 30, 250));
328 assert_eq!(bounded.validate(), Ok(()));
329
330 let switchboard = OracleConfig::switchboard(SwitchboardOracleConfig::new(
332 [1; 32], [2; 32], [3; 32], 6, 100,
333 ));
334 assert_eq!(switchboard.pyth.max_confidence_bps, 0);
335 assert_eq!(switchboard.validate(), Ok(()));
336 }
337
338 #[test]
339 fn oracle_config_rejects_invalid_kind() {
340 let config = OracleConfig {
341 kind: 255,
342 ..OracleConfig::default()
343 };
344
345 assert_eq!(config.kind(), Err(InvalidOracleConfig));
346 assert_eq!(config.validate(), Err(InvalidOracleConfig));
347 }
348}