Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5f8c692
feat: add PriceData type for XLS-47 price oracle support
e-desouza Apr 1, 2026
40cd76e
feat: add OracleSet transaction type (XLS-47)
e-desouza Apr 1, 2026
cfdfb77
feat: add OracleDelete transaction type (XLS-47)
e-desouza Apr 1, 2026
bfb2f8c
feat: add Oracle ledger entry type (XLS-47)
e-desouza Apr 1, 2026
b761010
test: add integration tests for price oracle types
e-desouza Apr 1, 2026
f912731
fix: make OracleSet required fields non-optional and add validation
e-desouza Apr 3, 2026
3745b48
fix(oracle-set): enforce PriceData bounds and oracle currency codes
e-desouza Apr 20, 2026
5c64a44
fix: align PriceData fields with oracle protocol
e-desouza Jun 4, 2026
9485e07
fix: enforce OracleSet required price data
e-desouza Jun 4, 2026
4075569
fix: model Oracle ledger required fields
e-desouza Jun 4, 2026
d706f54
test: submit oracle transactions in integration suite
e-desouza Jun 4, 2026
2fe13af
fix(oracle): validate Blob metadata as hex
e-desouza Jun 5, 2026
f321064
fix(oracle): cap AssetPrice UInt64 hex
e-desouza Jun 5, 2026
4955002
fix(oracle): restrict Scale to 0 through 10
e-desouza Jun 5, 2026
51d0258
fix(oracle): require AssetPrice and Scale pairing
e-desouza Jun 5, 2026
d22bf3b
test(oracle): submit POSIX LastUpdateTime
e-desouza Jun 5, 2026
656c1ae
feat(requests): add oracle AccountObjectType
e-desouza Jun 5, 2026
fa71e72
test(oracle): verify account_objects lifecycle
e-desouza Jun 5, 2026
f20aeb9
test: remove redundant serde roundtrip tests from integration test files
e-desouza Jun 10, 2026
5febcdc
refactor(oracle): remove unused new() constructor and owner_node_hex
e-desouza Jun 10, 2026
722c97c
test: update integration tests, use structural patterns, and remove r…
e-desouza Jun 10, 2026
d6b1aa6
test: align Oracle integration tests with xrpl.js
e-desouza Jun 10, 2026
3610c6a
test: add rippled-inspired integration tests for oracle delete errors…
e-desouza Jun 10, 2026
a17d4ea
fix(oracle): correct protocol constants and use struct pattern throug…
e-desouza Jun 10, 2026
ecb6705
refactor: add fixtures constants, submit_tx helper, and name CommonFi…
e-desouza Jun 10, 2026
95cc6f5
chore: ignore local dev artifacts (.local, dev/, .playwright-mcp)
e-desouza Jun 10, 2026
bc4f0c0
style: apply cargo fmt
e-desouza Jun 10, 2026
3e8ab37
ci: bump codecov-action to v5.5.5 (fixes missing gpg dependency)
e-desouza Jun 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/integration_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
--ignore-filename-regex '${{ env.COVERAGE_IGNORE_REGEX }}'
- name: Upload coverage to Codecov
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
**/.DS_Store

rustc-ice*
.local/*
dev/*
.playwright-mcp/*
4 changes: 4 additions & 0 deletions src/models/ledger/objects/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod negative_unl;
pub mod nftoken_offer;
pub mod nftoken_page;
pub mod offer;
pub mod oracle;
pub mod pay_channel;
pub mod ripple_state;
pub mod signer_list;
Expand All @@ -34,6 +35,7 @@ use negative_unl::NegativeUNL;
use nftoken_offer::NFTokenOffer;
use nftoken_page::NFTokenPage;
use offer::Offer;
use oracle::Oracle;
use pay_channel::PayChannel;
use ripple_state::RippleState;
use signer_list::SignerList;
Expand Down Expand Up @@ -66,6 +68,7 @@ pub enum LedgerEntryType {
NFTokenOffer = 0x0037,
NFTokenPage = 0x0050,
Offer = 0x006F,
Oracle = 0x0080,
PayChannel = 0x0078,
RippleState = 0x0072,
SignerList = 0x0053,
Expand All @@ -90,6 +93,7 @@ pub enum LedgerEntry<'a> {
NFTokenOffer(NFTokenOffer<'a>),
NFTokenPage(NFTokenPage<'a>),
Offer(Offer<'a>),
Oracle(Oracle<'a>),
PayChannel(PayChannel<'a>),
RippleState(RippleState<'a>),
SignerList(SignerList<'a>),
Expand Down
258 changes: 258 additions & 0 deletions src/models/ledger/objects/oracle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
use crate::models::ledger::objects::LedgerEntryType;
use crate::models::transactions::PriceData;
use crate::models::{Model, NoFlags};
use alloc::borrow::Cow;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;

use super::{CommonFields, LedgerObject};

/// The Oracle ledger entry holds data associated with a single price oracle object.
///
/// See Oracle:
/// `<https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/oracle>`
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct Oracle<'a> {
/// The base fields for all ledger object models.
///
/// See Ledger Object Common Fields:
/// `<https://xrpl.org/ledger-entry-common-fields.html>`
#[serde(flatten)]
pub common_fields: CommonFields<'a, NoFlags>,
/// The XRPL account with update and delete privileges for the oracle.
pub owner: Cow<'a, str>,
/// An arbitrary value that identifies an oracle provider.
pub provider: Cow<'a, str>,
/// Describes the type of asset, such as "currency", "commodity", or "NFT".
pub asset_class: Cow<'a, str>,
/// An array of up to 10 PriceData objects, representing the price information.
pub price_data_series: Vec<PriceData>,
/// The time the data was last updated, represented in the ripple epoch.
pub last_update_time: u32,
/// An optional Universal Resource Identifier to reference price data off-chain.
#[serde(rename = "URI")]
pub uri: Option<Cow<'a, str>>,
/// A hint indicating which page of the owner directory links to this entry.
pub owner_node: Cow<'a, str>,
/// The identifying hash of the transaction that most recently modified this entry.
#[serde(rename = "PreviousTxnID")]
pub previous_txn_id: Cow<'a, str>,
/// The index of the ledger that contains the transaction that most recently
/// modified this entry.
pub previous_txn_lgr_seq: u32,
/// A unique identifier of the price oracle for the account, if present in
/// the ledger entry returned by the server.
#[serde(rename = "OracleDocumentID")]
pub oracle_document_id: Option<u32>,
}

impl Model for Oracle<'_> {}

impl<'a> LedgerObject<NoFlags> for Oracle<'a> {
fn get_ledger_entry_type(&self) -> LedgerEntryType {
self.common_fields.get_ledger_entry_type()
}
}

#[cfg(test)]
mod test_serde {
use super::*;
use crate::models::transactions::PriceData;
use crate::models::FlagCollection;
use alloc::borrow::Cow;
use alloc::string::ToString;
use alloc::vec;

#[test]
fn test_serialize() {
let oracle = Oracle {
common_fields: CommonFields {
flags: FlagCollection::default(),
ledger_entry_type: LedgerEntryType::Oracle,
index: Some(Cow::from("ForTest")),
ledger_index: None,
},
owner: Cow::from("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"),
provider: Cow::from("636861696E6C696E6B"),
asset_class: Cow::from("63757272656E6379"),
price_data_series: vec![PriceData {
base_asset: "XRP".to_string(),
quote_asset: "USD".to_string(),
asset_price: Some("2E4".to_string()),
scale: Some(1),
}],
last_update_time: 743609014,
uri: Some(Cow::from("68747470733A2F2F6578616D706C652E636F6D")),
owner_node: Cow::from("0000000000000000"),
previous_txn_id: Cow::from("ABC123DEF456"),
previous_txn_lgr_seq: 12345678,
oracle_document_id: Some(1),
};

let serialized = serde_json::to_string(&oracle).unwrap();
let deserialized: Oracle = serde_json::from_str(&serialized).unwrap();
assert_eq!(oracle, deserialized);
}

#[test]
fn test_deserialize() {
let json = r#"{
"LedgerEntryType": "Oracle",
"Flags": 0,
"Owner": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"Provider": "636861696E6C696E6B",
"AssetClass": "63757272656E6379",
"PriceDataSeries": [
{
"PriceData": {
"BaseAsset": "XRP",
"QuoteAsset": "USD",
"AssetPrice": "2E4",
"Scale": 1
}
}
],
"LastUpdateTime": 743609014,
"URI": "68747470733A2F2F6578616D706C652E636F6D",
"OwnerNode": "0000000000000000",
"PreviousTxnID": "ABC123DEF456",
"PreviousTxnLgrSeq": 12345678,
"OracleDocumentID": 1
}"#;

let oracle: Oracle = serde_json::from_str(json).unwrap();
assert_eq!(oracle.owner, "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW");
assert_eq!(oracle.provider, "636861696E6C696E6B");
assert_eq!(oracle.asset_class, "63757272656E6379");
assert_eq!(oracle.price_data_series.len(), 1);
assert_eq!(oracle.price_data_series[0].base_asset, "XRP");
assert_eq!(oracle.price_data_series[0].quote_asset, "USD");
assert_eq!(oracle.price_data_series[0].asset_price, Some("2E4".into()));
assert_eq!(oracle.price_data_series[0].scale, Some(1));
assert_eq!(oracle.last_update_time, 743609014);
assert_eq!(
oracle.uri,
Some("68747470733A2F2F6578616D706C652E636F6D".into())
);
assert_eq!(oracle.owner_node, "0000000000000000");
assert_eq!(oracle.previous_txn_id, "ABC123DEF456");
assert_eq!(oracle.previous_txn_lgr_seq, 12345678);
assert_eq!(oracle.oracle_document_id, Some(1));
}

#[test]
fn test_new_minimal() {
let oracle = Oracle {
common_fields: CommonFields {
flags: FlagCollection::default(),
ledger_entry_type: LedgerEntryType::Oracle,
index: None,
ledger_index: None,
},
owner: Cow::from("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"),
provider: Cow::from("70726F766964657231"),
asset_class: Cow::from("63757272656E6379"),
price_data_series: vec![PriceData {
base_asset: "XRP".to_string(),
quote_asset: "USD".to_string(),
asset_price: Some("2E4".to_string()),
scale: Some(1),
}],
last_update_time: 743609014,
uri: None,
owner_node: Cow::from("0000000000000000"),
previous_txn_id: Cow::from("ABC123"),
previous_txn_lgr_seq: 100,
oracle_document_id: None,
};

assert_eq!(oracle.owner, "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW");
assert_eq!(oracle.provider, "70726F766964657231");
assert_eq!(oracle.asset_class, "63757272656E6379");
assert_eq!(oracle.price_data_series.len(), 1);
assert_eq!(oracle.last_update_time, 743609014);
assert!(oracle.uri.is_none());
assert_eq!(oracle.owner_node, "0000000000000000");
assert_eq!(oracle.previous_txn_id, "ABC123");
assert_eq!(oracle.previous_txn_lgr_seq, 100);
}

#[test]
fn test_ledger_entry_type() {
let oracle = Oracle {
common_fields: CommonFields {
flags: FlagCollection::default(),
ledger_entry_type: LedgerEntryType::Oracle,
index: None,
ledger_index: None,
},
owner: Cow::from("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"),
provider: Cow::from("70726F766964657231"),
asset_class: Cow::from("63757272656E6379"),
price_data_series: vec![PriceData {
base_asset: "XRP".to_string(),
quote_asset: "USD".to_string(),
asset_price: Some("2E4".to_string()),
scale: Some(1),
}],
last_update_time: 0,
uri: None,
owner_node: Cow::from("0000000000000000"),
previous_txn_id: Cow::from("ABC123"),
previous_txn_lgr_seq: 0,
oracle_document_id: None,
};

assert_eq!(oracle.get_ledger_entry_type(), LedgerEntryType::Oracle);
}

#[test]
fn test_with_multiple_price_data() {
let oracle = Oracle {
common_fields: CommonFields {
flags: FlagCollection::default(),
ledger_entry_type: LedgerEntryType::Oracle,
index: Some(Cow::from("TestIndex")),
ledger_index: None,
},
owner: Cow::from("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"),
provider: Cow::from("636861696E6C696E6B"),
asset_class: Cow::from("63757272656E6379"),
price_data_series: vec![
PriceData {
base_asset: "XRP".to_string(),
quote_asset: "USD".to_string(),
asset_price: Some("2E4".to_string()),
scale: Some(1),
},
PriceData {
base_asset: "BTC".to_string(),
quote_asset: "USD".to_string(),
asset_price: Some("27AC40".to_string()),
scale: Some(2),
},
PriceData {
base_asset: "ETH".to_string(),
quote_asset: "USD".to_string(),
asset_price: Some("27100".to_string()),
scale: Some(2),
},
],
last_update_time: 743609014,
uri: Some(Cow::from("68747470733A2F2F6578616D706C652E636F6D")),
owner_node: Cow::from("0000000000000000"),
previous_txn_id: Cow::from("DEF789"),
previous_txn_lgr_seq: 99999,
oracle_document_id: None,
};

let series = oracle.price_data_series;
assert_eq!(series.len(), 3);
assert_eq!(series[0].base_asset, "XRP");
assert_eq!(series[1].base_asset, "BTC");
assert_eq!(series[2].base_asset, "ETH");
}
}
3 changes: 2 additions & 1 deletion src/models/requests/account_objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum AccountObjectType {
DepositPreauth,
Escrow,
Offer,
Oracle,
PaymentChannel,
SignerList,
State,
Expand All @@ -43,7 +44,7 @@ pub struct AccountObjects<'a> {
pub ledger_lookup: Option<LookupByLedgerRequest<'a>>,
/// If included, filter results to include only this type
/// of ledger object. The valid types are: check, deposit_preauth,
/// escrow, offer, payment_channel, signer_list, ticket,
/// escrow, offer, oracle, payment_channel, signer_list, ticket,
/// and state (trust line).
pub r#type: Option<AccountObjectType>,
/// If true, the response only includes objects that would block
Expand Down
Loading
Loading