1use alloc::borrow::Cow;
2use alloc::collections::BTreeSet;
3use alloc::vec::Vec;
4use serde::{Deserialize, Serialize};
5use serde_with::skip_serializing_none;
6
7use crate::models::amount::XRPAmount;
8use crate::models::transactions::{Memo, PriceData, Signer, Transaction, TransactionType};
9use crate::models::{FlagCollection, Model, NoFlags, XRPLModelException, XRPLModelResult};
10
11use super::{CommonFields, CommonTransactionBuilder};
12
13const MAX_ORACLE_DATA_SERIES: u32 = 10;
16const MAX_ORACLE_PROVIDER_DECODED_BYTES: usize = 256;
20const MAX_ORACLE_URI_DECODED_BYTES: usize = 256;
23const MAX_ORACLE_ASSET_CLASS_DECODED_BYTES: usize = 16;
26
27#[skip_serializing_none]
32#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)]
33#[serde(rename_all = "PascalCase")]
34pub struct OracleSet<'a> {
35 #[serde(flatten)]
40 pub common_fields: CommonFields<'a, NoFlags>,
41 #[serde(rename = "OracleDocumentID")]
43 pub oracle_document_id: u32,
44 pub provider: Option<Cow<'a, str>>,
48 #[serde(rename = "URI")]
51 pub uri: Option<Cow<'a, str>>,
52 pub asset_class: Option<Cow<'a, str>>,
56 pub last_update_time: u32,
58 pub price_data_series: Vec<PriceData>,
61}
62
63impl Model for OracleSet<'_> {
64 fn get_errors(&self) -> XRPLModelResult<()> {
65 validate_optional_blob(
66 "provider",
67 self.provider.as_deref(),
68 MAX_ORACLE_PROVIDER_DECODED_BYTES,
69 )?;
70 validate_optional_blob("uri", self.uri.as_deref(), MAX_ORACLE_URI_DECODED_BYTES)?;
71 validate_optional_blob(
72 "asset_class",
73 self.asset_class.as_deref(),
74 MAX_ORACLE_ASSET_CLASS_DECODED_BYTES,
75 )?;
76
77 let series = &self.price_data_series;
78 if series.is_empty() {
79 return Err(XRPLModelException::ValueTooLow {
80 field: "price_data_series".into(),
81 min: 1,
82 found: 0,
83 });
84 }
85 if series.len() as u32 > MAX_ORACLE_DATA_SERIES {
86 return Err(XRPLModelException::ValueTooHigh {
87 field: "price_data_series".into(),
88 max: MAX_ORACLE_DATA_SERIES,
89 found: series.len() as u32,
90 });
91 }
92
93 let mut pairs = BTreeSet::new();
94 for entry in series {
95 entry.validate()?;
96 if entry.base_asset == entry.quote_asset {
97 return Err(XRPLModelException::ValueEqualsValue {
98 field1: "base_asset".into(),
99 field2: "quote_asset".into(),
100 });
101 }
102 let pair = (entry.base_asset.clone(), entry.quote_asset.clone());
103 if !pairs.insert(pair) {
104 return Err(XRPLModelException::InvalidValue {
105 field: "price_data_series".into(),
106 expected: "unique BaseAsset/QuoteAsset pairs".into(),
107 found: alloc::format!("{}/{}", entry.base_asset, entry.quote_asset),
108 });
109 }
110 }
111 Ok(())
112 }
113}
114
115fn validate_optional_blob(
116 field: &'static str,
117 value: Option<&str>,
118 max_bytes: usize,
119) -> XRPLModelResult<()> {
120 let Some(value) = value else {
121 return Ok(());
122 };
123 let bytes = hex::decode(value).map_err(|e| {
124 use hex::FromHexError;
125 let reason = match e {
126 FromHexError::OddLength => "hex string has odd length (incomplete byte)",
127 FromHexError::InvalidHexCharacter { .. } => "non-hexadecimal character in string",
128 FromHexError::InvalidStringLength => "invalid hex string length",
129 };
130 XRPLModelException::InvalidValue {
131 field: field.into(),
132 expected: alloc::format!("a valid hex-encoded Blob string ({reason})"),
133 found: value.into(),
134 }
135 })?;
136 if bytes.is_empty() {
140 return Err(XRPLModelException::InvalidValue {
141 field: field.into(),
142 expected: "a non-empty hex-encoded Blob string (empty strings are rejected)".into(),
143 found: value.into(),
144 });
145 }
146 if bytes.len() > max_bytes {
147 return Err(XRPLModelException::ValueTooLong {
148 field: field.into(),
149 max: max_bytes,
150 found: bytes.len(),
151 });
152 }
153 Ok(())
154}
155
156impl<'a> Transaction<'a, NoFlags> for OracleSet<'a> {
157 fn get_transaction_type(&self) -> &TransactionType {
158 self.common_fields.get_transaction_type()
159 }
160
161 fn get_common_fields(&self) -> &CommonFields<'_, NoFlags> {
162 self.common_fields.get_common_fields()
163 }
164
165 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
166 self.common_fields.get_mut_common_fields()
167 }
168}
169
170impl<'a> CommonTransactionBuilder<'a, NoFlags> for OracleSet<'a> {
171 fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NoFlags> {
172 &mut self.common_fields
173 }
174
175 fn into_self(self) -> Self {
176 self
177 }
178}
179
180impl<'a> OracleSet<'a> {
181 pub fn new(
182 account: Cow<'a, str>,
183 account_txn_id: Option<Cow<'a, str>>,
184 fee: Option<XRPAmount<'a>>,
185 last_ledger_sequence: Option<u32>,
186 memos: Option<Vec<Memo>>,
187 sequence: Option<u32>,
188 signers: Option<Vec<Signer>>,
189 source_tag: Option<u32>,
190 ticket_sequence: Option<u32>,
191 oracle_document_id: u32,
192 provider: Option<Cow<'a, str>>,
193 uri: Option<Cow<'a, str>>,
194 asset_class: Option<Cow<'a, str>>,
195 last_update_time: u32,
196 price_data_series: Vec<PriceData>,
197 ) -> Self {
198 Self {
199 common_fields: CommonFields {
200 account,
201 transaction_type: TransactionType::OracleSet,
202 account_txn_id,
203 fee,
204 flags: FlagCollection::default(),
205 last_ledger_sequence,
206 memos,
207 network_id: None,
208 sequence,
209 signers,
210 signing_pub_key: None, source_tag,
212 ticket_sequence,
213 txn_signature: None, },
215 oracle_document_id,
216 provider,
217 uri,
218 asset_class,
219 last_update_time,
220 price_data_series,
221 }
222 }
223
224 pub fn with_oracle_document_id(mut self, id: u32) -> Self {
226 self.oracle_document_id = id;
227 self
228 }
229
230 pub fn with_provider(mut self, provider: Cow<'a, str>) -> Self {
232 self.provider = Some(provider);
233 self
234 }
235
236 pub fn with_uri(mut self, uri: Cow<'a, str>) -> Self {
238 self.uri = Some(uri);
239 self
240 }
241
242 pub fn with_asset_class(mut self, asset_class: Cow<'a, str>) -> Self {
244 self.asset_class = Some(asset_class);
245 self
246 }
247
248 pub fn with_last_update_time(mut self, time: u32) -> Self {
250 self.last_update_time = time;
251 self
252 }
253
254 pub fn with_price_data_series(mut self, series: Vec<PriceData>) -> Self {
256 self.price_data_series = series;
257 self
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use alloc::string::ToString;
265 use alloc::vec;
266
267 const TEST_ACCOUNT: &str = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW";
269 const TEST_FEE: &str = "12";
270 const TEST_SEQUENCE: u32 = 391;
271 const TEST_LAST_LEDGER: u32 = 596447;
272 const TEST_DOC_ID: u32 = 1;
273 const TEST_LAST_UPDATE_TIME: u32 = 743609014;
274 const TEST_PROVIDER: &str = "636861696E6C696E6B";
276 const TEST_ASSET_CLASS: &str = "63757272656E6379";
278
279 #[test]
280 fn test_serde() {
281 let oracle_set = OracleSet {
282 common_fields: CommonFields {
283 account: TEST_ACCOUNT.into(),
284 transaction_type: TransactionType::OracleSet,
285 fee: Some("12".into()),
286 sequence: Some(391),
287 signing_pub_key: Some("".into()),
288 ..Default::default()
289 },
290 oracle_document_id: 1,
291 provider: Some("636861696E6C696E6B".into()),
292 uri: Some("68747470733A2F2F6578616D706C652E636F6D2F6F7261636C6531".into()),
293 asset_class: Some("63757272656E6379".into()),
294 last_update_time: 743609014,
295 price_data_series: vec![PriceData {
296 base_asset: "EUR".to_string(),
297 quote_asset: "USD".to_string(),
298 asset_price: Some("740".to_string()), scale: Some(1),
300 }],
301 };
302
303 let serialized = serde_json::to_string(&oracle_set)
304 .expect("OracleSet should serialize to JSON without error");
305 let deserialized: OracleSet = serde_json::from_str(&serialized)
306 .expect("OracleSet should deserialize from its own JSON output");
307 assert_eq!(oracle_set, deserialized);
308 assert!(oracle_set.get_errors().is_ok());
310 }
311
312 #[test]
313 fn test_builder_pattern() {
314 let oracle_set = OracleSet {
315 common_fields: CommonFields {
316 account: TEST_ACCOUNT.into(),
317 transaction_type: TransactionType::OracleSet,
318 ..Default::default()
319 },
320 ..Default::default()
321 }
322 .with_oracle_document_id(1)
323 .with_provider("chainlink".into())
324 .with_uri("https://example.com".into())
325 .with_asset_class("63757272656E6379".into())
326 .with_last_update_time(743609014)
327 .with_fee("12".into())
328 .with_sequence(100)
329 .with_last_ledger_sequence(596447)
330 .with_source_tag(42);
331
332 assert_eq!(oracle_set.oracle_document_id, 1);
333 assert_eq!(oracle_set.provider.as_deref(), Some("chainlink"));
334 assert_eq!(oracle_set.uri.as_deref(), Some("https://example.com"));
335 assert_eq!(oracle_set.asset_class.as_deref(), Some("63757272656E6379"));
336 assert_eq!(oracle_set.last_update_time, 743609014);
337 assert_eq!(oracle_set.common_fields.fee.as_ref().unwrap().0, "12");
338 assert_eq!(oracle_set.common_fields.sequence, Some(100));
339 assert_eq!(oracle_set.common_fields.last_ledger_sequence, Some(596447));
340 assert_eq!(oracle_set.common_fields.source_tag, Some(42));
341 }
342
343 #[test]
344 fn test_default() {
345 let oracle_set = OracleSet {
346 common_fields: CommonFields {
347 account: TEST_ACCOUNT.into(),
348 transaction_type: TransactionType::OracleSet,
349 ..Default::default()
350 },
351 ..Default::default()
352 };
353
354 assert_eq!(oracle_set.common_fields.account, TEST_ACCOUNT);
355 assert_eq!(
356 oracle_set.common_fields.transaction_type,
357 TransactionType::OracleSet
358 );
359 assert_eq!(oracle_set.oracle_document_id, 0);
360 assert!(oracle_set.provider.is_none());
361 assert!(oracle_set.uri.is_none());
362 assert!(oracle_set.asset_class.is_none());
363 assert_eq!(oracle_set.last_update_time, 0);
364 assert!(oracle_set.price_data_series.is_empty());
365 }
366
367 #[test]
368 fn test_with_price_data() {
369 let price_data = vec![
370 PriceData {
371 base_asset: "EUR".to_string(),
372 quote_asset: "USD".to_string(),
373 asset_price: Some("740".to_string()), scale: Some(1),
375 },
376 PriceData {
377 base_asset: "BTC".to_string(),
378 quote_asset: "USD".to_string(),
379 asset_price: Some("2600000".to_string()), scale: Some(2),
381 },
382 ];
383
384 let oracle_set = OracleSet {
385 common_fields: CommonFields {
386 account: TEST_ACCOUNT.into(),
387 transaction_type: TransactionType::OracleSet,
388 ..Default::default()
389 },
390 ..Default::default()
391 }
392 .with_price_data_series(price_data.clone());
393
394 let series = oracle_set.price_data_series;
395 assert_eq!(series.len(), 2);
396 assert_eq!(series[0].base_asset, "EUR");
397 assert_eq!(series[0].quote_asset, "USD");
398 assert_eq!(series[0].asset_price.as_deref(), Some("740"));
399 assert_eq!(series[0].scale, Some(1));
400 assert_eq!(series[1].base_asset, "BTC");
401 }
402
403 #[test]
404 fn test_minimal() {
405 let oracle_set = OracleSet {
406 common_fields: CommonFields {
407 account: TEST_ACCOUNT.into(),
408 transaction_type: TransactionType::OracleSet,
409 ..Default::default()
410 },
411 oracle_document_id: TEST_DOC_ID,
412 last_update_time: TEST_LAST_UPDATE_TIME,
413 price_data_series: vec![],
414 ..Default::default()
415 };
416
417 assert_eq!(oracle_set.common_fields.account, TEST_ACCOUNT);
418 assert_eq!(
419 oracle_set.common_fields.transaction_type,
420 TransactionType::OracleSet
421 );
422 assert_eq!(oracle_set.oracle_document_id, TEST_DOC_ID);
423 }
424
425 #[test]
426 fn test_new_constructor() {
427 let oracle_set = OracleSet {
428 common_fields: CommonFields {
429 account: TEST_ACCOUNT.into(),
430 transaction_type: TransactionType::OracleSet,
431 fee: Some(TEST_FEE.into()),
432 last_ledger_sequence: Some(TEST_LAST_LEDGER),
433 sequence: Some(TEST_SEQUENCE),
434 ..Default::default()
435 },
436 oracle_document_id: TEST_DOC_ID,
437 provider: Some("chainlink".into()),
440 uri: Some("68747470733A2F2F6578616D706C652E636F6D2F6F7261636C6531".into()),
441 asset_class: Some(TEST_ASSET_CLASS.into()),
442 last_update_time: TEST_LAST_UPDATE_TIME,
443 price_data_series: vec![PriceData {
444 base_asset: "EUR".to_string(),
445 quote_asset: "USD".to_string(),
446 asset_price: Some("2E4".to_string()), scale: Some(1),
448 }],
449 };
450
451 assert_eq!(
452 oracle_set.common_fields.transaction_type,
453 TransactionType::OracleSet
454 );
455 assert_eq!(oracle_set.common_fields.fee, Some(TEST_FEE.into()));
456 assert_eq!(oracle_set.common_fields.sequence, Some(TEST_SEQUENCE));
457 assert_eq!(oracle_set.oracle_document_id, TEST_DOC_ID);
458 assert_eq!(oracle_set.provider.as_deref(), Some("chainlink"));
459 assert_eq!(oracle_set.last_update_time, TEST_LAST_UPDATE_TIME);
460 assert_eq!(oracle_set.price_data_series.len(), 1);
461 }
462
463 #[test]
464 fn test_transaction_type() {
465 let oracle_set = OracleSet {
466 common_fields: CommonFields {
467 account: TEST_ACCOUNT.into(),
468 transaction_type: TransactionType::OracleSet,
469 ..Default::default()
470 },
471 ..Default::default()
472 };
473
474 assert_eq!(
475 *oracle_set.get_transaction_type(),
476 TransactionType::OracleSet
477 );
478 }
479
480 #[test]
481 fn test_with_memos() {
482 let oracle_set = OracleSet {
483 common_fields: CommonFields {
484 account: TEST_ACCOUNT.into(),
485 transaction_type: TransactionType::OracleSet,
486 ..Default::default()
487 },
488 ..Default::default()
489 }
490 .with_oracle_document_id(1)
491 .with_memo(Memo {
492 memo_data: Some("oracle update".into()),
493 memo_format: None,
494 memo_type: Some("text".into()),
495 });
496
497 assert_eq!(oracle_set.common_fields.memos.as_ref().unwrap().len(), 1);
498 }
499
500 #[test]
501 fn test_empty_price_data_series_rejected() {
502 let oracle_set = OracleSet {
504 common_fields: CommonFields {
505 account: TEST_ACCOUNT.into(),
506 transaction_type: TransactionType::OracleSet,
507 ..Default::default()
508 },
509 ..Default::default()
510 }
511 .with_price_data_series(vec![]);
512
513 let err = oracle_set.get_errors().unwrap_err();
514 assert_eq!(
515 err,
516 XRPLModelException::ValueTooLow {
517 field: "price_data_series".into(),
518 min: 1,
519 found: 0,
520 }
521 );
522 }
523
524 #[test]
525 fn test_price_data_optional_update_fields() {
526 let price_data = PriceData {
529 base_asset: "EUR".to_string(),
530 quote_asset: "USD".to_string(),
531 asset_price: None,
532 scale: None,
533 };
534
535 let oracle_set = OracleSet {
536 common_fields: CommonFields {
537 account: TEST_ACCOUNT.into(),
538 transaction_type: TransactionType::OracleSet,
539 ..Default::default()
540 },
541 ..Default::default()
542 }
543 .with_price_data_series(vec![price_data]);
544
545 let series = oracle_set.price_data_series;
546 assert_eq!(series[0].base_asset, "EUR");
547 assert_eq!(series[0].quote_asset, "USD");
548 assert!(series[0].asset_price.is_none());
549 assert!(series[0].scale.is_none());
550 }
551
552 #[test]
553 fn test_price_data_series_max_valid() {
554 let series: Vec<PriceData> = (0..10)
556 .map(|i| PriceData {
557 base_asset: alloc::format!("A{i:02}"),
558 quote_asset: "USD".to_string(),
559 asset_price: Some("100".to_string()), scale: Some(1),
561 })
562 .collect();
563
564 let oracle_set = OracleSet {
565 common_fields: CommonFields {
566 account: TEST_ACCOUNT.into(),
567 transaction_type: TransactionType::OracleSet,
568 ..Default::default()
569 },
570 ..Default::default()
571 }
572 .with_price_data_series(series);
573
574 assert!(oracle_set.get_errors().is_ok());
575 }
576
577 #[test]
578 fn test_price_data_series_exceeds_max() {
579 let series: Vec<PriceData> = (0..11)
580 .map(|i| PriceData {
581 base_asset: alloc::format!("A{i:02}"),
582 quote_asset: "USD".to_string(),
583 asset_price: Some("100".to_string()), scale: Some(1),
585 })
586 .collect();
587
588 let oracle_set = OracleSet {
589 common_fields: CommonFields {
590 account: TEST_ACCOUNT.into(),
591 transaction_type: TransactionType::OracleSet,
592 ..Default::default()
593 },
594 ..Default::default()
595 }
596 .with_price_data_series(series);
597
598 let err = oracle_set.get_errors().unwrap_err();
599 assert_eq!(
600 err,
601 XRPLModelException::ValueTooHigh {
602 field: "price_data_series".into(),
603 max: 10,
604 found: 11,
605 }
606 );
607 }
608
609 #[test]
610 fn test_scale_too_high_rejected() {
611 let oracle_set = OracleSet {
613 common_fields: CommonFields {
614 account: TEST_ACCOUNT.into(),
615 transaction_type: TransactionType::OracleSet,
616 ..Default::default()
617 },
618 ..Default::default()
619 }
620 .with_price_data_series(vec![PriceData {
621 base_asset: "EUR".to_string(),
622 quote_asset: "USD".to_string(),
623 asset_price: Some("100".to_string()), scale: Some(21),
625 }]);
626
627 let err = oracle_set.get_errors().unwrap_err();
628 assert_eq!(
629 err,
630 XRPLModelException::ValueTooHigh {
631 field: "scale".into(),
632 max: 20,
633 found: 21,
634 }
635 );
636 }
637
638 #[test]
639 fn test_scale_at_max_ok() {
640 let oracle_set = OracleSet {
642 common_fields: CommonFields {
643 account: TEST_ACCOUNT.into(),
644 transaction_type: TransactionType::OracleSet,
645 ..Default::default()
646 },
647 ..Default::default()
648 }
649 .with_price_data_series(vec![PriceData {
650 base_asset: "EUR".to_string(),
651 quote_asset: "USD".to_string(),
652 asset_price: Some("100".to_string()), scale: Some(20),
654 }]);
655
656 assert!(oracle_set.get_errors().is_ok());
657 }
658
659 #[test]
660 fn test_scale_mid_range_ok() {
661 let oracle_set = OracleSet {
663 common_fields: CommonFields {
664 account: TEST_ACCOUNT.into(),
665 transaction_type: TransactionType::OracleSet,
666 ..Default::default()
667 },
668 ..Default::default()
669 }
670 .with_price_data_series(vec![PriceData {
671 base_asset: "EUR".to_string(),
672 quote_asset: "USD".to_string(),
673 asset_price: Some("100".to_string()), scale: Some(15),
675 }]);
676
677 assert!(oracle_set.get_errors().is_ok());
678 }
679
680 #[test]
681 fn test_asset_price_and_scale_must_be_paired() {
682 let oracle_set = OracleSet {
683 common_fields: CommonFields {
684 account: TEST_ACCOUNT.into(),
685 transaction_type: TransactionType::OracleSet,
686 ..Default::default()
687 },
688 ..Default::default()
689 }
690 .with_price_data_series(vec![PriceData {
691 base_asset: "XRP".to_string(),
692 quote_asset: "USD".to_string(),
693 asset_price: Some("100".to_string()), scale: None,
695 }]);
696
697 assert!(matches!(
698 oracle_set.get_errors().unwrap_err(),
699 XRPLModelException::InvalidValue { ref field, .. } if field == "price_data"
700 ));
701 }
702
703 #[test]
704 fn test_invalid_base_asset_rejected() {
705 let oracle_set = OracleSet {
707 common_fields: CommonFields {
708 account: TEST_ACCOUNT.into(),
709 transaction_type: TransactionType::OracleSet,
710 ..Default::default()
711 },
712 ..Default::default()
713 }
714 .with_price_data_series(vec![PriceData {
715 base_asset: "EURO".to_string(),
716 quote_asset: "USD".to_string(),
717 asset_price: Some("100".to_string()), scale: Some(1),
719 }]);
720
721 let err = oracle_set.get_errors().unwrap_err();
722 assert!(matches!(
723 err,
724 XRPLModelException::InvalidValue { ref field, .. } if field == "base_asset"
725 ));
726 }
727
728 #[test]
729 fn test_xrp_as_asset_accepted() {
730 let oracle_set = OracleSet {
732 common_fields: CommonFields {
733 account: TEST_ACCOUNT.into(),
734 transaction_type: TransactionType::OracleSet,
735 ..Default::default()
736 },
737 ..Default::default()
738 }
739 .with_price_data_series(vec![PriceData {
740 base_asset: "XRP".to_string(),
741 quote_asset: "USD".to_string(),
742 asset_price: Some("100".to_string()), scale: Some(1),
744 }]);
745
746 assert!(oracle_set.get_errors().is_ok());
747 }
748
749 #[test]
750 fn test_hex_currency_accepted() {
751 let oracle_set = OracleSet {
753 common_fields: CommonFields {
754 account: TEST_ACCOUNT.into(),
755 transaction_type: TransactionType::OracleSet,
756 ..Default::default()
757 },
758 ..Default::default()
759 }
760 .with_price_data_series(vec![PriceData {
761 base_asset: "0000000000000000000000005553440000000000".to_string(),
762 quote_asset: "USD".to_string(),
763 asset_price: Some("100".to_string()), scale: Some(0),
765 }]);
766
767 assert!(oracle_set.get_errors().is_ok());
768 }
769
770 #[test]
771 fn test_oracle_metadata_lengths_rejected() {
772 let oracle_set = OracleSet {
773 common_fields: CommonFields {
774 account: TEST_ACCOUNT.into(),
775 transaction_type: TransactionType::OracleSet,
776 ..Default::default()
777 },
778 provider: Some("AA".repeat(MAX_ORACLE_PROVIDER_DECODED_BYTES + 1).into()),
779 price_data_series: vec![PriceData {
780 base_asset: "XRP".to_string(),
781 quote_asset: "USD".to_string(),
782 asset_price: Some("100".to_string()), scale: Some(1),
784 }],
785 ..Default::default()
786 };
787
788 assert!(matches!(
789 oracle_set.get_errors().unwrap_err(),
790 XRPLModelException::ValueTooLong { ref field, max, .. }
791 if field == "provider" && max == MAX_ORACLE_PROVIDER_DECODED_BYTES
792 ));
793 }
794
795 #[test]
796 fn test_oracle_metadata_must_be_hex() {
797 let oracle_set = OracleSet {
798 common_fields: CommonFields {
799 account: TEST_ACCOUNT.into(),
800 transaction_type: TransactionType::OracleSet,
801 ..Default::default()
802 },
803 provider: Some("chainlink".into()),
804 price_data_series: vec![PriceData {
805 base_asset: "XRP".to_string(),
806 quote_asset: "USD".to_string(),
807 asset_price: Some("100".to_string()), scale: Some(1),
809 }],
810 ..Default::default()
811 };
812
813 assert!(matches!(
814 oracle_set.get_errors().unwrap_err(),
815 XRPLModelException::InvalidValue { ref field, .. } if field == "provider"
816 ));
817 }
818
819 #[test]
820 fn test_asset_price_full_u64_range_accepted() {
821 for price in ["8000000000000000", "FFFFFFFFFFFFFFFF", "1", "0"] {
825 let oracle_set = OracleSet {
826 common_fields: CommonFields {
827 account: TEST_ACCOUNT.into(),
828 transaction_type: TransactionType::OracleSet,
829 ..Default::default()
830 },
831 ..Default::default()
832 }
833 .with_price_data_series(vec![PriceData {
834 base_asset: "XRP".to_string(),
835 quote_asset: "USD".to_string(),
836 asset_price: Some(price.to_string()),
837 scale: Some(1),
838 }]);
839
840 assert!(
841 oracle_set.get_errors().is_ok(),
842 "AssetPrice {price} should be valid"
843 );
844 }
845 }
846
847 #[test]
848 fn test_asset_price_non_hex_rejected() {
849 let oracle_set = OracleSet {
850 common_fields: CommonFields {
851 account: TEST_ACCOUNT.into(),
852 transaction_type: TransactionType::OracleSet,
853 ..Default::default()
854 },
855 ..Default::default()
856 }
857 .with_price_data_series(vec![PriceData {
858 base_asset: "XRP".to_string(),
859 quote_asset: "USD".to_string(),
860 asset_price: Some("ZZZZZZZZZZZZZZZZ".to_string()),
861 scale: Some(1),
862 }]);
863
864 assert!(matches!(
865 oracle_set.get_errors().unwrap_err(),
866 XRPLModelException::InvalidValue { ref field, .. } if field == "asset_price"
867 ));
868 }
869
870 #[test]
871 fn test_empty_blob_fields_rejected() {
872 for (field_name, oracle) in [
874 (
875 "provider",
876 OracleSet {
877 common_fields: CommonFields {
878 account: TEST_ACCOUNT.into(),
879 transaction_type: TransactionType::OracleSet,
880 ..Default::default()
881 },
882 provider: Some("".into()),
883 price_data_series: vec![PriceData {
884 base_asset: "XRP".to_string(),
885 quote_asset: "USD".to_string(),
886 asset_price: Some("100".to_string()), scale: Some(1),
888 }],
889 ..Default::default()
890 },
891 ),
892 (
893 "uri",
894 OracleSet {
895 common_fields: CommonFields {
896 account: TEST_ACCOUNT.into(),
897 transaction_type: TransactionType::OracleSet,
898 ..Default::default()
899 },
900 uri: Some("".into()),
901 price_data_series: vec![PriceData {
902 base_asset: "XRP".to_string(),
903 quote_asset: "USD".to_string(),
904 asset_price: Some("100".to_string()), scale: Some(1),
906 }],
907 ..Default::default()
908 },
909 ),
910 (
911 "asset_class",
912 OracleSet {
913 common_fields: CommonFields {
914 account: TEST_ACCOUNT.into(),
915 transaction_type: TransactionType::OracleSet,
916 ..Default::default()
917 },
918 asset_class: Some("".into()),
919 price_data_series: vec![PriceData {
920 base_asset: "XRP".to_string(),
921 quote_asset: "USD".to_string(),
922 asset_price: Some("100".to_string()), scale: Some(1),
924 }],
925 ..Default::default()
926 },
927 ),
928 ] {
929 assert!(
930 matches!(
931 oracle.get_errors().unwrap_err(),
932 XRPLModelException::InvalidValue { ref field, .. } if field == field_name
933 ),
934 "empty {field_name} should be rejected"
935 );
936 }
937 }
938
939 #[test]
940 fn test_duplicate_price_data_pair_rejected() {
941 let oracle_set = OracleSet {
942 common_fields: CommonFields {
943 account: TEST_ACCOUNT.into(),
944 transaction_type: TransactionType::OracleSet,
945 ..Default::default()
946 },
947 ..Default::default()
948 }
949 .with_price_data_series(vec![
950 PriceData {
951 base_asset: "XRP".to_string(),
952 quote_asset: "USD".to_string(),
953 asset_price: Some("100".to_string()), scale: Some(1),
955 },
956 PriceData {
957 base_asset: "XRP".to_string(),
958 quote_asset: "USD".to_string(),
959 asset_price: Some("101".to_string()),
960 scale: Some(1),
961 },
962 ]);
963
964 assert!(matches!(
965 oracle_set.get_errors().unwrap_err(),
966 XRPLModelException::InvalidValue { ref field, .. } if field == "price_data_series"
967 ));
968 }
969
970 #[test]
971 fn test_same_base_quote_rejected() {
972 let oracle_set = OracleSet {
973 common_fields: CommonFields {
974 account: TEST_ACCOUNT.into(),
975 transaction_type: TransactionType::OracleSet,
976 ..Default::default()
977 },
978 ..Default::default()
979 }
980 .with_price_data_series(vec![PriceData {
981 base_asset: "XRP".to_string(),
982 quote_asset: "XRP".to_string(),
983 asset_price: Some("100".to_string()), scale: Some(1),
985 }]);
986
987 assert!(matches!(
988 oracle_set.get_errors().unwrap_err(),
989 XRPLModelException::ValueEqualsValue { ref field1, ref field2 }
990 if field1 == "base_asset" && field2 == "quote_asset"
991 ));
992 }
993}