Skip to content
Merged
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
40cf50c
add get_xchain_claim_id
LimpidCrypto Nov 19, 2024
4a5578d
add str_conversion
LimpidCrypto Nov 19, 2024
893914b
current state of txn parsing
LimpidCrypto Nov 19, 2024
5b2b6df
fix core build
LimpidCrypto Nov 20, 2024
3f29ef3
add get_xchain_claim_id
LimpidCrypto Nov 19, 2024
926571b
refactor transaction metadata and balance parsing logic
LimpidCrypto Nov 25, 2024
a230d87
add balance change parsing
LimpidCrypto Jan 4, 2025
12ed42a
Merge branch 'dev' into utility-functions
LimpidCrypto Jan 4, 2025
99a25ae
fix clippy
LimpidCrypto Jan 4, 2025
eb24b2f
remove dbg! to fix gh workflow
LimpidCrypto Jan 4, 2025
4373cfd
fix gh workflow
LimpidCrypto Jan 4, 2025
ae280c7
add final balance parsing and fix gh workflow
LimpidCrypto Jan 5, 2025
6259906
current state of order book parsing
LimpidCrypto Jan 9, 2025
c02f071
fix multisigning
LimpidCrypto Feb 12, 2025
0f495ae
fix transactions to use the new constructor and fix the serde tests t…
LimpidCrypto Feb 13, 2025
bbfb487
fix multisign test
LimpidCrypto Feb 13, 2025
d688fb0
add changelog
LimpidCrypto Feb 13, 2025
392f17b
add get_xchain_claim_id
LimpidCrypto Nov 19, 2024
87c8c46
current state of txn parsing
LimpidCrypto Nov 19, 2024
d2e6197
refactor transaction metadata and balance parsing logic
LimpidCrypto Nov 25, 2024
5b46652
add balance change parsing
LimpidCrypto Jan 4, 2025
ba6e366
resolve errors caused by merging
LimpidCrypto Feb 15, 2025
b0b075a
resolve errors caused by merging
LimpidCrypto Feb 15, 2025
00a1d15
Merge branch 'dev' into utility-functions
LimpidCrypto Feb 27, 2025
765be3c
remove order book changes parser
LimpidCrypto Feb 27, 2025
8159f3b
fix deref error in account_info
LimpidCrypto Feb 27, 2025
7b68fac
improve tests
LimpidCrypto Feb 27, 2025
110b971
revert metadata changes
LimpidCrypto Feb 27, 2025
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
7 changes: 5 additions & 2 deletions src/core/binarycodec/types/currency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use alloc::vec::Vec;
use core::convert::TryFrom;
use core::convert::TryInto;
use core::fmt::Display;
use exceptions::XRPLUtilsException;
use serde::Serializer;
use serde::{Deserialize, Serialize};

Expand All @@ -28,11 +29,13 @@ pub const NATIVE_CODE: &str = "XRP";
#[serde(try_from = "&str")]
pub struct Currency(Hash160);

fn _iso_code_from_hex(value: &[u8]) -> Result<Option<String>, ISOCodeException> {
fn _iso_code_from_hex(value: &[u8]) -> Result<Option<String>, XRPLUtilsException> {
let candidate_iso = alloc::str::from_utf8(&value[12..15])?;

if candidate_iso == NATIVE_CODE {
Err(ISOCodeException::InvalidXRPBytes)
Err(XRPLUtilsException::ISOCodeError(
ISOCodeException::InvalidXRPBytes,
))
} else if is_iso_code(candidate_iso) {
Ok(Some(candidate_iso.to_string()))
} else {
Expand Down
14 changes: 14 additions & 0 deletions src/models/flag_collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ where
}
}

impl<T> TryFrom<Option<u32>> for FlagCollection<T>
where
T: IntoEnumIterator + Serialize,
{
type Error = XRPLModelException;

fn try_from(flags: Option<u32>) -> XRPLModelResult<Self> {
match flags {
Some(flags) => FlagCollection::try_from(flags),
None => Ok(FlagCollection::default()),
}
}
}

impl<T> TryFrom<FlagCollection<T>> for u32
where
T: IntoEnumIterator + Serialize,
Expand Down
32 changes: 28 additions & 4 deletions src/utils/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ pub enum XRPLUtilsException {
#[cfg(feature = "models")]
#[error("XRPL Model error: {0}")]
XRPLModelError(#[from] XRPLModelException),
#[error("XRPL Txn Parser error: {0}")]
XRPLTxnParserError(#[from] XRPLTxnParserException),
#[error("XRPL XChain Claim ID error: {0}")]
XRPLXChainClaimIdError(#[from] XRPLXChainClaimIdException),
#[error("ISO Code error: {0}")]
ISOCodeError(#[from] ISOCodeException),
#[error("Decimal error: {0}")]
Expand All @@ -35,6 +39,8 @@ pub enum XRPLUtilsException {
FromHexError(#[from] hex::FromHexError),
#[error("ParseInt error: {0}")]
ParseIntError(#[from] core::num::ParseIntError),
#[error("Invalid UTF-8")]
Utf8Error,
}

#[derive(Debug, Clone, PartialEq, Error)]
Expand Down Expand Up @@ -80,6 +86,26 @@ pub enum XRPLNFTIdException {
InvalidNFTIdLength { expected: usize, found: usize },
}

#[derive(Debug, Clone, PartialEq, Error)]
#[non_exhaustive]
pub enum XRPLXChainClaimIdException {
#[error("No XChainOwnedClaimID created")]
NoXChainOwnedClaimID,
}

#[cfg(feature = "std")]
impl alloc::error::Error for XRPLXChainClaimIdException {}

#[derive(Debug, Clone, PartialEq, Error)]
#[non_exhaustive]
pub enum XRPLTxnParserException {
#[error("Missing field: {0}")]
MissingField(&'static str),
}

#[cfg(feature = "std")]
impl alloc::error::Error for XRPLTxnParserException {}

#[derive(Debug, Clone, PartialEq, Error)]
#[non_exhaustive]
pub enum ISOCodeException {
Expand All @@ -91,13 +117,11 @@ pub enum ISOCodeException {
InvalidXRPBytes,
#[error("Invalid Currency representation")]
UnsupportedCurrencyRepresentation,
#[error("Invalid UTF-8")]
Utf8Error,
}

impl From<core::str::Utf8Error> for ISOCodeException {
impl From<core::str::Utf8Error> for XRPLUtilsException {
fn from(_: core::str::Utf8Error) -> Self {
ISOCodeException::Utf8Error
XRPLUtilsException::Utf8Error
}
}

Expand Down
45 changes: 45 additions & 0 deletions src/utils/get_xchain_claim_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use alloc::{borrow::Cow, vec::Vec};

use crate::models::{
ledger::objects::LedgerEntryType, transactions::metadata::TransactionMetadata,
};

use super::exceptions::{XRPLUtilsException, XRPLUtilsResult, XRPLXChainClaimIdException};
use crate::models::transactions::metadata::AffectedNode;

pub fn get_xchain_claim_id<'a: 'b, 'b>(
meta: &TransactionMetadata<'a>,
) -> XRPLUtilsResult<Cow<'b, str>> {
let affected_nodes: Vec<&AffectedNode> = meta
.affected_nodes
.iter()
.filter(|node| {
// node.is_created_node() && node.created_node().ledger_entry_type == "XChainOwnedClaimID"
match node {
AffectedNode::CreatedNode {
ledger_entry_type, ..
} => ledger_entry_type == &LedgerEntryType::XChainOwnedClaimID,
_ => false,
}
})
.collect();

if affected_nodes.is_empty() {
Err(XRPLXChainClaimIdException::NoXChainOwnedClaimID.into())
} else {
match affected_nodes[0] {
AffectedNode::CreatedNode { new_fields, .. } => {
if let Some(xchain_claim_id) = new_fields.xchain_claim_id.as_ref() {
Ok(xchain_claim_id.clone())
} else {
Err(XRPLUtilsException::XRPLXChainClaimIdError(
XRPLXChainClaimIdException::NoXChainOwnedClaimID,
))
}
}
_ => Err(XRPLUtilsException::XRPLXChainClaimIdError(
XRPLXChainClaimIdException::NoXChainOwnedClaimID,
)),
}
}
}
5 changes: 5 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ pub mod exceptions;
#[cfg(feature = "models")]
pub mod get_nftoken_id;
#[cfg(feature = "models")]
pub mod get_xchain_claim_id;
#[cfg(feature = "models")]
pub mod parse_nftoken_id;
pub mod str_conversion;
pub mod time_conversion;
#[cfg(feature = "models")]
pub(crate) mod transactions;
#[cfg(feature = "models")]
pub mod txn_parser;
pub mod xrpl_conversion;

pub use self::time_conversion::*;
Expand Down
22 changes: 22 additions & 0 deletions src/utils/str_conversion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use alloc::{borrow::Cow, string::String};

use super::exceptions::XRPLUtilsResult;

/// Convert a UTF-8-encoded string into hexadecimal encoding.
/// XRPL uses hex strings as inputs in fields like `domain`
/// in the `AccountSet` transaction.
pub fn str_to_hex<'a: 'b, 'b>(value: Cow<'a, str>) -> XRPLUtilsResult<Cow<'b, str>> {
let hex_string = hex::encode(value.as_bytes());

Ok(Cow::Owned(hex_string))
}

/// Convert a hex string into a human-readable string.
/// XRPL uses hex strings as inputs in fields like `domain`
/// in the `AccountSet` transaction.
pub fn hex_to_str<'a: 'b, 'b>(value: Cow<'a, str>) -> XRPLUtilsResult<Cow<'b, str>> {
let bytes = hex::decode(value.as_ref())?;
let string = String::from_utf8(bytes).map_err(|e| e.utf8_error())?;

Ok(Cow::Owned(string))
}
112 changes: 112 additions & 0 deletions src/utils/txn_parser/get_balance_changes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use alloc::vec::Vec;
use bigdecimal::BigDecimal;

use crate::{
models::transactions::metadata::TransactionMetadata,
utils::{exceptions::XRPLUtilsResult, txn_parser::utils::parser::get_value},
};

use super::utils::{
balance_parser::derive_account_balances, nodes::NormalizedNode, AccountBalances,
};

/// Parses the balance changes of all accounts affected by a transaction from the transaction metadata.
pub fn get_balance_changes<'a: 'b, 'b>(
meta: &'a TransactionMetadata<'a>,
) -> XRPLUtilsResult<Vec<AccountBalances<'b>>> {
derive_account_balances(meta, compute_balance_change)
}

/// Get the balance change from a node.
fn compute_balance_change(node: &NormalizedNode) -> XRPLUtilsResult<Option<BigDecimal>> {
let new_fields = node.new_fields.as_ref();
let previous_fields = node.previous_fields.as_ref();
let final_fields = node.final_fields.as_ref();

if let Some(new_fields) = new_fields {
if let Some(balance) = &new_fields.balance {
Ok(Some(get_value(&balance.clone().into())?))
} else {
Ok(None)
}
} else if let (Some(previous_fields), Some(final_fields)) = (previous_fields, final_fields) {
if let (Some(prev_balance), Some(final_balance)) =
(&previous_fields.balance, &final_fields.balance)
{
let prev_value = get_value(&prev_balance.clone().into())?;
let final_value = get_value(&final_balance.clone().into())?;

Ok(Some(final_value - prev_value))
} else {
Ok(None)
}
} else {
Ok(None)
}
}

#[cfg(test)]
mod test {
use core::cell::LazyCell;

use serde_json::Value;

use super::*;
use crate::{
models::transactions::metadata::TransactionMetadata, utils::txn_parser::utils::Balance,
};

#[test]
fn test_parse_balance_changes() {
let txn: LazyCell<TransactionMetadata> = LazyCell::new(|| {
let txn_value: Value =
serde_json::from_str(include_str!("./test_data/payment_iou.json")).unwrap();
let txn_meta = txn_value["meta"].clone();
let txn_meta: TransactionMetadata = serde_json::from_value(txn_meta).unwrap();

txn_meta
});
let expected_balances = Vec::from([
AccountBalances {
account: "rKmBGxocj9Abgy25J51Mk1iqFzW9aVF9Tc".into(),
balances: Vec::from([
Balance {
currency: "USD".into(),
value: "-0.01".into(),
issuer: Some("rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q".into()),
},
Balance {
currency: "XRP".into(),
value: "-0.012".into(),
issuer: None,
},
]),
},
AccountBalances {
account: "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q".into(),
balances: Vec::from([
Balance {
currency: "USD".into(),
value: "0.01".into(),
issuer: Some("rKmBGxocj9Abgy25J51Mk1iqFzW9aVF9Tc".into()),
},
Balance {
currency: "USD".into(),
value: "-0.01".into(),
issuer: Some("rLDYrujdKUfVx28T9vRDAbyJ7G2WVXKo4K".into()),
},
]),
},
AccountBalances {
account: "rLDYrujdKUfVx28T9vRDAbyJ7G2WVXKo4K".into(),
balances: Vec::from([Balance {
currency: "USD".into(),
value: "0.01".into(),
issuer: Some("rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q".into()),
}]),
},
]);
let balance_changes = get_balance_changes(&txn).unwrap();
assert_eq!(expected_balances, balance_changes);
}
}
Loading
Loading