openzeppelin_monitor/models/blockchain/stellar/
transaction.rs

1//! Stellar transaction data structures.
2//!
3//! Note: These structures are based on the Stellar RPC implementation:
4//! <https://github.com/stellar/stellar-rpc/blob/main/cmd/stellar-rpc/internal/methods/get_transactions.go>
5
6use std::ops::Deref;
7
8use base64::Engine;
9use serde::{Deserialize, Serialize};
10use serde_json;
11use stellar_xdr::curr::{Limits, ReadXdr, TransactionEnvelope, TransactionMeta, TransactionResult};
12
13/// Information about a Stellar transaction
14///
15/// This structure represents the response from the Stellar RPC endpoint
16/// and matches the format defined in the stellar-rpc repository.
17#[derive(Debug, Serialize, Deserialize, Clone, Default)]
18pub struct TransactionInfo {
19	// Status fields
20	/// Current status of the transaction
21	pub status: String,
22
23	/// Hash of the transaction
24	#[serde(rename = "txHash")]
25	pub transaction_hash: String,
26
27	/// Order of this transaction within its ledger
28	#[serde(rename = "applicationOrder")]
29	pub application_order: i32,
30
31	/// Whether this is a fee bump transaction
32	#[serde(rename = "feeBump")]
33	pub fee_bump: bool,
34
35	// XDR and JSON fields
36	/// Base64-encoded XDR of the transaction envelope
37	#[serde(rename = "envelopeXdr", skip_serializing_if = "Option::is_none")]
38	pub envelope_xdr: Option<String>,
39
40	/// Decoded JSON representation of the envelope
41	#[serde(rename = "envelopeJson", skip_serializing_if = "Option::is_none")]
42	pub envelope_json: Option<serde_json::Value>,
43
44	/// Base64-encoded XDR of the transaction result
45	#[serde(rename = "resultXdr", skip_serializing_if = "Option::is_none")]
46	pub result_xdr: Option<String>,
47
48	/// Decoded JSON representation of the result
49	#[serde(rename = "resultJson", skip_serializing_if = "Option::is_none")]
50	pub result_json: Option<serde_json::Value>,
51
52	/// Base64-encoded XDR of the transaction metadata
53	#[serde(rename = "resultMetaXdr", skip_serializing_if = "Option::is_none")]
54	pub result_meta_xdr: Option<String>,
55
56	/// Decoded JSON representation of the metadata
57	#[serde(rename = "resultMetaJson", skip_serializing_if = "Option::is_none")]
58	pub result_meta_json: Option<serde_json::Value>,
59
60	// Diagnostic events
61	/// Base64-encoded XDR of diagnostic events
62	#[serde(
63		rename = "diagnosticEventsXdr",
64		skip_serializing_if = "Option::is_none"
65	)]
66	pub diagnostic_events_xdr: Option<Vec<String>>,
67
68	/// Decoded JSON representation of diagnostic events
69	#[serde(
70		rename = "diagnosticEventsJson",
71		skip_serializing_if = "Option::is_none"
72	)]
73	pub diagnostic_events_json: Option<Vec<serde_json::Value>>,
74
75	// Ledger information
76	/// Sequence number of the containing ledger
77	pub ledger: u32,
78
79	/// Timestamp when the ledger was closed
80	#[serde(rename = "createdAt")]
81	pub ledger_close_time: i64,
82
83	// Custom fields
84	/// Decoded transaction data
85	pub decoded: Option<DecodedTransaction>,
86}
87
88/// Decoded transaction data including envelope, result, and metadata
89///
90/// This structure contains the parsed XDR data from a Stellar transaction.
91/// It provides access to the detailed transaction data in a more usable format
92/// than the raw base64-encoded XDR strings.
93#[derive(Debug, Serialize, Deserialize, Clone)]
94pub struct DecodedTransaction {
95	/// Decoded transaction envelope containing the original transaction data
96	pub envelope: Option<TransactionEnvelope>,
97
98	/// Decoded transaction result containing success/failure and return values
99	pub result: Option<TransactionResult>,
100
101	/// Decoded transaction metadata containing execution effects
102	pub meta: Option<TransactionMeta>,
103}
104
105/// Wrapper around TransactionInfo that provides additional functionality
106///
107/// This type implements convenience methods for working with Stellar transactions
108/// while maintaining compatibility with the RPC response format.
109#[derive(Debug, Serialize, Deserialize, Clone)]
110pub struct Transaction(pub TransactionInfo);
111
112impl Transaction {
113	/// Get the transaction hash
114	pub fn hash(&self) -> &String {
115		&self.0.transaction_hash
116	}
117
118	/// Get the decoded transaction data if available
119	///
120	/// Returns the parsed XDR data including envelope, result, and metadata
121	/// if it was successfully decoded during transaction creation.
122	pub fn decoded(&self) -> Option<&DecodedTransaction> {
123		self.0.decoded.as_ref()
124	}
125
126	/// Decode base64-encoded XDR data into raw bytes
127	///
128	/// This is an internal helper function used during transaction creation
129	/// to parse the XDR fields from the RPC response.
130	fn decode_xdr(xdr: &str) -> Option<Vec<u8>> {
131		base64::engine::general_purpose::STANDARD.decode(xdr).ok()
132	}
133}
134
135impl From<TransactionInfo> for Transaction {
136	fn from(tx: TransactionInfo) -> Self {
137		let decoded = DecodedTransaction {
138			envelope: tx
139				.envelope_xdr
140				.as_ref()
141				.and_then(|xdr| Self::decode_xdr(xdr))
142				.and_then(|bytes| TransactionEnvelope::from_xdr(bytes, Limits::none()).ok()),
143
144			result: tx
145				.result_xdr
146				.as_ref()
147				.and_then(|xdr| Self::decode_xdr(xdr))
148				.and_then(|bytes| TransactionResult::from_xdr(bytes, Limits::none()).ok()),
149
150			meta: tx
151				.result_meta_xdr
152				.as_ref()
153				.and_then(|xdr| Self::decode_xdr(xdr))
154				.and_then(|bytes| TransactionMeta::from_xdr(bytes, Limits::none()).ok()),
155		};
156
157		Self(TransactionInfo {
158			decoded: Some(decoded),
159			..tx
160		})
161	}
162}
163
164impl Deref for Transaction {
165	type Target = TransactionInfo;
166
167	fn deref(&self) -> &Self::Target {
168		&self.0
169	}
170}
171
172#[cfg(test)]
173mod tests {
174	use super::*;
175	use base64::Engine;
176
177	#[test]
178	fn test_transaction_wrapper_methods() {
179		let tx_info = TransactionInfo {
180			transaction_hash: "test_hash".to_string(),
181			status: "SUCCESS".to_string(),
182			..Default::default()
183		};
184
185		let transaction = Transaction(tx_info);
186
187		assert_eq!(transaction.hash(), "test_hash");
188		assert!(transaction.decoded().is_none());
189	}
190
191	#[test]
192	fn test_decode_xdr() {
193		// Create a simple byte array and encode it to base64
194		let test_bytes = vec![1, 2, 3, 4];
195		let encoded = base64::engine::general_purpose::STANDARD.encode(&test_bytes);
196
197		// Test successful decoding
198		let decoded = Transaction::decode_xdr(&encoded);
199		assert!(decoded.is_some());
200		assert_eq!(decoded.unwrap(), test_bytes);
201
202		// Test invalid base64
203		let invalid_base64 = "invalid@@base64";
204		let result = Transaction::decode_xdr(invalid_base64);
205		assert!(result.is_none());
206	}
207
208	#[test]
209	fn test_transaction_from_info() {
210		let tx_info = TransactionInfo {
211			transaction_hash: "test_hash".to_string(),
212			status: "SUCCESS".to_string(),
213			envelope_xdr: Some("AAAA".to_string()),
214			result_xdr: Some("BBBB".to_string()),
215			result_meta_xdr: Some("CCCC".to_string()),
216			..Default::default()
217		};
218
219		let transaction = Transaction::from(tx_info);
220
221		// Verify the transaction was created
222		assert_eq!(transaction.hash(), "test_hash");
223		assert!(transaction.decoded().is_some());
224
225		let decoded = transaction.decoded().unwrap();
226		assert!(decoded.envelope.is_none());
227		assert!(decoded.result.is_none());
228		assert!(decoded.meta.is_none());
229	}
230
231	#[test]
232	fn test_transaction_deref() {
233		let tx_info = TransactionInfo {
234			transaction_hash: "test_hash".to_string(),
235			status: "SUCCESS".to_string(),
236			application_order: 1,
237			fee_bump: false,
238			ledger: 123,
239			ledger_close_time: 1234567890,
240			..Default::default()
241		};
242
243		let transaction = Transaction(tx_info);
244
245		// Test that we can access TransactionInfo fields through deref
246		assert_eq!(transaction.transaction_hash, "test_hash");
247		assert_eq!(transaction.status, "SUCCESS");
248		assert_eq!(transaction.application_order, 1);
249		assert!(!transaction.fee_bump);
250		assert_eq!(transaction.ledger, 123);
251		assert_eq!(transaction.ledger_close_time, 1234567890);
252	}
253}