diff --git a/src/_serde/mod.rs b/src/_serde/mod.rs index 3b314bfe..3a481b88 100644 --- a/src/_serde/mod.rs +++ b/src/_serde/mod.rs @@ -192,7 +192,7 @@ macro_rules! serde_with_tag { )* } - let hash_map: $crate::_serde::HashMap<&$lt str, Helper<$lt>> = $crate::_serde::HashMap::deserialize(deserializer)?; + let hash_map: $crate::_serde::HashMap> = $crate::_serde::HashMap::deserialize(deserializer)?; let helper_result = hash_map.get(stringify!($name)); match helper_result { @@ -269,7 +269,7 @@ macro_rules! serde_with_tag { )* } - let hash_map: $crate::_serde::HashMap<&'de str, Helper> = $crate::_serde::HashMap::deserialize(deserializer)?; + let hash_map: $crate::_serde::HashMap = $crate::_serde::HashMap::deserialize(deserializer)?; let helper_result = hash_map.get(stringify!($name)); match helper_result { diff --git a/src/models/requests/mod.rs b/src/models/requests/mod.rs index 3493a183..9fe2c9bb 100644 --- a/src/models/requests/mod.rs +++ b/src/models/requests/mod.rs @@ -62,6 +62,7 @@ pub enum RequestMethod { #[serde(rename = "amm_info")] AMMInfo, GatewayBalances, + #[serde(rename = "noripple_check")] NoRippleCheck, // Transaction methods diff --git a/src/models/requests/no_ripple_check.rs b/src/models/requests/no_ripple_check.rs index 854d89de..e23d1009 100644 --- a/src/models/requests/no_ripple_check.rs +++ b/src/models/requests/no_ripple_check.rs @@ -12,7 +12,6 @@ use super::{CommonFields, LedgerIndex, LookupByLedgerRequest, Request}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display)] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] -#[serde(tag = "role")] #[derive(Default)] pub enum NoRippleCheckRole { #[default] diff --git a/src/models/requests/ripple_path_find.rs b/src/models/requests/ripple_path_find.rs index 1ac23f70..c12f7b9f 100644 --- a/src/models/requests/ripple_path_find.rs +++ b/src/models/requests/ripple_path_find.rs @@ -3,7 +3,7 @@ use alloc::vec::Vec; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; -use crate::models::{currency::Currency, requests::RequestMethod, Model}; +use crate::models::{amount::Amount, currency::Currency, requests::RequestMethod, Model}; use super::{CommonFields, LedgerIndex, LookupByLedgerRequest, Request}; @@ -38,7 +38,7 @@ pub struct RipplePathFind<'a> { /// of the value field (for non-XRP currencies). This requests a /// path to deliver as much as possible, while spending no more /// than the amount specified in send_max (if provided). - pub destination_amount: Currency<'a>, + pub destination_amount: Amount<'a>, /// Unique address of the account that would send funds /// in a transaction. pub source_account: Cow<'a, str>, @@ -74,7 +74,7 @@ impl<'a> RipplePathFind<'a> { pub fn new( id: Option>, destination_account: Cow<'a, str>, - destination_amount: Currency<'a>, + destination_amount: Amount<'a>, source_account: Cow<'a, str>, ledger_hash: Option>, ledger_index: Option>, diff --git a/src/models/results/account_tx.rs b/src/models/results/account_tx.rs index 35d57834..065778f7 100644 --- a/src/models/results/account_tx.rs +++ b/src/models/results/account_tx.rs @@ -77,10 +77,12 @@ pub struct AccountTxV1<'a> { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct TransactionBase<'a> { /// The ledger index of the ledger version that included this transaction. - pub ledger_index: u32, + /// In API v1 this field is inside the `tx` object rather than at the + /// transaction-entry level, so it may be absent here. + pub ledger_index: Option, /// Whether or not the transaction is included in a validated ledger. Any /// transaction not yet in a validated ledger is subject to change. - pub validated: bool, + pub validated: Option, /// (Binary mode) A unique hex string defining the transaction. pub tx_blob: Option>, } @@ -103,8 +105,10 @@ pub struct AccountTxTransaction<'a> { pub struct AccountTxTransactionV1<'a> { #[serde(flatten)] pub base: TransactionBase<'a>, - /// (Binary mode) A hex string of the transaction in binary format. - pub tx: Cow<'a, str>, + /// (JSON mode) JSON object defining the transaction (API v1). + pub tx: Option, + /// (JSON mode) The transaction results metadata in JSON. + pub meta: Option>, } impl<'a> TryFrom> for AccountTxVersionMap<'a> { @@ -113,6 +117,9 @@ impl<'a> TryFrom> for AccountTxVersionMap<'a> { fn try_from(result: XRPLResult<'a>) -> XRPLModelResult { match result { XRPLResult::AccountTx(account_tx) => Ok(account_tx), + XRPLResult::Other(super::XRPLOtherResult(ref value)) => { + serde_json::from_value(value.clone()).map_err(Into::into) + } res => Err(XRPLResultException::UnexpectedResultType( "AccountTx".to_string(), res.get_name(), diff --git a/src/models/results/ledger_entry.rs b/src/models/results/ledger_entry.rs index 5f38fddf..55318049 100644 --- a/src/models/results/ledger_entry.rs +++ b/src/models/results/ledger_entry.rs @@ -1,61 +1,105 @@ use alloc::borrow::Cow; use serde::{Deserialize, Serialize}; +use serde_json::Value; -/// Represents an AccountRoot ledger object in the XRP Ledger. -/// This object type represents a single account, its settings, and XRP balance. +use crate::models::ledger::objects::{ + account_root::AccountRoot, amendments::Amendments, amm::AMM, bridge::Bridge, check::Check, + deposit_preauth::DepositPreauth, directory_node::DirectoryNode, escrow::Escrow, + fee_settings::FeeSettings, ledger_hashes::LedgerHashes, negative_unl::NegativeUNL, + nftoken_offer::NFTokenOffer, nftoken_page::NFTokenPage, offer::Offer, pay_channel::PayChannel, + ripple_state::RippleState, signer_list::SignerList, ticket::Ticket, + xchain_owned_claim_id::XChainOwnedClaimID, + xchain_owned_create_account_claim_id::XChainOwnedCreateAccountClaimID, +}; + +/// A discriminated union representing any ledger object type that can be +/// returned by the `ledger_entry` method. Dispatches on the `LedgerEntryType` +/// field, mirroring the `LedgerEntry` union type in xrpl.js. /// -/// See AccountRoot: -/// `` -#[serde_with::skip_serializing_none] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "PascalCase")] -pub struct Node<'a> { - /// The identifying address of this account - pub account: Cow<'a, str>, - /// The identifying hash of the transaction that most recently modified - /// this object - #[serde(rename = "AccountTxnID")] - pub account_txn_id: Cow<'a, str>, - /// The account's current XRP balance in drops - pub balance: Cow<'a, str>, - /// The domain associated with this account. The raw domain value is a - /// hex string representing the ASCII for the domain - pub domain: Option>, - /// Hash of an email address to be used for generating an avatar image - pub email_hash: Option>, - /// Various boolean flags enabled for this account - pub flags: u32, - /// The type of ledger object. For AccountRoot objects, this is always - /// "AccountRoot" - pub ledger_entry_type: Cow<'a, str>, - /// Public key for sending encrypted messages to this account - pub message_key: Option>, - /// Number of objects this account owns in the ledger, which contributes - /// to its owner reserve - pub owner_count: u32, - /// Identifying hash of the previous transaction that modified this object - #[serde(rename = "PreviousTxnID")] - pub previous_txn_id: Cow<'a, str>, - /// Ledger index of the ledger containing the previous transaction that - /// modified this object - pub previous_txn_lgr_seq: u32, - /// The identifying address of a key pair that can be used to authorize - /// transactions for this account instead of the master key - pub regular_key: Option>, - /// The sequence number of the next valid transaction for this account - pub sequence: u32, - /// The rate to charge when users transfer this account's issued currencies, - /// represented as billionths of a unit. A value of 0 means no fee - pub transfer_rate: Option, - /// The unique ID of this ledger entry - #[serde(rename = "index")] - pub index: Cow<'a, str>, +/// Each variant wraps the corresponding ledger object struct from +/// `crate::models::ledger::objects`. The `Unknown` variant handles any +/// ledger entry types not yet modeled in the library. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum LedgerEntryNode<'a> { + AccountRoot(AccountRoot<'a>), + Amendments(Amendments<'a>), + AMM(AMM<'a>), + Bridge(Bridge<'a>), + Check(Check<'a>), + DepositPreauth(DepositPreauth<'a>), + DirectoryNode(DirectoryNode<'a>), + Escrow(Escrow<'a>), + FeeSettings(FeeSettings<'a>), + LedgerHashes(LedgerHashes<'a>), + NegativeUNL(NegativeUNL<'a>), + NFTokenOffer(NFTokenOffer<'a>), + NFTokenPage(NFTokenPage<'a>), + Offer(Offer<'a>), + PayChannel(PayChannel<'a>), + RippleState(RippleState<'a>), + SignerList(SignerList<'a>), + Ticket(Ticket<'a>), + XChainOwnedClaimID(XChainOwnedClaimID<'a>), + XChainOwnedCreateAccountClaimID(XChainOwnedCreateAccountClaimID<'a>), + /// Fallback for unknown or new ledger entry types not yet modeled. + Unknown(Value), +} + +/// Custom deserializer that reads `LedgerEntryType` to dispatch to the +/// correct variant, avoiding the serde limitation where internally tagged +/// enums strip the tag from flattened content. +impl<'de, 'a> Deserialize<'de> for LedgerEntryNode<'a> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + let entry_type = value + .get("LedgerEntryType") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let result = match entry_type { + "AccountRoot" => serde_json::from_value(value.clone()).map(Self::AccountRoot), + "Amendments" => serde_json::from_value(value.clone()).map(Self::Amendments), + "AMM" => serde_json::from_value(value.clone()).map(Self::AMM), + "Bridge" => serde_json::from_value(value.clone()).map(Self::Bridge), + "Check" => serde_json::from_value(value.clone()).map(Self::Check), + "DepositPreauth" => serde_json::from_value(value.clone()).map(Self::DepositPreauth), + "DirectoryNode" => serde_json::from_value(value.clone()).map(Self::DirectoryNode), + "Escrow" => serde_json::from_value(value.clone()).map(Self::Escrow), + "FeeSettings" => serde_json::from_value(value.clone()).map(Self::FeeSettings), + "LedgerHashes" => serde_json::from_value(value.clone()).map(Self::LedgerHashes), + "NegativeUNL" => serde_json::from_value(value.clone()).map(Self::NegativeUNL), + "NFTokenOffer" => serde_json::from_value(value.clone()).map(Self::NFTokenOffer), + "NFTokenPage" => serde_json::from_value(value.clone()).map(Self::NFTokenPage), + "Offer" => serde_json::from_value(value.clone()).map(Self::Offer), + "PayChannel" => serde_json::from_value(value.clone()).map(Self::PayChannel), + "RippleState" => serde_json::from_value(value.clone()).map(Self::RippleState), + "SignerList" => serde_json::from_value(value.clone()).map(Self::SignerList), + "Ticket" => serde_json::from_value(value.clone()).map(Self::Ticket), + "XChainOwnedClaimID" => { + serde_json::from_value(value.clone()).map(Self::XChainOwnedClaimID) + } + "XChainOwnedCreateAccountClaimID" => { + serde_json::from_value(value.clone()).map(Self::XChainOwnedCreateAccountClaimID) + } + _ => return Ok(Self::Unknown(value)), + }; + + result.map_err(serde::de::Error::custom) + } } /// Response format for the ledger_entry method, which returns a single ledger /// object from the XRP Ledger in its raw format. /// +/// The `node` field is a typed enum (`LedgerEntryNode`) that can represent any +/// ledger object type (AccountRoot, DirectoryNode, Offer, RippleState, etc.), +/// mirroring the `LedgerEntry` union type in xrpl.js. +/// /// See Ledger Entry: /// `` #[serde_with::skip_serializing_none] @@ -68,8 +112,9 @@ pub struct LedgerEntry<'a> { /// The identifying hash of the ledger version used to retrieve this data pub ledger_hash: Option>, /// Object containing the data of this ledger entry, according to the - /// ledger format. Omitted if "binary": true specified. - pub node: Option>, + /// ledger format. Can be any ledger object type (AccountRoot, + /// DirectoryNode, Offer, etc.). Omitted if "binary": true specified. + pub node: Option>, /// The binary representation of the ledger object, as hexadecimal. /// Only present if "binary": true specified. pub node_binary: Option>, @@ -124,38 +169,16 @@ mod tests { assert_eq!(result.validated, Some(true)); let node = result.node.unwrap(); - assert_eq!(node.account, "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"); - assert_eq!( - node.account_txn_id, - "4E0AA11CBDD1760DE95B68DF2ABBE75C9698CEB548BEA9789053FCB3EBD444FB" - ); - assert_eq!(node.balance, "424021949"); - assert_eq!(node.domain, Some("6D64756F31332E636F6D".into())); - assert_eq!( - node.email_hash, - Some("98B4375E1D753E5B91627516F6D70977".into()) - ); - assert_eq!(node.flags, 9568256); - assert_eq!(node.ledger_entry_type, "AccountRoot"); - assert_eq!( - node.message_key, - Some("0000000000000000000000070000000300".into()) - ); - assert_eq!(node.owner_count, 12); - assert_eq!( - node.previous_txn_id, - "4E0AA11CBDD1760DE95B68DF2ABBE75C9698CEB548BEA9789053FCB3EBD444FB" - ); - assert_eq!(node.previous_txn_lgr_seq, 61965653); - assert_eq!( - node.regular_key, - Some("rD9iJmieYHn8jTtPjwwkW2Wm9sVDvPXLoJ".into()) - ); - assert_eq!(node.sequence, 385); - assert_eq!(node.transfer_rate, Some(4294967295)); - assert_eq!( - node.index, - "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8" - ); + match node { + LedgerEntryNode::AccountRoot(account_root) => { + assert_eq!(account_root.account, "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"); + assert_eq!( + account_root.account_txn_id, + Some("4E0AA11CBDD1760DE95B68DF2ABBE75C9698CEB548BEA9789053FCB3EBD444FB".into()) + ); + assert_eq!(account_root.sequence, 385); + } + _ => panic!("Expected AccountRoot variant"), + } } } diff --git a/src/models/results/mod.rs b/src/models/results/mod.rs index 6d06dc5f..9e4a6715 100644 --- a/src/models/results/mod.rs +++ b/src/models/results/mod.rs @@ -279,8 +279,40 @@ impl_try_from_result!(gateway_balances, GatewayBalances, GatewayBalances); impl_try_from_result!(ledger, Ledger, Ledger); impl_try_from_result!(ledger_closed, LedgerClosed, LedgerClosed); impl_try_from_result!(ledger_current, LedgerCurrent, LedgerCurrent); -impl_try_from_result!(ledger_data, LedgerData, LedgerData); -impl_try_from_result!(ledger_entry, LedgerEntry, LedgerEntry); +// LedgerData: serde's untagged enum may match LedgerClosed (which has fewer +// required fields) before reaching LedgerData. Re-serialize and re-parse +// to recover the full data. +impl<'a> TryFrom> for ledger_data::LedgerData<'a> { + type Error = XRPLModelException; + fn try_from(result: XRPLResult<'a>) -> XRPLModelResult { + match result { + XRPLResult::LedgerData(value) => Ok(value), + XRPLResult::Other(XRPLOtherResult(ref value)) => { + serde_json::from_value(value.clone()).map_err(Into::into) + } + other => { + let value = serde_json::to_value(&other)?; + serde_json::from_value(value).map_err(Into::into) + } + } + } +} +// LedgerEntry: may match Ledger or LedgerClosed before reaching LedgerEntry. +impl<'a> TryFrom> for ledger_entry::LedgerEntry<'a> { + type Error = XRPLModelException; + fn try_from(result: XRPLResult<'a>) -> XRPLModelResult { + match result { + XRPLResult::LedgerEntry(value) => Ok(value), + XRPLResult::Other(XRPLOtherResult(ref value)) => { + serde_json::from_value(value.clone()).map_err(Into::into) + } + other => { + let value = serde_json::to_value(&other)?; + serde_json::from_value(value).map_err(Into::into) + } + } + } +} impl_try_from_result!(manifest, Manifest, Manifest); // NFTBuyOffers and NFTSellOffers are structurally identical; the untagged enum // always picks the first matching variant (NFTBuyOffers). Both TryFrom impls @@ -325,16 +357,49 @@ impl<'a> TryFrom> for nft_sell_offers::NFTSellOffers<'a> { } } impl_try_from_result!(nftoken, NFTokenMintResult, NFTokenMintResult); -impl_try_from_result!(no_ripple_check, NoRippleCheck, NoRippleCheck); +// NoRippleCheck: may match Other(Value) due to untagged enum ordering. +impl<'a> TryFrom> for no_ripple_check::NoRippleCheck<'a> { + type Error = XRPLModelException; + fn try_from(result: XRPLResult<'a>) -> XRPLModelResult { + match result { + XRPLResult::NoRippleCheck(value) => Ok(value), + XRPLResult::Other(XRPLOtherResult(ref value)) => { + serde_json::from_value(value.clone()).map_err(Into::into) + } + other => { + let value = serde_json::to_value(&other)?; + serde_json::from_value(value).map_err(Into::into) + } + } + } +} impl_try_from_result!(path_find, PathFind, PathFind); impl_try_from_result!(random, Random, Random); -impl_try_from_result!(ripple_path_find, RipplePathFind, RipplePathFind); +// RipplePathFind: may match LedgerCurrent due to untagged enum ordering. +impl<'a> TryFrom> for ripple_path_find::RipplePathFind<'a> { + type Error = XRPLModelException; + fn try_from(result: XRPLResult<'a>) -> XRPLModelResult { + match result { + XRPLResult::RipplePathFind(value) => Ok(value), + XRPLResult::Other(XRPLOtherResult(ref value)) => { + serde_json::from_value(value.clone()).map_err(Into::into) + } + other => { + let value = serde_json::to_value(&other)?; + serde_json::from_value(value).map_err(Into::into) + } + } + } +} impl<'a> TryFrom> for server_info::ServerInfo<'a> { type Error = XRPLModelException; fn try_from(result: XRPLResult<'a>) -> XRPLModelResult { match result { XRPLResult::ServerInfo(value) => Ok(*value), + XRPLResult::Other(XRPLOtherResult(ref value)) => { + serde_json::from_value(value.clone()).map_err(Into::into) + } res => Err(XRPLResultException::UnexpectedResultType( "ServerInfo".to_string(), res.get_name(), @@ -358,7 +423,22 @@ impl<'a> TryFrom> for server_state::ServerState<'a> { } } impl_try_from_result!(submit, Submit, Submit); -impl_try_from_result!(submit_multisigned, SubmitMultisigned, SubmitMultisigned); +// SubmitMultisigned: may match Submit due to untagged enum ordering. +impl<'a> TryFrom> for submit_multisigned::SubmitMultisigned<'a> { + type Error = XRPLModelException; + fn try_from(result: XRPLResult<'a>) -> XRPLModelResult { + match result { + XRPLResult::SubmitMultisigned(value) => Ok(value), + XRPLResult::Other(XRPLOtherResult(ref value)) => { + serde_json::from_value(value.clone()).map_err(Into::into) + } + other => { + let value = serde_json::to_value(&other)?; + serde_json::from_value(value).map_err(Into::into) + } + } + } +} impl_try_from_result!(transaction_entry, TransactionEntry, TransactionEntry); impl_try_from_result!(ping, Ping, Ping); impl_try_from_result!(subscribe, Subscribe, Subscribe); @@ -445,6 +525,10 @@ pub struct XRPLResponse<'a> { pub forwarded: Option, pub request: Option>, pub result: Option>, + /// Raw JSON of the `result` field, preserved for fallback re-deserialization + /// when the untagged `XRPLResult` enum matches the wrong variant. + #[serde(skip)] + pub raw_result: Option, pub status: Option, pub r#type: Option, pub warning: Option>, @@ -517,8 +601,41 @@ impl_try_from_response!(gateway_balances, GatewayBalances, GatewayBalances); impl_try_from_response!(ledger, Ledger, Ledger); impl_try_from_response!(ledger_closed, LedgerClosed, LedgerClosed); impl_try_from_response!(ledger_current, LedgerCurrent, LedgerCurrent); -impl_try_from_response!(ledger_data, LedgerData, LedgerData); -impl_try_from_response!(ledger_entry, LedgerEntry, LedgerEntry); +// LedgerData / LedgerEntry: use raw_result fallback for untagged enum +// mismatch where serde picks a wrong variant and loses fields. +impl<'a, 'b> TryFrom> for ledger_data::LedgerData<'b> +where + 'a: 'b, + 'b: 'a, +{ + type Error = XRPLModelException; + fn try_from(response: XRPLResponse<'a>) -> XRPLModelResult { + if let Some(XRPLResult::LedgerData(value)) = response.result { + return Ok(value); + } + // Fallback: re-deserialize from the raw result JSON + match response.raw_result { + Some(raw) => serde_json::from_value(raw).map_err(Into::into), + None => Err(XRPLModelException::MissingField("result".to_string())), + } + } +} +impl<'a, 'b> TryFrom> for ledger_entry::LedgerEntry<'b> +where + 'a: 'b, + 'b: 'a, +{ + type Error = XRPLModelException; + fn try_from(response: XRPLResponse<'a>) -> XRPLModelResult { + if let Some(XRPLResult::LedgerEntry(value)) = response.result { + return Ok(value); + } + match response.raw_result { + Some(raw) => serde_json::from_value(raw).map_err(Into::into), + None => Err(XRPLModelException::MissingField("result".to_string())), + } + } +} impl_try_from_response!(manifest, Manifest, Manifest); impl_try_from_response!(nft_info, NFTInfo, NFTInfo); // NFTBuyOffers and NFTSellOffers are structurally identical; the untagged enum @@ -576,11 +693,43 @@ where } } impl_try_from_response!(nftoken, NFTokenMintResult, NFTokenMintResult); -impl_try_from_response!(no_ripple_check, NoRippleCheck, NoRippleCheck); +// NoRippleCheck / RipplePathFind: use raw_result fallback for untagged +// enum mismatch. +impl<'a, 'b> TryFrom> for no_ripple_check::NoRippleCheck<'b> +where + 'a: 'b, + 'b: 'a, +{ + type Error = XRPLModelException; + fn try_from(response: XRPLResponse<'a>) -> XRPLModelResult { + if let Some(XRPLResult::NoRippleCheck(value)) = response.result { + return Ok(value); + } + match response.raw_result { + Some(raw) => serde_json::from_value(raw).map_err(Into::into), + None => Err(XRPLModelException::MissingField("result".to_string())), + } + } +} impl_try_from_response!(path_find, PathFind, PathFind); impl_try_from_response!(ping, Ping, Ping); impl_try_from_response!(random, Random, Random); -impl_try_from_response!(ripple_path_find, RipplePathFind, RipplePathFind); +impl<'a, 'b> TryFrom> for ripple_path_find::RipplePathFind<'b> +where + 'a: 'b, + 'b: 'a, +{ + type Error = XRPLModelException; + fn try_from(response: XRPLResponse<'a>) -> XRPLModelResult { + if let Some(XRPLResult::RipplePathFind(value)) = response.result { + return Ok(value); + } + match response.raw_result { + Some(raw) => serde_json::from_value(raw).map_err(Into::into), + None => Err(XRPLModelException::MissingField("result".to_string())), + } + } +} impl<'a> TryFrom> for server_info::ServerInfo<'a> { type Error = XRPLModelException; @@ -588,6 +737,9 @@ impl<'a> TryFrom> for server_info::ServerInfo<'a> { match response.result { Some(result) => match result { XRPLResult::ServerInfo(value) => Ok(*value), + XRPLResult::Other(XRPLOtherResult(ref value)) => { + serde_json::from_value(value.clone()).map_err(Into::into) + } res => Err(XRPLResultException::UnexpectedResultType( "ServerInfo".to_string(), res.get_name(), @@ -616,7 +768,23 @@ impl<'a> TryFrom> for server_state::ServerState<'a> { } } impl_try_from_response!(submit, Submit, Submit); -impl_try_from_response!(submit_multisigned, SubmitMultisigned, SubmitMultisigned); +// SubmitMultisigned: may match Submit due to untagged enum ordering. +impl<'a, 'b> TryFrom> for submit_multisigned::SubmitMultisigned<'b> +where + 'a: 'b, + 'b: 'a, +{ + type Error = XRPLModelException; + fn try_from(response: XRPLResponse<'a>) -> XRPLModelResult { + if let Some(XRPLResult::SubmitMultisigned(value)) = response.result { + return Ok(value); + } + match response.raw_result { + Some(raw) => serde_json::from_value(raw).map_err(Into::into), + None => Err(XRPLModelException::MissingField("result".to_string())), + } + } +} impl_try_from_response!(transaction_entry, TransactionEntry, TransactionEntry); impl_try_from_response!(subscribe, Subscribe, Subscribe); impl_try_from_response!(unsubscribe, Unsubscribe, Unsubscribe); @@ -645,12 +813,19 @@ impl<'a, 'de> Deserialize<'de> for XRPLResponse<'a> { forwarded: None, request: None, result: serde_json::from_value(map_as_value).map_err(serde::de::Error::custom)?, + raw_result: None, status: None, r#type: None, warning: None, warnings: None, }) } else { + // Preserve the raw result JSON so that TryFrom impls can + // re-deserialize when the untagged enum picks the wrong variant. + let raw_result = map.remove("result"); + let result = raw_result + .as_ref() + .and_then(|v| serde_json::from_value(v.clone()).ok()); Ok(XRPLResponse { id: map .remove("id") @@ -668,9 +843,8 @@ impl<'a, 'de> Deserialize<'de> for XRPLResponse<'a> { request: map .remove("request") .and_then(|v| serde_json::from_value(v).ok()), - result: map - .remove("result") - .and_then(|v| serde_json::from_value(v).ok()), + result, + raw_result, status: map .remove("status") .and_then(|v| serde_json::from_value(v).ok()), diff --git a/src/models/results/server_info.rs b/src/models/results/server_info.rs index 2c734951..4338b9a4 100644 --- a/src/models/results/server_info.rs +++ b/src/models/results/server_info.rs @@ -82,18 +82,31 @@ pub struct Info<'a> { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct ValidatorList<'a> { pub count: u32, - pub expiration: u32, + /// Expiration can be a number or "unknown" when the validator list + /// status is unknown (e.g. standalone mode). + pub expiration: Value, pub status: Cow<'a, str>, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct LastClose { - /// Time to reach consensus in seconds - pub converge_time_s: u64, + /// Time to reach consensus in seconds. + /// Can be a fractional value (e.g. 0.1), so we use serde_json::Number + /// to handle both integer and float representations. + pub converge_time_s: serde_json::Number, /// Number of trusted validators considered pub proposers: u32, } +impl Default for LastClose { + fn default() -> Self { + Self { + converge_time_s: serde_json::Number::from(0), + proposers: 0, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct Load<'a> { /// Information about job types and time spent @@ -202,7 +215,10 @@ mod tests { assert_eq!(result.info.hostid, Some("LEST".into())); assert_eq!(result.info.io_latency_ms, 1); assert_eq!(result.info.jq_trans_overflow, Some("0".into())); - assert_eq!(result.info.last_close.converge_time_s, 3); + assert_eq!( + result.info.last_close.converge_time_s, + serde_json::Number::from(3) + ); assert_eq!(result.info.last_close.proposers, 35); assert_eq!(result.info.load_factor, 1); assert_eq!(result.info.network_id, Some(10)); diff --git a/tests/common/amm.rs b/tests/common/amm.rs index 7504ee67..7b3310b1 100644 --- a/tests/common/amm.rs +++ b/tests/common/amm.rs @@ -1,12 +1,11 @@ // Shared AMM pool setup helper used by all AMM integration tests. // -// Creates a minimal XRP/USD AMM pool (mirrors xrpl.js setupAMMPool): +// Creates a minimal XRP/USD AMM pool: // 1. issuerWallet: AccountSet — enable DefaultRipple so the IOU can flow // 2. lpWallet: TrustSet — trust issuer for up to 1000 USD (tfClearNoRipple) // 3. issuerWallet: Payment — send 500 USD to lpWallet // 4. lpWallet: AMMCreate — 250 XRP drops + 250 USD, trading_fee = 12 -// 5. lpWallet: AMMDeposit — 1000 XRP drops (TfSingleAsset), matching -// xrpl.js setupAMMPool which adds a testWallet deposit so +// 5. lpWallet: AMMDeposit — 1000 XRP drops (TfSingleAsset) so // pool has enough XRP for a 500-drop single-asset withdraw. // // The returned `AmmPool` carries both wallets so individual tests can build @@ -132,7 +131,7 @@ pub async fn setup_amm_pool() -> AmmPool { // Step 5: lp_wallet deposits 1000 XRP drops (TfSingleAsset) so pool has // enough XRP for a 500-drop single-asset withdraw in amm_withdraw tests. - // Mirrors xrpl.js setupAMMPool testWallet deposit. + // Adds liquidity so pool has enough XRP for single-asset withdraw tests. let mut deposit_tx = AMMDeposit::new( lp_wallet.classic_address.clone().into(), None, // account_txn_id diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 14093264..36c47b40 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -183,9 +183,8 @@ where } /// Look up the OfferSequence for the first escrow owned by `account`. -/// Mirrors xrpl.js: -/// const accountObjects = (await client.request({command:'account_objects', account})).result.account_objects -/// const sequence = (await client.request({command:'tx', transaction: accountObjects[0].PreviousTxnID})).result.tx_json.Sequence +/// Queries account_objects to find the escrow, then looks up its creating +/// transaction to extract the validated Sequence number. #[cfg(feature = "std")] pub async fn get_escrow_offer_sequence(account: &str) -> u32 { use xrpl::asynch::clients::XRPLAsyncClient; diff --git a/tests/integration_test.rs b/tests/integration_test.rs index c9ea7a1d..e87606a0 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -6,6 +6,14 @@ ))] mod common; +#[cfg(all( + feature = "integration", + feature = "std", + feature = "json-rpc", + feature = "helpers" +))] +mod requests; + #[cfg(all( feature = "integration", feature = "std", diff --git a/tests/requests/account_channels.rs b/tests/requests/account_channels.rs new file mode 100644 index 00000000..a7f75896 --- /dev/null +++ b/tests/requests/account_channels.rs @@ -0,0 +1,48 @@ +// Scenarios: +// - base: send an account_channels request for a funded wallet and verify +// the response returns an empty channels list (no channels created) + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::account_channels::AccountChannels; +use xrpl::models::requests::LedgerIndex; +use xrpl::models::results::account_channels::AccountChannels as AccountChannelsResult; + +#[tokio::test] +async fn test_account_channels_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + + let request = AccountChannels::new( + None, // id + wallet.classic_address.clone().into(), // account + None, // destination_account + None, // ledger_hash + Some(LedgerIndex::Str("validated".into())), // ledger_index + None, // limit + None, // marker + ); + + let response = client + .request(request.into()) + .await + .expect("account_channels request failed"); + + let result: AccountChannelsResult = response + .try_into() + .expect("failed to parse account_channels result"); + + // Verify account matches + assert_eq!(result.account.as_ref(), wallet.classic_address.as_str()); + // Verify channels is empty (no channels created) + assert!(result.channels.is_empty()); + // Verify validated + assert!(result.validated); + // Verify ledger_hash exists + assert!(result.ledger_hash.is_some()); + // Verify ledger_index is valid + assert!(result.ledger_index > 0); + }) + .await; +} diff --git a/tests/requests/account_currencies.rs b/tests/requests/account_currencies.rs new file mode 100644 index 00000000..7a9ae879 --- /dev/null +++ b/tests/requests/account_currencies.rs @@ -0,0 +1,45 @@ +// Scenarios: +// - base: send an account_currencies request for a funded wallet and verify +// the response returns empty currency lists (no trust lines created) + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::account_currencies::AccountCurrencies; +use xrpl::models::requests::LedgerIndex; +use xrpl::models::results::account_currencies::AccountCurrencies as AccountCurrenciesResult; + +#[tokio::test] +async fn test_account_currencies_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + + let request = AccountCurrencies::new( + None, // id + wallet.classic_address.clone().into(), // account + None, // ledger_hash + Some(LedgerIndex::Str("validated".into())), // ledger_index + Some(true), // strict + ); + + let response = client + .request(request.into()) + .await + .expect("account_currencies request failed"); + + let result: AccountCurrenciesResult = response + .try_into() + .expect("failed to parse account_currencies result"); + + // Verify currencies are empty (no trust lines) + assert!(result.receive_currencies.is_empty()); + assert!(result.send_currencies.is_empty()); + // Verify validated + assert!(result.validated); + // Verify ledger_hash exists + assert!(result.ledger_hash.is_some()); + // Verify ledger_index is valid + assert!(result.ledger_index > 0); + }) + .await; +} diff --git a/tests/requests/account_info.rs b/tests/requests/account_info.rs new file mode 100644 index 00000000..dc858513 --- /dev/null +++ b/tests/requests/account_info.rs @@ -0,0 +1,55 @@ +// Scenarios: +// - base: send an account_info request for a funded wallet and verify the response + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::account_info::AccountInfo; +use xrpl::models::requests::LedgerIndex; +use xrpl::models::results::account_info::AccountInfoVersionMap; + +#[tokio::test] +async fn test_account_info_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + + let request = AccountInfo::new( + None, // id + wallet.classic_address.clone().into(), // account + None, // ledger_hash + Some(LedgerIndex::Str("validated".into())), // ledger_index + Some(true), // strict + None, // queue + None, // signer_lists + ); + + let response = client + .request(request.into()) + .await + .expect("account_info request failed"); + + let result: AccountInfoVersionMap = response + .try_into() + .expect("failed to parse account_info result"); + + let account_data = result.get_account_root(); + + // Verify account matches + assert_eq!( + account_data.account.as_ref(), + wallet.classic_address.as_str() + ); + // Verify balance is 400 XRP (400000000 drops) + assert_eq!( + account_data.balance.as_ref().map(|b| b.0.as_ref()), + Some("400000000") + ); + // Verify owner count + assert_eq!(account_data.owner_count, 0); + // Verify sequence is a valid number + assert!(account_data.sequence > 0); + // Verify PreviousTxnID exists + assert!(!account_data.previous_txn_id.is_empty()); + }) + .await; +} diff --git a/tests/requests/account_lines.rs b/tests/requests/account_lines.rs new file mode 100644 index 00000000..dce7a921 --- /dev/null +++ b/tests/requests/account_lines.rs @@ -0,0 +1,45 @@ +// Scenarios: +// - base: send an account_lines request for a funded wallet and verify +// the response returns an empty lines list (no trust lines created) + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::account_lines::AccountLines; +use xrpl::models::requests::LedgerIndex; +use xrpl::models::results::account_lines::AccountLines as AccountLinesResult; + +#[tokio::test] +async fn test_account_lines_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + + let request = AccountLines::new( + None, // id + wallet.classic_address.clone().into(), // account + None, // ledger_hash + Some(LedgerIndex::Str("validated".into())), // ledger_index + None, // limit + None, // peer + ); + + let response = client + .request(request.into()) + .await + .expect("account_lines request failed"); + + let result: AccountLinesResult = response + .try_into() + .expect("failed to parse account_lines result"); + + // Verify account matches + assert_eq!(result.account.as_ref(), wallet.classic_address.as_str()); + // Verify lines is empty (no trust lines created) + assert!(result.lines.is_empty()); + // Verify ledger_hash exists + assert!(result.ledger_hash.is_some()); + // Verify ledger_index is valid + assert!(result.ledger_index.unwrap() > 0); + }) + .await; +} diff --git a/tests/requests/account_objects.rs b/tests/requests/account_objects.rs new file mode 100644 index 00000000..9d1ce6ba --- /dev/null +++ b/tests/requests/account_objects.rs @@ -0,0 +1,47 @@ +// Scenarios: +// - base: send an account_objects request for a funded wallet and verify +// the response returns an empty account_objects list + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::account_objects::AccountObjects; +use xrpl::models::requests::LedgerIndex; +use xrpl::models::results::account_objects::AccountObjects as AccountObjectsResult; + +#[tokio::test] +async fn test_account_objects_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + + let request = AccountObjects::new( + None, // id + wallet.classic_address.clone().into(), // account + None, // ledger_hash + Some(LedgerIndex::Str("validated".into())), // ledger_index + None, // type + None, // deletion_blockers_only + None, // limit + None, // marker + ); + + let response = client + .request(request.into()) + .await + .expect("account_objects request failed"); + + let result: AccountObjectsResult = response + .try_into() + .expect("failed to parse account_objects result"); + + // Verify account matches + assert_eq!(result.account.as_ref(), wallet.classic_address.as_str()); + // Verify account_objects is empty + assert!(result.account_objects.is_empty()); + // Verify ledger_hash exists + assert!(result.ledger_hash.is_some()); + // Verify ledger_index is valid + assert!(result.ledger_index.unwrap() > 0); + }) + .await; +} diff --git a/tests/requests/account_offers.rs b/tests/requests/account_offers.rs new file mode 100644 index 00000000..9c5811a1 --- /dev/null +++ b/tests/requests/account_offers.rs @@ -0,0 +1,43 @@ +// Scenarios: +// - base: send an account_offers request for a funded wallet and verify +// the response returns an empty offers list + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::account_offers::AccountOffers; +use xrpl::models::results::account_offers::AccountOffers as AccountOffersResult; + +#[tokio::test] +async fn test_account_offers_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + + let request = AccountOffers::new( + None, // id + wallet.classic_address.clone().into(), // account + None, // ledger_hash + None, // ledger_index + None, // limit + Some(true), // strict + None, // marker + ); + + let response = client + .request(request.into()) + .await + .expect("account_offers request failed"); + + let result: AccountOffersResult = response + .try_into() + .expect("failed to parse account_offers result"); + + // Verify account matches + assert_eq!(result.account.as_ref(), wallet.classic_address.as_str()); + // Verify offers is empty + assert!(result.offers.is_empty()); + // Verify ledger_current_index exists (no specific ledger requested) + assert!(result.ledger_current_index.unwrap() > 0); + }) + .await; +} diff --git a/tests/requests/account_tx.rs b/tests/requests/account_tx.rs new file mode 100644 index 00000000..b695a135 --- /dev/null +++ b/tests/requests/account_tx.rs @@ -0,0 +1,82 @@ +// Scenarios: +// - base: send an account_tx request for a funded wallet and verify the +// response contains the funding transaction (Payment from genesis) + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::account_tx::AccountTx; +use xrpl::models::requests::LedgerIndex; +use xrpl::models::results::account_tx::AccountTxVersionMap; + +#[tokio::test] +async fn test_account_tx_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + + let request = AccountTx::new( + None, // id + wallet.classic_address.clone().into(), // account + None, // ledger_hash + Some(LedgerIndex::Str("validated".into())), // ledger_index + None, // binary + None, // forward + None, // ledger_index_min + None, // ledger_index_max + None, // limit + None, // marker + ); + + let response = client + .request(request.into()) + .await + .expect("account_tx request failed"); + + let result: AccountTxVersionMap = response + .try_into() + .expect("failed to parse account_tx result"); + + // Standalone rippled returns API v1 format by default (tx object + // instead of tx_json), so we handle both variants. + match result { + AccountTxVersionMap::Default(tx_result) => { + // API v2 format + assert_eq!( + tx_result.base.account.as_ref(), + wallet.classic_address.as_str() + ); + assert!( + !tx_result.base.transactions.is_empty(), + "Expected at least one transaction" + ); + let first_tx = &tx_result.base.transactions[0]; + assert!(!first_tx.hash.is_empty()); + let tx_json = first_tx.tx_json.as_ref().expect("tx_json should exist"); + assert_eq!(tx_json["TransactionType"].as_str().unwrap(), "Payment"); + assert_eq!( + tx_json["Destination"].as_str().unwrap(), + wallet.classic_address.as_str() + ); + } + AccountTxVersionMap::V1(tx_result) => { + // API v1 format (standalone node default) + assert_eq!( + tx_result.base.account.as_ref(), + wallet.classic_address.as_str() + ); + assert!( + !tx_result.base.transactions.is_empty(), + "Expected at least one transaction" + ); + let first_tx = &tx_result.base.transactions[0]; + let tx = first_tx.tx.as_ref().expect("tx should exist"); + assert_eq!(tx["TransactionType"].as_str().unwrap(), "Payment"); + assert_eq!( + tx["Destination"].as_str().unwrap(), + wallet.classic_address.as_str() + ); + } + } + }) + .await; +} diff --git a/tests/requests/amm_info.rs b/tests/requests/amm_info.rs new file mode 100644 index 00000000..579f920e --- /dev/null +++ b/tests/requests/amm_info.rs @@ -0,0 +1,41 @@ +// Scenarios: +// - base: set up an AMM pool and query its info using amm_info + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::amm_info::AMMInfo as AMMInfoRequest, results::amm_info::AMMInfo as AMMInfoResult, + Currency, IssuedCurrency, XRP, +}; + +#[tokio::test] +async fn test_amm_info_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let pool = crate::common::amm::setup_amm_pool().await; + + let request = AMMInfoRequest::new( + None, // id + None, // amm_account + Some(Currency::XRP(XRP::new())), + Some(Currency::IssuedCurrency(IssuedCurrency::new( + "USD".into(), + pool.issuer_wallet.classic_address.clone().into(), + ))), + ); + + let response = client + .request(request.into()) + .await + .expect("amm_info request failed"); + + let result: AMMInfoResult = response + .try_into() + .expect("failed to parse amm_info result"); + + // Verify the AMM description has valid data + assert!(!result.amm.account.is_empty()); + assert_eq!(result.amm.trading_fee, 12); + }) + .await; +} diff --git a/tests/requests/book_offers.rs b/tests/requests/book_offers.rs new file mode 100644 index 00000000..a52471ed --- /dev/null +++ b/tests/requests/book_offers.rs @@ -0,0 +1,45 @@ +// Scenarios: +// - base: send a book_offers request for XRP/USD and verify the response + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::book_offers::BookOffers; +use xrpl::models::results::book_offers::BookOffers as BookOffersResult; +use xrpl::models::{Currency, IssuedCurrency, XRP}; + +#[tokio::test] +async fn test_book_offers_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + + let request = BookOffers::new( + None, // id + Currency::XRP(XRP::new()), // taker_gets + Currency::IssuedCurrency(IssuedCurrency::new( + // taker_pays + "USD".into(), + wallet.classic_address.clone().into(), + )), + None, // ledger_hash + None, // ledger_index + None, // limit + None, // taker + ); + + let response = client + .request(request.into()) + .await + .expect("book_offers request failed"); + + let result: BookOffersResult = response + .try_into() + .expect("failed to parse book_offers result"); + + // Verify offers is empty (no offers on the book) + assert!(result.offers.is_empty()); + // Verify ledger_current_index exists + assert!(result.ledger_current_index.unwrap() > 0); + }) + .await; +} diff --git a/tests/requests/channel_verify.rs b/tests/requests/channel_verify.rs new file mode 100644 index 00000000..99bf15a3 --- /dev/null +++ b/tests/requests/channel_verify.rs @@ -0,0 +1,39 @@ +// Scenarios: +// - base: send a channel_verify request with hardcoded test data and verify the response + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::channel_verify::ChannelVerify as ChannelVerifyRequest, + results::channel_verify::ChannelVerify as ChannelVerifyResult, XRPAmount, +}; + +#[tokio::test] +async fn test_channel_verify_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + let request = ChannelVerifyRequest::new( + None, // id + XRPAmount::from("1000000"), // amount + "5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3".into(), // channel_id + "aB44YfzW24VDEJQ2UuLPV2PvqcPCSoLnL7y5M1EzhdW4LnK5xMS3".into(), // public_key + "304402204EF0AFB78AC23ED1C472E74F4299C0C21F1B21D07EFC0A3838A420F76D783A400220154FB11B6F54320666E4C36CA7F686C16A3A0456800BBC43746F34AF50290064".into(), // signature + ); + + let response = client + .request(request.into()) + .await + .expect("channel_verify request failed"); + + let result: ChannelVerifyResult = response + .try_into() + .expect("failed to parse channel_verify result"); + + // Verify that signature_verified is returned (the value depends on whether + // the channel actually exists, but the field should be present) + // With hardcoded test data, the signature may or may not verify + let _ = result.signature_verified; + }) + .await; +} diff --git a/tests/requests/deposit_authorized.rs b/tests/requests/deposit_authorized.rs new file mode 100644 index 00000000..3f878c15 --- /dev/null +++ b/tests/requests/deposit_authorized.rs @@ -0,0 +1,49 @@ +// Scenarios: +// - base: send a deposit_authorized request between two funded wallets +// and verify that deposits are authorized by default + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::deposit_authorize::DepositAuthorized; +use xrpl::models::results::deposit_authorize::DepositAuthorized as DepositAuthorizedResult; + +#[tokio::test] +async fn test_deposit_authorized_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet1 = crate::common::generate_funded_wallet().await; + let wallet2 = crate::common::generate_funded_wallet().await; + + let request = DepositAuthorized::new( + None, // id + wallet2.classic_address.clone().into(), // destination_account + wallet1.classic_address.clone().into(), // source_account + None, // ledger_hash + None, // ledger_index + ); + + let response = client + .request(request.into()) + .await + .expect("deposit_authorized request failed"); + + let result: DepositAuthorizedResult = response + .try_into() + .expect("failed to parse deposit_authorized result"); + + // Verify deposit is authorized (default state) + assert!(result.deposit_authorized); + // Verify source and destination accounts match + assert_eq!( + result.source_account.as_ref(), + wallet1.classic_address.as_str() + ); + assert_eq!( + result.destination_account.as_ref(), + wallet2.classic_address.as_str() + ); + // Verify ledger_current_index exists + assert!(result.ledger_current_index.unwrap() > 0); + }) + .await; +} diff --git a/tests/requests/fee.rs b/tests/requests/fee.rs new file mode 100644 index 00000000..119cbf27 --- /dev/null +++ b/tests/requests/fee.rs @@ -0,0 +1,39 @@ +// Scenarios: +// - base: send a fee request and verify the response fields + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{requests::fee::Fee as FeeRequest, results::fee::Fee as FeeResult}; + +#[tokio::test] +async fn test_fee_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + let response = client + .request(FeeRequest::new(None).into()) + .await + .expect("fee request failed"); + + let result: FeeResult = response.try_into().expect("failed to parse fee result"); + + // Verify expected fields exist and have correct types + assert!(!result.current_ledger_size.is_empty()); + assert!(!result.current_queue_size.is_empty()); + assert!(!result.expected_ledger_size.is_empty()); + assert!(result.ledger_current_index > 0); + + // Verify drops fields + assert!(!result.drops.base_fee.0.is_empty()); + assert!(!result.drops.median_fee.0.is_empty()); + assert!(!result.drops.minimum_fee.0.is_empty()); + assert!(!result.drops.open_ledger_fee.0.is_empty()); + + // Verify levels fields + assert!(!result.levels.median_level.is_empty()); + assert!(!result.levels.minimum_level.is_empty()); + assert!(!result.levels.open_ledger_level.is_empty()); + assert!(!result.levels.reference_level.is_empty()); + }) + .await; +} diff --git a/tests/requests/gateway_balances.rs b/tests/requests/gateway_balances.rs new file mode 100644 index 00000000..e0c6d6fe --- /dev/null +++ b/tests/requests/gateway_balances.rs @@ -0,0 +1,45 @@ +// Scenarios: +// - base: send a gateway_balances request for a funded wallet and verify +// the response (no issued currencies, so balances/obligations are empty) + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::gateway_balances::GatewayBalances; +use xrpl::models::requests::LedgerIndex; +use xrpl::models::results::gateway_balances::GatewayBalances as GatewayBalancesResult; + +#[tokio::test] +async fn test_gateway_balances_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + + let request = GatewayBalances::new( + None, // id + wallet.classic_address.clone().into(), // account + None, // hotwallet + None, // ledger_hash + Some(LedgerIndex::Str("validated".into())), // ledger_index + Some(true), // strict + ); + + let response = client + .request(request.into()) + .await + .expect("gateway_balances request failed"); + + let result: GatewayBalancesResult = response + .try_into() + .expect("failed to parse gateway_balances result"); + + // Verify account matches + assert_eq!(result.account.as_ref(), wallet.classic_address.as_str()); + // Verify ledger_hash exists + assert!(result.ledger_hash.is_some()); + // Verify ledger_index is valid + assert!(result.ledger_index.unwrap() > 0); + // Verify no obligations (no issued currencies) + assert!(result.obligations.is_none()); + }) + .await; +} diff --git a/tests/requests/ledger.rs b/tests/requests/ledger.rs new file mode 100644 index 00000000..1776e6cd --- /dev/null +++ b/tests/requests/ledger.rs @@ -0,0 +1,48 @@ +// Scenarios: +// - base: send a ledger request and verify the response fields + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::ledger::Ledger as LedgerRequest, results::ledger::Ledger as LedgerResult, +}; + +#[tokio::test] +async fn test_ledger_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + let response = client + .request( + LedgerRequest::new( + None, + None, + None, + None, + None, + None, + Some("validated".into()), + None, + None, + None, + ) + .into(), + ) + .await + .expect("ledger request failed"); + + let result: LedgerResult = response.try_into().expect("failed to parse ledger result"); + + // Verify the response contains valid ledger data + assert!(!result.ledger_hash.is_empty()); + assert!(result.ledger_index > 0); + assert!(result.validated == Some(true)); + + // Verify ledger inner fields + assert!(!result.ledger.account_hash.is_empty()); + assert!(!result.ledger.ledger_hash.is_empty()); + assert!(result.ledger.close_time > 0); + assert!(result.ledger.closed); + }) + .await; +} diff --git a/tests/requests/ledger_closed.rs b/tests/requests/ledger_closed.rs new file mode 100644 index 00000000..b4b06bfd --- /dev/null +++ b/tests/requests/ledger_closed.rs @@ -0,0 +1,30 @@ +// Scenarios: +// - base: send a ledger_closed request and verify the response fields + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::ledger_closed::LedgerClosed as LedgerClosedRequest, + results::ledger_closed::LedgerClosed as LedgerClosedResult, +}; + +#[tokio::test] +async fn test_ledger_closed_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + let response = client + .request(LedgerClosedRequest::new(None).into()) + .await + .expect("ledger_closed request failed"); + + let result: LedgerClosedResult = response + .try_into() + .expect("failed to parse ledger_closed result"); + + // Verify the response contains a valid ledger hash and index + assert!(!result.ledger_hash.is_empty()); + assert!(result.ledger_index > 0); + }) + .await; +} diff --git a/tests/requests/ledger_current.rs b/tests/requests/ledger_current.rs new file mode 100644 index 00000000..3178fde7 --- /dev/null +++ b/tests/requests/ledger_current.rs @@ -0,0 +1,29 @@ +// Scenarios: +// - base: send a ledger_current request and verify the response fields + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::ledger_current::LedgerCurrent as LedgerCurrentRequest, + results::ledger_current::LedgerCurrent as LedgerCurrentResult, +}; + +#[tokio::test] +async fn test_ledger_current_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + let response = client + .request(LedgerCurrentRequest::new(None).into()) + .await + .expect("ledger_current request failed"); + + let result: LedgerCurrentResult = response + .try_into() + .expect("failed to parse ledger_current result"); + + // Verify the response contains a valid ledger current index + assert!(result.ledger_current_index > 0); + }) + .await; +} diff --git a/tests/requests/ledger_data.rs b/tests/requests/ledger_data.rs new file mode 100644 index 00000000..4098bbf8 --- /dev/null +++ b/tests/requests/ledger_data.rs @@ -0,0 +1,47 @@ +// Scenarios: +// - base: send a ledger_data request with binary=true and limit=5, verify the response + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::{ledger_data::LedgerData as LedgerDataRequest, LedgerIndex}, + results::ledger_data::LedgerData as LedgerDataResult, +}; + +#[tokio::test] +async fn test_ledger_data_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + let request = LedgerDataRequest::new( + None, // id + Some(true), // binary + None, // ledger_hash + Some(LedgerIndex::Str("validated".into())), // ledger_index + Some(5), // limit + None, // marker + ); + + let response = client + .request(request.into()) + .await + .expect("ledger_data request failed"); + + let result: LedgerDataResult = response + .try_into() + .expect("failed to parse ledger_data result"); + + // Verify response fields + assert!(!result.ledger_hash.is_empty()); + assert!(result.ledger_index > 0); + assert_eq!(result.state.len(), 5); + + // Verify each state object has binary data and an index + for item in result.state.iter() { + assert!(item.data.is_some()); + assert!(!item.data.as_ref().unwrap().is_empty()); + assert!(!item.index.is_empty()); + } + }) + .await; +} diff --git a/tests/requests/ledger_entry.rs b/tests/requests/ledger_entry.rs new file mode 100644 index 00000000..7a3b32e2 --- /dev/null +++ b/tests/requests/ledger_entry.rs @@ -0,0 +1,70 @@ +// Scenarios: +// - base: get an entry index from ledger_data, then query ledger_entry with that index + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::{ledger_data::LedgerData as LedgerDataRequest, LedgerIndex}, + results::ledger_data::LedgerData as LedgerDataResult, +}; + +#[tokio::test] +async fn test_ledger_entry_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + // First, get a valid entry index from ledger_data + let data_request = LedgerDataRequest::new( + None, // id + None, // binary + None, // ledger_hash + Some(LedgerIndex::Str("validated".into())), // ledger_index + Some(1), // limit + None, // marker + ); + + let data_response = client + .request(data_request.into()) + .await + .expect("ledger_data request failed"); + + let data_result: LedgerDataResult = data_response + .try_into() + .expect("failed to parse ledger_data result"); + + let entry_index = data_result.state[0].index.clone(); + + // Now query ledger_entry with that index + let entry_request = xrpl::models::requests::ledger_entry::LedgerEntry::new( + None, // id + None, // account_root + None, // binary + None, // check + None, // deposit_preauth + None, // directory + None, // escrow + Some(entry_index.clone()), // index + None, // ledger_hash + None, // ledger_index + None, // offer + None, // payment_channel + None, // ripple_state + None, // ticket + ); + + let entry_response = client + .request(entry_request.into()) + .await + .expect("failed ledger_entry request"); + + let entry_result: xrpl::models::results::ledger_entry::LedgerEntry = entry_response + .try_into() + .expect("failed to parse ledger_entry result"); + + // Verify the returned index matches what we requested + assert_eq!(entry_result.index.as_ref(), entry_index.as_ref()); + // Verify the node is present (non-binary mode) + assert!(entry_result.node.is_some()); + }) + .await; +} diff --git a/tests/requests/mod.rs b/tests/requests/mod.rs new file mode 100644 index 00000000..5c00d833 --- /dev/null +++ b/tests/requests/mod.rs @@ -0,0 +1,27 @@ +mod account_channels; +mod account_currencies; +mod account_info; +mod account_lines; +mod account_objects; +mod account_offers; +mod account_tx; +mod amm_info; +mod book_offers; +mod channel_verify; +mod deposit_authorized; +mod fee; +mod gateway_balances; +mod ledger; +mod ledger_closed; +mod ledger_current; +mod ledger_data; +mod ledger_entry; +mod no_ripple_check; +mod ping; +mod random; +mod ripple_path_find; +mod server_info; +mod server_state; +mod submit; +mod submit_multisigned; +mod tx; diff --git a/tests/requests/no_ripple_check.rs b/tests/requests/no_ripple_check.rs new file mode 100644 index 00000000..7d22e009 --- /dev/null +++ b/tests/requests/no_ripple_check.rs @@ -0,0 +1,49 @@ +// Scenarios: +// - base: send a noripple_check request for a funded wallet with role=gateway +// and transactions=true, verify problems and transactions arrays + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::no_ripple_check::{NoRippleCheck as NoRippleCheckRequest, NoRippleCheckRole}, + requests::LedgerIndex, + results::no_ripple_check::NoRippleCheck as NoRippleCheckResult, +}; + +#[tokio::test] +async fn test_no_ripple_check_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + + let request = NoRippleCheckRequest::new( + None, // id + wallet.classic_address.clone().into(), // account + NoRippleCheckRole::Gateway, // role + None, // ledger_hash + Some(LedgerIndex::Str("current".into())), // ledger_index + None, // limit + Some(true), // transactions + ); + + let response = client + .request(request.into()) + .await + .expect("noripple_check request failed"); + + let result: NoRippleCheckResult = response + .try_into() + .expect("failed to parse noripple_check result"); + + // A newly funded account with gateway role should have at least one problem + // (the default ripple flag recommendation) + assert!(!result.problems.is_empty()); + assert!(result.problems[0].contains("default ripple")); + + // With transactions=true, we should get suggested fix transactions + assert!(result.transactions.is_some()); + let transactions = result.transactions.unwrap(); + assert!(!transactions.is_empty()); + }) + .await; +} diff --git a/tests/requests/ping.rs b/tests/requests/ping.rs new file mode 100644 index 00000000..cbf4e2a5 --- /dev/null +++ b/tests/requests/ping.rs @@ -0,0 +1,23 @@ +// Scenarios: +// - base: send a ping request and verify the response is successful + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::requests::ping::Ping as PingRequest; + +#[tokio::test] +async fn test_ping_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + let response = client + .request(PingRequest::new(None).into()) + .await + .expect("ping request failed"); + + // Ping response is minimal — verify the response is successful + assert!(response.is_success()); + assert!(response.error.is_none()); + }) + .await; +} diff --git a/tests/requests/random.rs b/tests/requests/random.rs new file mode 100644 index 00000000..c4000bc4 --- /dev/null +++ b/tests/requests/random.rs @@ -0,0 +1,27 @@ +// Scenarios: +// - base: send a random request and verify it returns a 64-character hex string + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::random::Random as RandomRequest, results::random::Random as RandomResult, +}; + +#[tokio::test] +async fn test_random_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + let response = client + .request(RandomRequest::new(None).into()) + .await + .expect("random request failed"); + + let result: RandomResult = response.try_into().expect("failed to parse random result"); + + // Verify the random string is a 64-character hex value + assert_eq!(result.random.len(), 64); + assert!(result.random.chars().all(|c| c.is_ascii_hexdigit())); + }) + .await; +} diff --git a/tests/requests/ripple_path_find.rs b/tests/requests/ripple_path_find.rs new file mode 100644 index 00000000..abd0fa17 --- /dev/null +++ b/tests/requests/ripple_path_find.rs @@ -0,0 +1,48 @@ +// Scenarios: +// - base: send a ripple_path_find request between two funded wallets and +// verify destination_account and destination_currencies + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::ripple_path_find::RipplePathFind as RipplePathFindRequest, + results::ripple_path_find::RipplePathFind as RipplePathFindResult, Amount, +}; + +#[tokio::test] +async fn test_ripple_path_find_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet1 = crate::common::generate_funded_wallet().await; + let wallet2 = crate::common::generate_funded_wallet().await; + + let request = RipplePathFindRequest::new( + None, // id + wallet2.classic_address.clone().into(), // destination_account + Amount::XRPAmount("100".into()), // destination_amount (XRP drops) + wallet1.classic_address.clone().into(), // source_account + None, // ledger_hash + None, // ledger_index + None, // send_max + None, // source_currencies + ); + + let response = client + .request(request.into()) + .await + .expect("ripple_path_find request failed"); + + let result: RipplePathFindResult = response + .try_into() + .expect("failed to parse ripple_path_find result"); + + // Verify the destination account matches + assert_eq!( + result.destination_account.as_ref(), + wallet2.classic_address.as_str() + ); + // Verify destination_currencies is not empty (should at least contain XRP) + assert!(!result.destination_currencies.is_empty()); + }) + .await; +} diff --git a/tests/requests/server_info.rs b/tests/requests/server_info.rs new file mode 100644 index 00000000..00a1c38f --- /dev/null +++ b/tests/requests/server_info.rs @@ -0,0 +1,36 @@ +// Scenarios: +// - base: send a server_info request and verify the response fields + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::server_info::ServerInfo as ServerInfoRequest, + results::server_info::ServerInfo as ServerInfoResult, +}; + +#[tokio::test] +async fn test_server_info_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + let response = client + .request(ServerInfoRequest::new(None).into()) + .await + .expect("server_info request failed"); + + let result: ServerInfoResult = response + .try_into() + .expect("failed to parse server_info result"); + + // Verify essential server_info fields + assert!(!result.info.build_version.is_empty()); + assert!(!result.info.complete_ledgers.is_empty()); + assert!(result.info.load_factor > 0); + + // Verify validated_ledger is present (standalone always has one) + assert!(result.info.validated_ledger.is_some()); + let validated = result.info.validated_ledger.unwrap(); + assert!(validated.seq > 0); + }) + .await; +} diff --git a/tests/requests/server_state.rs b/tests/requests/server_state.rs new file mode 100644 index 00000000..faf7b7ff --- /dev/null +++ b/tests/requests/server_state.rs @@ -0,0 +1,34 @@ +// Scenarios: +// - base: send a server_state request and verify the response fields + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::models::{ + requests::server_state::ServerState as ServerStateRequest, + results::server_state::ServerState as ServerStateResult, +}; + +#[tokio::test] +async fn test_server_state_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + + let response = client + .request(ServerStateRequest::new(None).into()) + .await + .expect("server_state request failed"); + + let result: ServerStateResult = response + .try_into() + .expect("failed to parse server_state result"); + + // Verify essential server_state fields + assert!(!result.state.build_version.is_empty()); + + // Verify validated_ledger is present (standalone always has one) + assert!(result.state.validated_ledger.is_some()); + let validated = result.state.validated_ledger.unwrap(); + assert!(validated.seq > 0); + }) + .await; +} diff --git a/tests/requests/submit.rs b/tests/requests/submit.rs new file mode 100644 index 00000000..0d9ef21c --- /dev/null +++ b/tests/requests/submit.rs @@ -0,0 +1,62 @@ +// Scenarios: +// - base: sign a transaction, encode it, submit via the submit request, +// and verify tesSUCCESS + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::core::binarycodec::encode; +use xrpl::models::requests::submit::Submit as SubmitRequest; +use xrpl::models::results::submit::Submit as SubmitResult; +use xrpl::models::transactions::payment::Payment; +use xrpl::models::{Amount, XRPAmount}; + +#[tokio::test] +async fn test_submit_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + let destination = crate::common::generate_funded_wallet().await; + + // Build and sign a payment + let mut payment = Payment::new( + wallet.classic_address.clone().into(), + None, // account_txn_id + None, // fee + None, // flags + None, // last_ledger_sequence + None, // memos + None, // sequence + None, // signers + None, // source_tag + None, // ticket_sequence + Amount::XRPAmount(XRPAmount::from("1000000")), + destination.classic_address.clone().into(), + None, // destination_tag + None, // invoice_id + None, // paths + None, // send_max + None, // deliver_min + ); + + // Autofill sequence, fee, etc. + xrpl::asynch::transaction::autofill_and_sign(&mut payment, client, &wallet, true) + .await + .expect("autofill_and_sign failed"); + + // Encode to blob and submit + let tx_blob = encode(&payment).expect("encode failed"); + let request = SubmitRequest::new(None, tx_blob.into(), None); + + let response = client + .request(request.into()) + .await + .expect("submit request failed"); + + let result: SubmitResult = response.try_into().expect("failed to parse submit result"); + + assert_eq!(result.engine_result.as_ref(), "tesSUCCESS"); + assert!(!result.tx_blob.is_empty()); + assert!(result.tx_json.is_object()); + }) + .await; +} diff --git a/tests/requests/submit_multisigned.rs b/tests/requests/submit_multisigned.rs new file mode 100644 index 00000000..56b7450b --- /dev/null +++ b/tests/requests/submit_multisigned.rs @@ -0,0 +1,104 @@ +// Scenarios: +// - base: set up a SignerList on an account, multisign a transaction with +// two signers, submit via submit_multisigned, and verify tesSUCCESS + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::asynch::transaction::{autofill, sign}; +use xrpl::models::requests::submit_multisigned::SubmitMultisigned as SubmitMultisignedRequest; +use xrpl::models::results::submit_multisigned::SubmitMultisigned as SubmitMultisignedResult; +use xrpl::models::transactions::account_set::AccountSet; +use xrpl::models::transactions::signer_list_set::{SignerEntry, SignerListSet}; + +#[tokio::test] +async fn test_submit_multisigned_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let main_wallet = crate::common::generate_funded_wallet().await; + let signer1 = crate::common::generate_funded_wallet().await; + let signer2 = crate::common::generate_funded_wallet().await; + + // Step 1: Set up a SignerList on the main account (quorum=2, each weight=1) + let mut signer_list_tx = SignerListSet::new( + main_wallet.classic_address.clone().into(), + None, // account_txn_id + None, // fee + None, // last_ledger_sequence + None, // memos + None, // sequence + None, // signers + None, // source_tag + None, // ticket_sequence + 2, // signer_quorum + Some(vec![ + SignerEntry::new(signer1.classic_address.clone(), 1), + SignerEntry::new(signer2.classic_address.clone(), 1), + ]), + ); + crate::common::test_transaction(&mut signer_list_tx, &main_wallet).await; + + // Step 2: Build the transaction to be multisigned (AccountSet to set a domain) + let mut tx = AccountSet::new( + main_wallet.classic_address.clone().into(), + None, // account_txn_id + None, // fee + None, // flags + None, // last_ledger_sequence + None, // memos + None, // sequence + None, // signers + None, // source_tag + None, // ticket_sequence + None, // clear_flag + Some("6578616d706c652e636f6d".into()), // domain = "example.com" in hex + None, // email_hash + None, // message_key + None, // set_flag + None, // transfer_rate + None, // tick_size + None, // nftoken_minter + ); + + // Autofill without signing + autofill(&mut tx, client, None) + .await + .expect("autofill failed"); + + // Set fee high enough for multisig (n+1 signatures * base_fee) + // For 2 signers: 3 * 10 = 30 drops minimum + tx.common_fields.fee = Some("30000".into()); + + // Step 3: Multisign with both signers + let mut tx_signer1 = tx.clone(); + let mut tx_signer2 = tx.clone(); + + sign(&mut tx_signer1, &signer1, true).expect("multisign signer1 failed"); + sign(&mut tx_signer2, &signer2, true).expect("multisign signer2 failed"); + + // Combine signers + let signers1 = tx_signer1.common_fields.signers.unwrap_or_default(); + let signers2 = tx_signer2.common_fields.signers.unwrap_or_default(); + let mut combined_signers = signers1; + combined_signers.extend(signers2); + + tx.common_fields.signing_pub_key = Some("".into()); + tx.common_fields.signers = Some(combined_signers); + + // Step 4: Serialize and submit as multisigned + let tx_json = serde_json::to_value(&tx).expect("serialize tx failed"); + let request = SubmitMultisignedRequest::new(None, tx_json, None); + + let response = client + .request(request.into()) + .await + .expect("submit_multisigned request failed"); + + let result: SubmitMultisignedResult = response + .try_into() + .expect("failed to parse submit_multisigned result"); + + assert_eq!(result.engine_result.as_ref(), "tesSUCCESS"); + assert!(!result.tx_blob.is_empty()); + }) + .await; +} diff --git a/tests/requests/tx.rs b/tests/requests/tx.rs new file mode 100644 index 00000000..becd504b --- /dev/null +++ b/tests/requests/tx.rs @@ -0,0 +1,84 @@ +// Scenarios: +// - base: submit a payment transaction, then query it by hash using the tx request + +use crate::common::with_blockchain_lock; +use xrpl::asynch::clients::XRPLAsyncClient; +use xrpl::asynch::transaction::sign_and_submit; +use xrpl::models::requests::tx::Tx as TxRequest; +use xrpl::models::results::tx::TxVersionMap; +use xrpl::models::transactions::payment::Payment; +use xrpl::models::{Amount, XRPAmount}; + +#[tokio::test] +async fn test_tx_base() { + with_blockchain_lock(|| async { + let client = crate::common::get_client().await; + let wallet = crate::common::generate_funded_wallet().await; + let destination = crate::common::generate_funded_wallet().await; + + // Submit a payment so we have a transaction to look up + let mut payment = Payment::new( + wallet.classic_address.clone().into(), + None, // account_txn_id + None, // fee + None, // flags + None, // last_ledger_sequence + None, // memos + None, // sequence + None, // signers + None, // source_tag + None, // ticket_sequence + Amount::XRPAmount(XRPAmount::from("1000000")), + destination.classic_address.clone().into(), + None, // destination_tag + None, // invoice_id + None, // paths + None, // send_max + None, // deliver_min + ); + + let submit_result = sign_and_submit(&mut payment, client, &wallet, true, true) + .await + .expect("sign_and_submit failed"); + assert_eq!(submit_result.engine_result.as_ref(), "tesSUCCESS"); + + // Extract the hash from the submit result + let tx_hash = submit_result + .tx_json + .get("hash") + .expect("submit result should contain hash") + .as_str() + .expect("hash should be a string"); + + // Advance the ledger so the transaction is validated + crate::common::ledger_accept().await; + + // Query the transaction by hash + let request = TxRequest::new( + None, // id + None, // binary + None, // max_ledger + None, // min_ledger + Some(tx_hash.to_string().into()), // transaction + ); + + let response = client + .request(request.into()) + .await + .expect("tx request failed"); + + let result: TxVersionMap = response.try_into().expect("failed to parse tx result"); + + // Verify the hash matches what we submitted + match &result { + TxVersionMap::Default(tx) => { + assert_eq!(tx.base.hash.as_ref(), tx_hash); + assert!(tx.base.validated.unwrap_or(false)); + } + TxVersionMap::V1(tx) => { + assert_eq!(tx.base.hash.as_ref(), tx_hash); + } + } + }) + .await; +} diff --git a/tests/transactions/account_delete.rs b/tests/transactions/account_delete.rs index 508eb2d1..e7e912af 100644 --- a/tests/transactions/account_delete.rs +++ b/tests/transactions/account_delete.rs @@ -1,12 +1,10 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/accountDelete.test.ts -// // Scenarios: // - base: submit AccountDelete and expect tecTOO_SOON // // NOTE: AccountDelete requires the account's sequence number to be at least 256 lower than the // current ledger index. A freshly funded account never satisfies this condition on testnet, so -// this test asserts tecTOO_SOON rather than tesSUCCESS (matching xrpl.js behavior — it only -// verifies the transaction is accepted by the node, not that the deletion succeeds). +// this test asserts tecTOO_SOON rather than tesSUCCESS (it only verifies the transaction is +// accepted by the node, not that the deletion succeeds). // // On Docker standalone mode, call ledger_accept() 256 times before submitting to satisfy the // condition and assert tesSUCCESS instead. diff --git a/tests/transactions/account_set.rs b/tests/transactions/account_set.rs index 4c377cc1..1e6782ca 100644 --- a/tests/transactions/account_set.rs +++ b/tests/transactions/account_set.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/accountSet.test.ts -// // Scenarios: // - base: set domain field with hex-encoded value // - with_memo: attach a memo to the transaction diff --git a/tests/transactions/amm_bid.rs b/tests/transactions/amm_bid.rs index 49ee78f3..6ff494e5 100644 --- a/tests/transactions/amm_bid.rs +++ b/tests/transactions/amm_bid.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/ammBid.test.ts -// // Scenarios: // - base: LP holder bids for the AMM's auction slot (no BidMin/BidMax/AuthAccounts) // diff --git a/tests/transactions/amm_create.rs b/tests/transactions/amm_create.rs index 140e72a3..095664c5 100644 --- a/tests/transactions/amm_create.rs +++ b/tests/transactions/amm_create.rs @@ -1,9 +1,7 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/ammCreate.test.ts -// // Scenarios: // - base: create an XRP/USD AMM pool (250 drops / 250 USD, trading_fee = 12) // -// Setup (mirrors createAMMPool in xrpl.js/utils.ts): +// Setup: // 1. issuerWallet AccountSet — enable DefaultRipple // 2. lpWallet TrustSet — trust issuer for 1000 USD (tfClearNoRipple) // 3. issuerWallet Payment — send 500 USD to lpWallet diff --git a/tests/transactions/amm_delete.rs b/tests/transactions/amm_delete.rs index 82f675e9..5090a6fd 100644 --- a/tests/transactions/amm_delete.rs +++ b/tests/transactions/amm_delete.rs @@ -1,4 +1,4 @@ -// xrpl.js reference: no dedicated test file — AMMDelete is a cleanup operation +// AMMDelete is a cleanup operation // for AMMs that could not be fully deleted by AMMWithdraw due to too many trust // lines (> ~512 LP token holders). In the simple 2-trust-line setup used here, // AMMWithdraw TfWithdrawAll auto-deletes the AMM in a single transaction, so diff --git a/tests/transactions/amm_deposit.rs b/tests/transactions/amm_deposit.rs index a3d8c437..bce9b979 100644 --- a/tests/transactions/amm_deposit.rs +++ b/tests/transactions/amm_deposit.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/ammDeposit.test.ts -// // Scenarios: // - single_asset: deposit 1000 XRP drops into an XRP/USD pool (TfSingleAsset flag) // diff --git a/tests/transactions/amm_vote.rs b/tests/transactions/amm_vote.rs index c8cb2090..9cb1e5b2 100644 --- a/tests/transactions/amm_vote.rs +++ b/tests/transactions/amm_vote.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/ammVote.test.ts -// // Scenarios: // - base: LP holder votes to change trading_fee to 150 (per 100_000) // diff --git a/tests/transactions/amm_withdraw.rs b/tests/transactions/amm_withdraw.rs index 83092c52..0719a311 100644 --- a/tests/transactions/amm_withdraw.rs +++ b/tests/transactions/amm_withdraw.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/ammWithdraw.test.ts -// // Scenarios: // - single_asset: withdraw 500 XRP drops from an XRP/USD pool (TfSingleAsset flag) // diff --git a/tests/transactions/check_cancel.rs b/tests/transactions/check_cancel.rs index b5197be1..33c00035 100644 --- a/tests/transactions/check_cancel.rs +++ b/tests/transactions/check_cancel.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/checkCancel.test.ts -// // Scenarios: // - base: create a Check for 50 drops, then cancel it (creator cancels their own check) diff --git a/tests/transactions/check_cash.rs b/tests/transactions/check_cash.rs index 50247ecf..f8bce789 100644 --- a/tests/transactions/check_cash.rs +++ b/tests/transactions/check_cash.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/checkCash.test.ts -// // Scenarios: // - base: create a Check for 500 drops, then cash it for the exact amount diff --git a/tests/transactions/check_create.rs b/tests/transactions/check_create.rs index b712ad25..02d1a55b 100644 --- a/tests/transactions/check_create.rs +++ b/tests/transactions/check_create.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/checkCreate.test.ts -// // Scenarios: // - base: create a Check for 50 drops and verify one check object exists on the ledger diff --git a/tests/transactions/deposit_preauth.rs b/tests/transactions/deposit_preauth.rs index 6904a488..8632a664 100644 --- a/tests/transactions/deposit_preauth.rs +++ b/tests/transactions/deposit_preauth.rs @@ -1,11 +1,8 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/depositPreauth.test.ts -// // Scenarios: // - base: authorize a second account to send payments to a deposit-auth-enabled account // -// NOTE: The AuthorizeCredentials / UnauthorizeCredentials scenarios in xrpl.js require the -// Credentials amendment which is not yet enabled on the public testnet. Those variants are -// deferred until Docker standalone mode. +// NOTE: The AuthorizeCredentials / UnauthorizeCredentials scenarios require the +// Credentials amendment which is not yet enabled. Those variants are deferred. use crate::common::{generate_funded_wallet, test_transaction, with_blockchain_lock}; use xrpl::models::transactions::deposit_preauth::DepositPreauth; diff --git a/tests/transactions/escrow_cancel.rs b/tests/transactions/escrow_cancel.rs index 38b8dc94..05067bdf 100644 --- a/tests/transactions/escrow_cancel.rs +++ b/tests/transactions/escrow_cancel.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/escrowCancel.test.ts -// // Scenarios: // - base: create a time-locked XRP escrow then cancel it once CancelAfter has passed // @@ -8,7 +6,6 @@ // 1. Queries account_objects to confirm the escrow exists on-chain // 2. Looks up the creating tx to get the validated Sequence (OfferSequence) // 3. Waits for close_time >= CancelAfter, then one more ledger_accept -// This mirrors the xrpl.js pattern exactly (account_objects → tx lookup). // An escrow can only be cancelled after CancelAfter passes. use crate::common::{ @@ -49,13 +46,12 @@ async fn test_escrow_cancel_base() { // test_transaction signs, submits, asserts tesSUCCESS, and calls ledger_accept. test_transaction(&mut create_tx, &wallet).await; - // Mirroring xrpl.js: look up the validated Sequence via account_objects → tx query + // Look up the validated Sequence via account_objects → tx query // instead of reading the autofilled value from the tx struct. This confirms the // escrow actually exists on-chain before we try to cancel it. let offer_sequence = get_escrow_offer_sequence(&wallet.classic_address).await; - // Wait for the validated ledger close_time to reach CancelAfter (mirrors - // xrpl.js waitForAndForceProgressLedgerTime(CLOSE_TIME + 3)). + // Wait for the validated ledger close_time to reach CancelAfter. wait_for_ledger_close_time(cancel_after as u64).await; // rippled validates a cancel using the *previous* ledger's close_time, // so one more ledger_accept ensures that previous close_time > CancelAfter. diff --git a/tests/transactions/escrow_create.rs b/tests/transactions/escrow_create.rs index 7a5a7d01..02021b07 100644 --- a/tests/transactions/escrow_create.rs +++ b/tests/transactions/escrow_create.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/escrowCreate.test.ts -// // Scenarios: // - base: create a time-locked XRP escrow (FinishAfter = close_time + 2) and verify tesSUCCESS // diff --git a/tests/transactions/escrow_finish.rs b/tests/transactions/escrow_finish.rs index 413776b3..ae2a3db6 100644 --- a/tests/transactions/escrow_finish.rs +++ b/tests/transactions/escrow_finish.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/escrowFinish.test.ts -// // Scenarios: // - base: create a time-locked XRP escrow then finish it once FinishAfter has passed // @@ -7,7 +5,6 @@ // 1. Queries account_objects to confirm the escrow exists on-chain // 2. Looks up the creating tx to get the validated Sequence (OfferSequence) // 3. Waits for close_time >= FinishAfter, then one more ledger_accept -// This mirrors the xrpl.js pattern exactly (account_objects → tx lookup). use crate::common::{ generate_funded_wallet, get_escrow_offer_sequence, get_ledger_close_time, ledger_accept, @@ -46,13 +43,12 @@ async fn test_escrow_finish_base() { // test_transaction signs, submits, asserts tesSUCCESS, and calls ledger_accept. test_transaction(&mut create_tx, &wallet).await; - // Mirroring xrpl.js: look up the validated Sequence via account_objects → tx query + // Look up the validated Sequence via account_objects → tx query // instead of reading the autofilled value from the tx struct. This confirms the // escrow actually exists on-chain before we try to finish it. let offer_sequence = get_escrow_offer_sequence(&wallet.classic_address).await; - // Wait for the validated ledger close_time to reach FinishAfter (mirrors - // xrpl.js waitForAndForceProgressLedgerTime(CLOSE_TIME + 2)). + // Wait for the validated ledger close_time to reach FinishAfter. wait_for_ledger_close_time(finish_after as u64).await; // rippled validates a finish using the *previous* ledger's close_time, // so one more ledger_accept ensures that previous close_time > FinishAfter. diff --git a/tests/transactions/nftoken_accept_offer.rs b/tests/transactions/nftoken_accept_offer.rs index de0edc48..7271d534 100644 --- a/tests/transactions/nftoken_accept_offer.rs +++ b/tests/transactions/nftoken_accept_offer.rs @@ -1,6 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/nftokenMint.test.ts -// (xrpl.js covers NFTokenAcceptOffer indirectly via the "test with Amount" scenario in nftokenMint) -// // Scenarios: // - sell_offer: seller mints a transferable NFT, creates a sell offer, buyer accepts it diff --git a/tests/transactions/nftoken_burn.rs b/tests/transactions/nftoken_burn.rs index 446d2e75..d7bfb9b8 100644 --- a/tests/transactions/nftoken_burn.rs +++ b/tests/transactions/nftoken_burn.rs @@ -1,6 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/nftokenMint.test.ts -// (xrpl.js does not have a dedicated NFTokenBurn test file) -// // Scenarios: // - base: mint an NFT then burn it diff --git a/tests/transactions/nftoken_cancel_offer.rs b/tests/transactions/nftoken_cancel_offer.rs index b79aef32..9ffef038 100644 --- a/tests/transactions/nftoken_cancel_offer.rs +++ b/tests/transactions/nftoken_cancel_offer.rs @@ -1,6 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/nftokenMint.test.ts -// (xrpl.js does not have a dedicated NFTokenCancelOffer test file) -// // Scenarios: // - base: mint an NFT, create a sell offer, then cancel it diff --git a/tests/transactions/nftoken_create_offer.rs b/tests/transactions/nftoken_create_offer.rs index 057d5cd6..dc41871f 100644 --- a/tests/transactions/nftoken_create_offer.rs +++ b/tests/transactions/nftoken_create_offer.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/nftokenMint.test.ts -// // Scenarios: // - sell_offer: mint an NFT then create a sell offer for it diff --git a/tests/transactions/nftoken_mint.rs b/tests/transactions/nftoken_mint.rs index 08879ce9..1873058c 100644 --- a/tests/transactions/nftoken_mint.rs +++ b/tests/transactions/nftoken_mint.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/nftokenMint.test.ts -// // Scenarios: // - base: mint an NFT with a URI diff --git a/tests/transactions/offer_cancel.rs b/tests/transactions/offer_cancel.rs index 051b4bd4..0c7848db 100644 --- a/tests/transactions/offer_cancel.rs +++ b/tests/transactions/offer_cancel.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/offerCancel.test.ts -// // Scenarios: // - base: create an offer then cancel it by sequence number diff --git a/tests/transactions/offer_create.rs b/tests/transactions/offer_create.rs index c1b8296c..30638822 100644 --- a/tests/transactions/offer_create.rs +++ b/tests/transactions/offer_create.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/offerCreate.test.ts -// // Scenarios: // - base: place an XRP/USD offer on the DEX // diff --git a/tests/transactions/payment.rs b/tests/transactions/payment.rs index 6385a926..9ca7d15d 100644 --- a/tests/transactions/payment.rs +++ b/tests/transactions/payment.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/payment.test.ts -// // Scenarios: // - base: XRP payment to a new (unfunded) address diff --git a/tests/transactions/payment_channel_claim.rs b/tests/transactions/payment_channel_claim.rs index 347dc190..5e033d8b 100644 --- a/tests/transactions/payment_channel_claim.rs +++ b/tests/transactions/payment_channel_claim.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/paymentChannelClaim.test.ts -// // Scenarios: // - base: create a channel, then submit a claim for 100 drops (channel source claims to destination) // @@ -9,8 +7,8 @@ // NOTE: `amount` in PaymentChannelClaim is `Option>` (raw drop string), // not XRPAmount. Pass `Some("100".into())` for 100 drops. // -// NOTE: xrpl.js computes the channel ID via hashPaymentChannel(). We read it from -// account_objects instead since xrpl-rust has no equivalent utility. +// NOTE: We read the channel ID from account_objects since xrpl-rust has no +// hashPaymentChannel utility. use crate::common::{ generate_funded_wallet, get_client, ledger_accept, test_transaction, with_blockchain_lock, diff --git a/tests/transactions/payment_channel_create.rs b/tests/transactions/payment_channel_create.rs index a158effd..0048bc43 100644 --- a/tests/transactions/payment_channel_create.rs +++ b/tests/transactions/payment_channel_create.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/paymentChannelCreate.test.ts -// // Scenarios: // - base: create a payment channel from sender to destination with 100 drops and 86400s settle delay diff --git a/tests/transactions/payment_channel_fund.rs b/tests/transactions/payment_channel_fund.rs index f5939dc1..4212ed76 100644 --- a/tests/transactions/payment_channel_fund.rs +++ b/tests/transactions/payment_channel_fund.rs @@ -1,11 +1,8 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/paymentChannelFund.test.ts -// // Scenarios: // - base: create a channel, then add 100 more drops via PaymentChannelFund // -// NOTE: xrpl.js computes the channel ID via hashPaymentChannel(account, dest, seq). -// xrpl-rust has no equivalent utility, so we read the channel ID from account_objects -// after the PaymentChannelCreate is validated. +// NOTE: xrpl-rust has no hashPaymentChannel utility, so we read the channel ID from +// account_objects after the PaymentChannelCreate is validated. use crate::common::{ generate_funded_wallet, get_client, ledger_accept, test_transaction, with_blockchain_lock, diff --git a/tests/transactions/set_regular_key.rs b/tests/transactions/set_regular_key.rs index 74eb2d42..bf9d131e 100644 --- a/tests/transactions/set_regular_key.rs +++ b/tests/transactions/set_regular_key.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: no dedicated test file in xrpl.js integration suite. -// // Scenarios: // - base: assign a regular key to a wallet // - remove: remove the regular key from a wallet (regular_key = None) diff --git a/tests/transactions/signer_list_set.rs b/tests/transactions/signer_list_set.rs index 142e0111..342bd8f4 100644 --- a/tests/transactions/signer_list_set.rs +++ b/tests/transactions/signer_list_set.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/signerListSet.test.ts -// // Scenarios: // - add: set a signer list with two signers and quorum 2 // - remove: clear the signer list by setting SignerQuorum to 0 diff --git a/tests/transactions/ticket_create.rs b/tests/transactions/ticket_create.rs index cd56cdd2..18c56d8e 100644 --- a/tests/transactions/ticket_create.rs +++ b/tests/transactions/ticket_create.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: no dedicated test file in xrpl.js integration suite. -// // Scenarios: // - base: create 2 tickets and verify both ticket objects appear in account_objects diff --git a/tests/transactions/trust_set.rs b/tests/transactions/trust_set.rs index 60b386f7..15ca6843 100644 --- a/tests/transactions/trust_set.rs +++ b/tests/transactions/trust_set.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/trustSet.test.ts -// // Scenarios: // - base: set a USD trust line to a locally funded issuer // diff --git a/tests/transactions/xchain_account_create_commit.rs b/tests/transactions/xchain_account_create_commit.rs index 6f482b3f..f1f36b4c 100644 --- a/tests/transactions/xchain_account_create_commit.rs +++ b/tests/transactions/xchain_account_create_commit.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/xchainAccountCreateCommit.test.ts -// // Scenarios: // - base: committer funds creation of a new account on the issuing chain by locking // 10_000_000 drops + signature_reward on the locking chain door diff --git a/tests/transactions/xchain_add_account_create_attestation.rs b/tests/transactions/xchain_add_account_create_attestation.rs index 68454fc7..84c2c288 100644 --- a/tests/transactions/xchain_add_account_create_attestation.rs +++ b/tests/transactions/xchain_add_account_create_attestation.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/xchainAddAccountCreateAttestation.test.ts -// // Scenarios: // - base: witness submits an account-create attestation for a 300 XRP transfer // to a new (unfunded) destination address. diff --git a/tests/transactions/xchain_add_claim_attestation.rs b/tests/transactions/xchain_add_claim_attestation.rs index 9a5d662c..d95dfd10 100644 --- a/tests/transactions/xchain_add_claim_attestation.rs +++ b/tests/transactions/xchain_add_claim_attestation.rs @@ -1,13 +1,10 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/xchainAddClaimAttestation.test.ts -// // Scenarios: // - base: witness submits a claim attestation for a transfer of 10 XRP. -// The attestation payload is binary-encoded and signed with the witness private key, -// matching the same flow as xrpl.js: encode(attestationToSign) → sign(encoded, privateKey). +// The attestation payload is binary-encoded and signed with the witness private key. // // NOTE: XChainAddClaimAttestation has NO flags; standard 9 common-field order. // -// Attestation signing flow (mirrors ripple-binary-codec + ripple-keypairs in xrpl.js): +// Attestation signing flow: // 1. Build a struct with the attestation fields (PascalCase serde names). // 2. Binary-encode with xrpl::core::binarycodec::encode → hex string. // 3. Hex-decode to bytes. diff --git a/tests/transactions/xchain_claim.rs b/tests/transactions/xchain_claim.rs index 438503b6..e55dbcec 100644 --- a/tests/transactions/xchain_claim.rs +++ b/tests/transactions/xchain_claim.rs @@ -1,12 +1,10 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/xchainClaim.test.ts -// // Scenarios: // - base: full cross-chain claim flow: // 1. XChainCreateClaimID — destination reserves claim ID 1, paying signature_reward // 2. XChainAddClaimAttestation — witness attests to a 10 XRP transfer (NO Destination) // 3. XChainClaim — destination explicitly claims the 10 XRP // -// The attestation does NOT include `Destination` — matching the xrpl.js test exactly. +// The attestation does NOT include `Destination`. // When no Destination is in the attestation, rippled does NOT auto-deliver on quorum; // the claimant must submit XChainClaim to specify the destination. // @@ -27,7 +25,7 @@ use xrpl::models::transactions::xchain_create_claim_id::XChainCreateClaimID; use xrpl::models::{Amount, Currency, XChainBridge, XRPAmount, XRP}; use xrpl::wallet::Wallet; -/// Attestation payload for XChainAddClaimAttestation (no Destination — mirrors xrpl.js). +/// Attestation payload for XChainAddClaimAttestation (no Destination). #[derive(Serialize)] struct ClaimAttestation<'a> { #[serde(rename = "XChainBridge")] @@ -79,7 +77,7 @@ async fn test_xchain_claim_base() { ledger_accept().await; - // Step 2: Build + sign attestation payload (NO Destination — matches xrpl.js) + // Step 2: Build + sign attestation payload (NO Destination) let attestation = ClaimAttestation { xchain_bridge: XChainBridge { issuing_chain_door: crate::common::constants::GENESIS_ACCOUNT.into(), diff --git a/tests/transactions/xchain_commit.rs b/tests/transactions/xchain_commit.rs index 2db12e46..9705a83d 100644 --- a/tests/transactions/xchain_commit.rs +++ b/tests/transactions/xchain_commit.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/xchainCommit.test.ts -// // Scenarios: // - base: committer locks 10_000_000 drops onto the locking chain door (XChainClaimID = 1) // diff --git a/tests/transactions/xchain_create_bridge.rs b/tests/transactions/xchain_create_bridge.rs index c501822b..e8c9a681 100644 --- a/tests/transactions/xchain_create_bridge.rs +++ b/tests/transactions/xchain_create_bridge.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/xchainCreateBridge.test.ts -// // Scenarios: // - base: door account creates an XRP/XRP bridge with genesis as the issuing chain door // diff --git a/tests/transactions/xchain_create_claim_id.rs b/tests/transactions/xchain_create_claim_id.rs index e93baaf9..790e43ba 100644 --- a/tests/transactions/xchain_create_claim_id.rs +++ b/tests/transactions/xchain_create_claim_id.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/xchainCreateClaimID.test.ts -// // Scenarios: // - base: claim ID holder creates a claim ID on an existing bridge // diff --git a/tests/transactions/xchain_modify_bridge.rs b/tests/transactions/xchain_modify_bridge.rs index 4cc3e3e2..b45a3bfd 100644 --- a/tests/transactions/xchain_modify_bridge.rs +++ b/tests/transactions/xchain_modify_bridge.rs @@ -1,5 +1,3 @@ -// xrpl.js reference: xrpl.js/packages/xrpl/test/integration/transactions/xchainModifyBridge.test.ts -// // Scenarios: // - base: create a bridge then modify the signature_reward from 200 to 300 drops //