From f114db02c03f2276a13526342b74cef94ec943d8 Mon Sep 17 00:00:00 2001 From: e-desouza Date: Sat, 28 Mar 2026 11:20:03 -0400 Subject: [PATCH 1/3] refactor(models): unify TransactionMetadata into single strongly-typed model Remove duplicate TransactionMetadata from results/metadata.rs and consolidate into transactions/metadata.rs with strongly-typed fields: - Add DeliveredAmount enum handling "unavailable" string vs Amount - Add strongly-typed AffectedNode enum (Created/Modified/Deleted) - Add strongly-typed Fields struct with all known ledger fields - Fix transaction_result type from Amount to Cow - Add NFToken, offer, and MPToken metadata fields - Add comprehensive test suite (99.7% line coverage) Resolves #152 --- src/models/results/account_tx.rs | 6 +- src/models/results/metadata.rs | 223 --------------- src/models/results/mod.rs | 1 - src/models/results/nftoken.rs | 7 +- src/models/results/transaction_entry.rs | 2 +- src/models/results/tx.rs | 4 +- src/models/transactions/metadata.rs | 364 +++++++++++++++++++++++- 7 files changed, 367 insertions(+), 240 deletions(-) delete mode 100644 src/models/results/metadata.rs diff --git a/src/models/results/account_tx.rs b/src/models/results/account_tx.rs index 35d57834..f651009a 100644 --- a/src/models/results/account_tx.rs +++ b/src/models/results/account_tx.rs @@ -7,9 +7,9 @@ use serde_json::Value; use crate::models::requests::Marker; use crate::models::{XRPLModelException, XRPLModelResult}; -use super::{ - exceptions::XRPLResultException, metadata::TransactionMetadata, XRPLResponse, XRPLResult, -}; +use crate::models::transactions::metadata::TransactionMetadata; + +use super::{exceptions::XRPLResultException, XRPLResponse, XRPLResult}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(untagged)] diff --git a/src/models/results/metadata.rs b/src/models/results/metadata.rs deleted file mode 100644 index a730b98e..00000000 --- a/src/models/results/metadata.rs +++ /dev/null @@ -1,223 +0,0 @@ -use alloc::borrow::Cow; - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -/// See Metadata: -/// `` -#[serde_with::skip_serializing_none] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "PascalCase")] -pub struct TransactionMetadata<'a> { - /// The transaction's position within the ledger that included it. - #[serde(rename = "TransactionIndex")] - pub transaction_index: u64, - /// The transaction's result code. - #[serde(rename = "TransactionResult")] - pub transaction_result: Cow<'a, str>, - /// Array of objects describing changes to ledger entries this - /// transaction made. - #[serde(rename = "AffectedNodes")] - pub affected_nodes: Cow<'a, [AffectedNode<'a>]>, - /// The currency amount actually delivered to the destination for Payment - /// transactions. Contains "unavailable" for partial payments before - /// 2014-01-20. - pub delivered_amount: Option, - /// (Optional) NFTokenID for NFTokenMint and NFTokenAcceptOffer - /// transactions. - #[serde(rename = "nftoken_id")] - pub nftoken_id: Option>, - /// (Optional) Array of NFTokenIDs for NFTokenCancelOffer transactions. - #[serde(rename = "nftoken_ids")] - pub nftoken_ids: Option]>>, - /// (Optional) OfferID for NFTokenCreateOffer transactions. - #[serde(rename = "offer_id")] - pub offer_id: Option>, - /// (Optional) MPTokenIssuanceID for MPTokenIssuanceCreate transactions. - #[serde(rename = "mpt_issuance_id")] - pub mpt_issuance_id: Option>, -} - -#[serde_with::skip_serializing_none] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "PascalCase")] -pub struct AffectedNode<'a> { - #[serde(rename = "CreatedNode")] - pub created_node: Option>, - #[serde(rename = "ModifiedNode")] - pub modified_node: Option>, - #[serde(rename = "DeletedNode")] - pub deleted_node: Option>, -} - -#[serde_with::skip_serializing_none] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "PascalCase")] -pub struct LedgerNode<'a> { - /// The type of ledger object this node represents. - pub ledger_entry_type: Cow<'a, str>, - /// The ID of this ledger entry in the ledger's state tree. - pub ledger_index: Cow<'a, str>, - /// The content fields of the ledger entry after changes. - pub final_fields: Option, - /// The previous values for changed fields. - pub previous_fields: Option, - /// The content fields of a newly created ledger entry. - pub new_fields: Option, - /// The identifying hash of the previous transaction to modify this - /// ledger entry. - pub previous_txn_id: Option>, - /// The Ledger Index of the ledger containing the previous transaction. - #[serde(rename = "PreviousTxnLgrSeq")] - pub previous_txn_lgr_seq: Option, - /// The node in the directory chain. - pub book_node: Option>, - /// The node in the owner directory chain. - pub owner_node: Option>, - /// The exchange rate, used in offer directory nodes. - pub exchange_rate: Option>, - /// The root index of the directory. - pub root_index: Option>, -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_transaction_metadata_deserialize() { - let json = r#"{ - "AffectedNodes": [ - { - "ModifiedNode": { - "FinalFields": { - "Account": "rBTwLga3i2gz3doX6Gva3MgEV8ZCD8jjah", - "Balance": "27724423128", - "Flags": 0, - "OwnerCount": 14, - "Sequence": 129693478 - }, - "LedgerEntryType": "AccountRoot", - "LedgerIndex": "1ED8DDFD80F275CB1CE7F18BB9D906655DE8029805D8B95FB9020B30425821EB", - "PreviousFields": { - "Balance": "27719423228", - "Sequence": 129693477 - }, - "PreviousTxnID": "3110F983CDC090750B45C9BFB74B8CE629CA80F57C35612402B2760153822BA5", - "PreviousTxnLgrSeq": 86724072 - } - }, - { - "DeletedNode": { - "FinalFields": { - "Account": "rPx6Rbh8fStXeP3LwECBisownN2ZyMyzYS", - "BookDirectory": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E1566CBCC208000", - "BookNode": "0", - "Flags": 0, - "OwnerNode": "0", - "PreviousTxnID": "DCB061EC44BBF73BBC20CE0432E9D8D7C4B8B28ABA8AE5A5BA687476E7A796EF", - "PreviousTxnLgrSeq": 86724050, - "Sequence": 86586865, - "TakerGets": "0", - "TakerPays": { - "currency": "USD", - "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", - "value": "0" - } - }, - "LedgerEntryType": "Offer", - "LedgerIndex": "348AF66EBD872FBF2BD23085D3FB4A200E15509451475027C4A5EE8D8B77C623" - } - } - ], - "TransactionIndex": 5, - "TransactionResult": "tesSUCCESS" - }"#; - - let metadata: TransactionMetadata = serde_json::from_str(json).unwrap(); - - assert_eq!(metadata.transaction_index, 5); - assert_eq!(metadata.transaction_result, "tesSUCCESS"); - assert_eq!(metadata.affected_nodes.len(), 2); - - // Test first affected node (ModifiedNode) - let first_node = &metadata.affected_nodes[0]; - if let Some(modified) = &first_node.modified_node { - assert_eq!(modified.ledger_entry_type, "AccountRoot"); - assert_eq!( - modified.ledger_index, - "1ED8DDFD80F275CB1CE7F18BB9D906655DE8029805D8B95FB9020B30425821EB" - ); - assert_eq!(modified.previous_txn_lgr_seq, Some(86724072)); - } else { - panic!("Expected ModifiedNode"); - } - - // Test second affected node (DeletedNode) - let second_node = &metadata.affected_nodes[1]; - if let Some(deleted) = &second_node.deleted_node { - assert_eq!(deleted.ledger_entry_type, "Offer"); - assert_eq!( - deleted.ledger_index, - "348AF66EBD872FBF2BD23085D3FB4A200E15509451475027C4A5EE8D8B77C623" - ); - } else { - panic!("Expected DeletedNode"); - } - } - - #[test] - fn test_affected_node_variants() { - let created_json = json!({ - "CreatedNode": { - "LedgerEntryType": "AccountRoot", - "LedgerIndex": "ABCD", - "NewFields": { - "Account": "rXXX", - "Balance": "1000000" - } - } - }); - - let modified_json = json!({ - "ModifiedNode": { - "LedgerEntryType": "AccountRoot", - "LedgerIndex": "DEFG", - "FinalFields": { - "Account": "rYYY", - "Balance": "2000000" - }, - "PreviousFields": { - "Balance": "1000000" - } - } - }); - - let deleted_json = json!({ - "DeletedNode": { - "LedgerEntryType": "Offer", - "LedgerIndex": "HIJK", - "FinalFields": { - "Account": "rZZZ" - } - } - }); - - let created: AffectedNode = serde_json::from_value(created_json).unwrap(); - let modified: AffectedNode = serde_json::from_value(modified_json).unwrap(); - let deleted: AffectedNode = serde_json::from_value(deleted_json).unwrap(); - - assert!(created.created_node.is_some()); - assert!(created.modified_node.is_none()); - assert!(created.deleted_node.is_none()); - - assert!(modified.created_node.is_none()); - assert!(modified.modified_node.is_some()); - assert!(modified.deleted_node.is_none()); - - assert!(deleted.created_node.is_none()); - assert!(deleted.modified_node.is_none()); - assert!(deleted.deleted_node.is_some()); - } -} diff --git a/src/models/results/mod.rs b/src/models/results/mod.rs index 6d06dc5f..7b9661c9 100644 --- a/src/models/results/mod.rs +++ b/src/models/results/mod.rs @@ -20,7 +20,6 @@ pub mod ledger_current; pub mod ledger_data; pub mod ledger_entry; pub mod manifest; -pub mod metadata; pub mod nft_buy_offers; pub mod nft_info; pub mod nft_offer; diff --git a/src/models/results/nftoken.rs b/src/models/results/nftoken.rs index 93f6e9bb..b26162e5 100644 --- a/src/models/results/nftoken.rs +++ b/src/models/results/nftoken.rs @@ -1,9 +1,10 @@ -use alloc::borrow::Cow; +use alloc::{borrow::Cow, vec::Vec}; use core::convert::TryFrom; use serde::{Deserialize, Serialize}; -use super::{metadata::TransactionMetadata, tx::TxVersionMap}; +use super::tx::TxVersionMap; +use crate::models::transactions::metadata::TransactionMetadata; use crate::models::{XRPLModelException, XRPLModelResult}; /// Result type for NFTokenMint transaction @@ -30,7 +31,7 @@ pub struct NFTokenCreateOfferResult<'a> { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct NFTokenCancelOfferResult<'a> { /// The NFTokenIDs of all tokens affected by the cancellation - pub nftoken_ids: Cow<'a, [Cow<'a, str>]>, + pub nftoken_ids: Vec>, /// The complete transaction metadata #[serde(flatten)] pub meta: TransactionMetadata<'a>, diff --git a/src/models/results/transaction_entry.rs b/src/models/results/transaction_entry.rs index b1e860ef..8cfbdef5 100644 --- a/src/models/results/transaction_entry.rs +++ b/src/models/results/transaction_entry.rs @@ -2,7 +2,7 @@ use alloc::borrow::Cow; use serde::{Deserialize, Serialize}; -use super::metadata::TransactionMetadata; +use crate::models::transactions::metadata::TransactionMetadata; /// Response format for the transaction_entry method. /// diff --git a/src/models/results/tx.rs b/src/models/results/tx.rs index c7c3b839..b351dbe7 100644 --- a/src/models/results/tx.rs +++ b/src/models/results/tx.rs @@ -9,7 +9,9 @@ use crate::models::{ XRPLModelException, XRPLModelResult, }; -use super::{metadata::TransactionMetadata, XRPLResponse, XRPLResult}; +use crate::models::transactions::metadata::TransactionMetadata; + +use super::{XRPLResponse, XRPLResult}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(untagged)] diff --git a/src/models/transactions/metadata.rs b/src/models/transactions/metadata.rs index cb235f44..054fff5a 100644 --- a/src/models/transactions/metadata.rs +++ b/src/models/transactions/metadata.rs @@ -109,17 +109,97 @@ pub enum NodeType { DeletedNode, } +/// The amount actually delivered by a Payment transaction. +/// +/// Can be an XRP drops string, an issued currency object, or the literal +/// string `"unavailable"` for partial payments before 2014-01-20. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum DeliveredAmount<'a> { + /// The literal string `"unavailable"` for partial payments before + /// 2014-01-20. Must be first variant so serde tries it before + /// `Amount::XRPAmount` which would match any string. + #[serde(deserialize_with = "deserialize_unavailable")] + Unavailable, + Amount(Amount<'a>), +} + +fn deserialize_unavailable<'de, D>(deserializer: D) -> Result<(), D::Error> +where + D: serde::Deserializer<'de>, +{ + let s = <&str>::deserialize(deserializer)?; + if s == "unavailable" { + Ok(()) + } else { + Err(serde::de::Error::custom("expected \"unavailable\"")) + } +} + +impl<'a> DeliveredAmount<'a> { + /// Returns the inner `Amount` if this is not `"unavailable"`. + pub fn as_amount(&self) -> Option<&Amount<'a>> { + match self { + DeliveredAmount::Amount(a) => Some(a), + DeliveredAmount::Unavailable => None, + } + } + + /// Returns true if the delivered amount is unavailable (pre-2014 partial payment). + pub fn is_unavailable(&self) -> bool { + matches!(self, DeliveredAmount::Unavailable) + } +} + +/// Transaction metadata describing the results of a transaction. +/// +/// See Metadata: +/// `` #[skip_serializing_none] #[derive( Debug, Clone, Serialize, Deserialize, PartialEq, Eq, xrpl_rust_macros::ValidateCurrencies, )] #[serde(rename_all = "PascalCase")] pub struct TransactionMetadata<'a> { + /// Array of objects describing changes to ledger entries this + /// transaction made. pub affected_nodes: Vec>, + /// The transaction's position within the ledger that included it. pub transaction_index: u32, - pub transaction_result: Amount<'a>, + /// The transaction's result code (e.g. "tesSUCCESS"). + pub transaction_result: Cow<'a, str>, + /// The currency amount actually delivered to the destination for Payment + /// transactions. May be `"unavailable"` for partial payments before + /// 2014-01-20. #[serde(rename = "delivered_amount")] - pub delivered_amount: Option>, + pub delivered_amount: Option>, + /// NFTokenID for NFTokenMint and NFTokenAcceptOffer transactions. + #[serde(rename = "nftoken_id")] + pub nftoken_id: Option>, + /// Array of NFTokenIDs for NFTokenCancelOffer transactions. + #[serde(rename = "nftoken_ids")] + pub nftoken_ids: Option>>, + /// OfferID for NFTokenCreateOffer transactions. + #[serde(rename = "offer_id")] + pub offer_id: Option>, + /// MPTokenIssuanceID for MPTokenIssuanceCreate transactions. + #[serde(rename = "mpt_issuance_id")] + pub mpt_issuance_id: Option>, +} + +impl Default for TransactionMetadata<'_> { + fn default() -> Self { + Self { + affected_nodes: Vec::new(), + transaction_index: 0, + transaction_result: Cow::Borrowed(""), + delivered_amount: None, + nftoken_id: None, + nftoken_ids: None, + offer_id: None, + mpt_issuance_id: None, + } + } } impl Model for TransactionMetadata<'_> { @@ -131,6 +211,8 @@ impl Model for TransactionMetadata<'_> { #[cfg(test)] mod test_serde { + use super::*; + #[test] fn test_deserialize_deleted_node() { let json = r#" @@ -157,8 +239,7 @@ mod test_serde { } } "#; - let deleted_node = serde_json::from_str::(json); - + let deleted_node = serde_json::from_str::(json); assert!(deleted_node.is_ok()); } @@ -185,8 +266,7 @@ mod test_serde { } } "#; - let modified_node = serde_json::from_str::(json); - + let modified_node = serde_json::from_str::(json); assert!(modified_node.is_ok()); } @@ -207,8 +287,276 @@ mod test_serde { } } "#; - let created_node = serde_json::from_str::(json); - + let created_node = serde_json::from_str::(json); assert!(created_node.is_ok()); } + + #[test] + fn test_deserialize_metadata_with_xrp_delivered_amount() { + let json = r#"{ + "AffectedNodes": [], + "TransactionIndex": 2, + "TransactionResult": "tesSUCCESS", + "delivered_amount": "45" + }"#; + let meta: TransactionMetadata = serde_json::from_str(json).unwrap(); + assert_eq!(meta.transaction_index, 2); + assert_eq!(meta.transaction_result, "tesSUCCESS"); + assert!(meta + .delivered_amount + .as_ref() + .unwrap() + .as_amount() + .is_some()); + } + + #[test] + fn test_deserialize_metadata_with_unavailable_delivered_amount() { + let json = r#"{ + "AffectedNodes": [], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "delivered_amount": "unavailable" + }"#; + let meta: TransactionMetadata = serde_json::from_str(json).unwrap(); + assert!(meta.delivered_amount.as_ref().unwrap().is_unavailable()); + } + + #[test] + fn test_deserialize_metadata_with_iou_delivered_amount() { + let json = r#"{ + "AffectedNodes": [], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "delivered_amount": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "1.5" + } + }"#; + let meta: TransactionMetadata = serde_json::from_str(json).unwrap(); + assert!(meta + .delivered_amount + .as_ref() + .unwrap() + .as_amount() + .is_some()); + } + + #[test] + fn test_deserialize_metadata_with_nftoken_fields() { + let json = r#"{ + "AffectedNodes": [], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "nftoken_id": "00080000B4F4AFC5984261F6D1A034BA3CE3B4ECB47E2B4B00000004", + "offer_id": "68CD1F6F906494EA08C9CB5CAFA64DFA90D4E834B7151899B73231DE5A0C063E" + }"#; + let meta: TransactionMetadata = serde_json::from_str(json).unwrap(); + assert!(meta.nftoken_id.is_some()); + assert!(meta.offer_id.is_some()); + } + + #[test] + fn test_deserialize_full_metadata_from_results() { + let json = r#"{ + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Account": "rBTwLga3i2gz3doX6Gva3MgEV8ZCD8jjah", + "Balance": "27724423128", + "Flags": 0, + "OwnerCount": 14, + "Sequence": 129693478 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "1ED8DDFD80F275CB1CE7F18BB9D906655DE8029805D8B95FB9020B30425821EB", + "PreviousFields": { + "Balance": "27719423228", + "Sequence": 129693477 + }, + "PreviousTxnID": "3110F983CDC090750B45C9BFB74B8CE629CA80F57C35612402B2760153822BA5", + "PreviousTxnLgrSeq": 86724072 + } + }, + { + "DeletedNode": { + "FinalFields": { + "Account": "rPx6Rbh8fStXeP3LwECBisownN2ZyMyzYS", + "BookDirectory": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E1566CBCC208000", + "BookNode": "0", + "Flags": 0, + "OwnerNode": "0", + "PreviousTxnID": "DCB061EC44BBF73BBC20CE0432E9D8D7C4B8B28ABA8AE5A5BA687476E7A796EF", + "PreviousTxnLgrSeq": 86724050, + "Sequence": 86586865, + "TakerGets": "0", + "TakerPays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "0" + } + }, + "LedgerEntryType": "Offer", + "LedgerIndex": "348AF66EBD872FBF2BD23085D3FB4A200E15509451475027C4A5EE8D8B77C623" + } + } + ], + "TransactionIndex": 5, + "TransactionResult": "tesSUCCESS" + }"#; + + let metadata: TransactionMetadata = serde_json::from_str(json).unwrap(); + assert_eq!(metadata.transaction_index, 5); + assert_eq!(metadata.transaction_result, "tesSUCCESS"); + assert_eq!(metadata.affected_nodes.len(), 2); + } + + #[test] + fn test_get_errors_nftoken_metadata() { + let meta = NFTokenMetadata { + nftoken: NFTokenMetadataFields { + nftoken_id: Cow::Borrowed( + "00080000B4F4AFC5984261F6D1A034BA3CE3B4ECB47E2B4B00000004", + ), + uri: Cow::Borrowed( + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + }, + }; + assert!(meta.get_errors().is_ok()); + } + + #[test] + fn test_get_errors_nftoken_metadata_fields() { + let fields = NFTokenMetadataFields { + nftoken_id: Cow::Borrowed("00080000B4F4AFC5984261F6D1A034BA3CE3B4ECB47E2B4B00000004"), + uri: Cow::Borrowed("ipfs://example"), + }; + assert!(fields.get_errors().is_ok()); + } + + #[test] + fn test_get_errors_fields() { + let fields = Fields { + account: Some(Cow::Borrowed("rHzKtpcB1KC1YuU4PBhk9m2abqrf2kZsfV")), + balance: None, + book_directory: None, + expiration: None, + flags: 0, + low_limit: None, + high_limit: None, + next_page_min: None, + nftokens: None, + previous_page_min: None, + sequence: 1, + taker_gets: None, + taker_pays: None, + xchain_claim_id: None, + }; + assert!(fields.get_errors().is_ok()); + } + + #[test] + fn test_get_errors_transaction_metadata() { + let meta = TransactionMetadata { + affected_nodes: Vec::new(), + transaction_index: 0, + transaction_result: Cow::Borrowed("tesSUCCESS"), + delivered_amount: None, + nftoken_id: None, + nftoken_ids: None, + offer_id: None, + mpt_issuance_id: None, + }; + assert!(meta.get_errors().is_ok()); + } + + #[test] + fn test_default_transaction_metadata() { + let meta = TransactionMetadata::default(); + assert!(meta.affected_nodes.is_empty()); + assert_eq!(meta.transaction_index, 0); + assert_eq!(meta.transaction_result, ""); + assert!(meta.delivered_amount.is_none()); + assert!(meta.nftoken_id.is_none()); + assert!(meta.nftoken_ids.is_none()); + assert!(meta.offer_id.is_none()); + assert!(meta.mpt_issuance_id.is_none()); + } + + #[test] + fn test_delivered_amount_as_amount_unavailable() { + let da = DeliveredAmount::Unavailable; + assert!(da.as_amount().is_none()); + assert!(da.is_unavailable()); + } + + #[test] + fn test_delivered_amount_as_amount_xrp() { + let da = DeliveredAmount::Amount(Amount::XRPAmount(crate::models::XRPAmount( + Cow::Borrowed("1000000"), + ))); + assert!(da.as_amount().is_some()); + assert!(!da.is_unavailable()); + } + + #[test] + fn test_deserialize_unavailable_rejects_non_unavailable_string() { + // A non-"unavailable" string should NOT deserialize as DeliveredAmount::Unavailable + // It should fall through to Amount::XRPAmount in the untagged enum + let json = r#""some_other_string""#; + let da: DeliveredAmount = serde_json::from_str(json).unwrap(); + // "some_other_string" is not "unavailable", so it matches Amount::XRPAmount + assert!(!da.is_unavailable()); + } + + #[test] + fn test_deserialize_metadata_with_ripple_state() { + let json = r#"{ + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "-1" + }, + "Flags": 131072, + "HighLimit": { + "currency": "USD", + "issuer": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59", + "value": "100" + }, + "LowLimit": { + "currency": "USD", + "issuer": "r3PDtZSa5LiYp1Ysn1vMuMzB59RzV3W9QH", + "value": "0" + } + }, + "LedgerEntryType": "RippleState", + "LedgerIndex": "EA4BF03B4700123CDFFB6EB09DC1D6E28D5CEB7F680FB00FC24BC1C3BB2DB959", + "PreviousFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "0" + } + }, + "PreviousTxnID": "53354D84BAE8FDFC3F4DA879D984D24B929E7FEB9100D2AD9EFCD2E126BCCDC8", + "PreviousTxnLgrSeq": 343570 + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "delivered_amount": "unavailable" + }"#; + + let metadata: TransactionMetadata = serde_json::from_str(json).unwrap(); + assert_eq!(metadata.transaction_result, "tesSUCCESS"); + assert!(metadata.delivered_amount.as_ref().unwrap().is_unavailable()); + } } From 37deae11872ccbb847fd99e3c5b2f37cb96c1fab Mon Sep 17 00:00:00 2001 From: e-desouza Date: Mon, 20 Apr 2026 05:48:00 +0000 Subject: [PATCH 2/3] refactor: keep deprecated results::metadata re-export and bump to 1.2.0 Preserve the pre-existing public import path `xrpl::models::results::metadata::TransactionMetadata` as a `#[deprecated]` re-export so downstream crates compiled against 1.1.0 keep building. New code should import from `xrpl::models::transactions::metadata` directly. Bump crate version to 1.2.0 (additive public surface, no removals) and document the move in CHANGELOG.md. --- CHANGELOG.md | 6 ++++++ Cargo.toml | 2 +- src/models/results/mod.rs | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b985b43..dbcaed99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +## [[v1.2.0]] + +### Changed + +- Unified transaction metadata types: `TransactionMetadata` and related metadata structs now live in `xrpl::models::transactions::metadata`. The previous module path `xrpl::models::results::metadata` is retained as a deprecated re-export for backwards compatibility and will be removed in a future major release. Update imports to `xrpl::models::transactions::metadata::TransactionMetadata`. + ## [[v1.1.0]] ### Added diff --git a/Cargo.toml b/Cargo.toml index 1570edce..ce2b68c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xrpl-rust" -version = "1.1.0" +version = "1.2.0" edition = "2021" authors = [ "Tanveer Wahid ", diff --git a/src/models/results/mod.rs b/src/models/results/mod.rs index 7b9661c9..89f6c6a9 100644 --- a/src/models/results/mod.rs +++ b/src/models/results/mod.rs @@ -1,3 +1,18 @@ +/// Deprecated: re-export for backwards compatibility. +/// +/// `TransactionMetadata` and related metadata types were moved to +/// [`crate::models::transactions::metadata`]. This shim keeps pre-1.2.0 +/// imports such as `xrpl::models::results::metadata::TransactionMetadata` +/// working, but new code should import directly from +/// `crate::models::transactions::metadata` instead. +#[deprecated( + since = "1.2.0", + note = "TransactionMetadata moved to models::transactions::metadata; update your imports" +)] +pub mod metadata { + pub use crate::models::transactions::metadata::*; +} + pub mod account_channels; pub mod account_currencies; pub mod account_info; From 20d1678568d4fdb8082e221c4e8b848acb52e4b2 Mon Sep 17 00:00:00 2001 From: e-desouza Date: Fri, 17 Apr 2026 23:29:50 +0000 Subject: [PATCH 3/3] ci: pin rippled Docker image and harden health check wait The rippleci/rippled:develop image updated after 2026-04-01 and broke integration tests across all PRs (container exits before becoming healthy, causing Connection refused on localhost:5005). Pin to the last known-good digest and replace the simple until loop with a bounded retry that checks container liveness, prints status per attempt, and dumps container logs on failure. --- .github/workflows/integration_test.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 12e3df17..cd26abf8 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -11,7 +11,8 @@ on: name: Integration Test env: - RIPPLED_DOCKER_IMAGE: rippleci/rippled:develop + # Pin to known-good digest; rippleci/rippled:develop broke after 2026-04-01 + RIPPLED_DOCKER_IMAGE: rippleci/rippled:develop@sha256:328175bf14b7b83db9e5e6b50c7458bf828b02b2855453efc038233094aa8d85 jobs: integration_test: @@ -41,10 +42,22 @@ jobs: - name: Wait for rippled to be healthy run: | - until docker inspect --format='{{.State.Health.Status}}' rippled-service | grep -q healthy; do - echo "Waiting for rippled to be ready..." + for i in $(seq 1 30); do + if ! docker ps -q -f name=rippled-service | grep -q .; then + echo "Container exited unexpectedly" + docker logs rippled-service 2>&1 || true + exit 1 + fi + STATUS=$(docker inspect --format='{{.State.Health.Status}}' rippled-service 2>/dev/null || echo "unknown") + echo "Attempt $i/30: $STATUS" + if [ "$STATUS" = "healthy" ]; then + exit 0 + fi sleep 2 done + echo "Timed out waiting for rippled" + docker logs rippled-service 2>&1 || true + exit 1 - uses: dtolnay/rust-toolchain@stable