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