openzeppelin_monitor/models/blockchain/solana/
transaction.rs1use serde::{Deserialize, Serialize};
7use std::ops::Deref;
8
9#[derive(Debug, Serialize, Deserialize, Clone, Default)]
14#[serde(rename_all = "camelCase")]
15pub struct TransactionInfo {
16 pub signature: String,
18
19 pub slot: u64,
21
22 pub block_time: Option<i64>,
24
25 pub transaction: TransactionMessage,
27
28 pub meta: Option<TransactionMeta>,
30}
31
32#[derive(Debug, Serialize, Deserialize, Clone, Default)]
34#[serde(rename_all = "camelCase")]
35pub struct TransactionMessage {
36 pub account_keys: Vec<String>,
38
39 pub recent_blockhash: String,
41
42 pub instructions: Vec<Instruction>,
44
45 #[serde(default)]
47 pub address_table_lookups: Vec<AddressTableLookup>,
48}
49
50#[derive(Debug, Serialize, Deserialize, Clone, Default)]
52#[serde(rename_all = "camelCase")]
53pub struct Instruction {
54 pub program_id_index: u8,
56
57 pub accounts: Vec<u8>,
59
60 pub data: String,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub parsed: Option<ParsedInstruction>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub program: Option<String>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub program_id: Option<String>,
74}
75
76#[derive(Debug, Serialize, Deserialize, Clone, Default)]
78#[serde(rename_all = "camelCase")]
79pub struct ParsedInstruction {
80 #[serde(rename = "type")]
82 pub instruction_type: String,
83
84 pub info: serde_json::Value,
86}
87
88#[derive(Debug, Serialize, Deserialize, Clone, Default)]
90#[serde(rename_all = "camelCase")]
91pub struct AddressTableLookup {
92 pub account_key: String,
94
95 pub writable_indexes: Vec<u8>,
97
98 pub readonly_indexes: Vec<u8>,
100}
101
102#[derive(Debug, Serialize, Deserialize, Clone, Default)]
104#[serde(rename_all = "camelCase")]
105pub struct TransactionMeta {
106 pub err: Option<serde_json::Value>,
108
109 pub fee: u64,
111
112 pub pre_balances: Vec<u64>,
114
115 pub post_balances: Vec<u64>,
117
118 #[serde(default)]
120 pub pre_token_balances: Vec<TokenBalance>,
121
122 #[serde(default)]
124 pub post_token_balances: Vec<TokenBalance>,
125
126 #[serde(default)]
128 pub inner_instructions: Vec<InnerInstruction>,
129
130 #[serde(default)]
132 pub log_messages: Vec<String>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub compute_units_consumed: Option<u64>,
137
138 #[serde(default)]
140 pub loaded_addresses: Option<LoadedAddresses>,
141}
142
143#[derive(Debug, Serialize, Deserialize, Clone, Default)]
145#[serde(rename_all = "camelCase")]
146pub struct LoadedAddresses {
147 #[serde(default)]
149 pub writable: Vec<String>,
150
151 #[serde(default)]
153 pub readonly: Vec<String>,
154}
155
156#[derive(Debug, Serialize, Deserialize, Clone, Default)]
158#[serde(rename_all = "camelCase")]
159pub struct TokenBalance {
160 pub account_index: u8,
162
163 pub mint: String,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub owner: Option<String>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub program_id: Option<String>,
173
174 pub ui_token_amount: UiTokenAmount,
176}
177
178#[derive(Debug, Serialize, Deserialize, Clone, Default)]
180#[serde(rename_all = "camelCase")]
181pub struct UiTokenAmount {
182 pub amount: String,
184
185 pub decimals: u8,
187
188 pub ui_amount: Option<f64>,
190
191 pub ui_amount_string: String,
193}
194
195#[derive(Debug, Serialize, Deserialize, Clone, Default)]
197#[serde(rename_all = "camelCase")]
198pub struct InnerInstruction {
199 pub index: u8,
201
202 pub instructions: Vec<Instruction>,
204}
205
206#[derive(Debug, Serialize, Deserialize, Clone, Default)]
211pub struct Transaction(pub TransactionInfo);
212
213impl Transaction {
214 pub fn signature(&self) -> &str {
216 &self.0.signature
217 }
218
219 pub fn slot(&self) -> u64 {
221 self.0.slot
222 }
223
224 pub fn is_success(&self) -> bool {
226 self.0
227 .meta
228 .as_ref()
229 .map(|m| m.err.is_none())
230 .unwrap_or(false)
231 }
232
233 pub fn logs(&self) -> &[String] {
235 self.0
236 .meta
237 .as_ref()
238 .map(|m| m.log_messages.as_slice())
239 .unwrap_or(&[])
240 }
241
242 pub fn fee(&self) -> u64 {
244 self.0.meta.as_ref().map(|m| m.fee).unwrap_or(0)
245 }
246
247 pub fn program_ids(&self) -> Vec<String> {
249 let account_keys = &self.0.transaction.account_keys;
250 self.0
251 .transaction
252 .instructions
253 .iter()
254 .filter_map(|ix| {
255 if let Some(program_id) = &ix.program_id {
257 return Some(program_id.clone());
258 }
259 let idx = ix.program_id_index as usize;
261 account_keys.get(idx).cloned()
262 })
263 .collect()
264 }
265
266 pub fn fee_payer(&self) -> Option<&str> {
269 self.0
270 .transaction
271 .account_keys
272 .first()
273 .filter(|s| !s.is_empty())
274 .map(|s| s.as_str())
275 }
276
277 pub fn accounts(&self) -> Vec<String> {
280 let mut accounts = self.0.transaction.account_keys.clone();
281
282 if let Some(meta) = &self.0.meta {
284 if let Some(loaded) = &meta.loaded_addresses {
285 accounts.extend(loaded.writable.iter().cloned());
286 accounts.extend(loaded.readonly.iter().cloned());
287 }
288 }
289
290 accounts
291 }
292}
293
294impl From<TransactionInfo> for Transaction {
295 fn from(tx: TransactionInfo) -> Self {
296 Self(tx)
297 }
298}
299
300impl Deref for Transaction {
301 type Target = TransactionInfo;
302
303 fn deref(&self) -> &Self::Target {
304 &self.0
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 fn create_test_transaction(success: bool) -> TransactionInfo {
313 TransactionInfo {
314 signature: "5wHu1qwD7q5ifaN5nwdcDqNFF53GJqa7nLp2BLPASe7FPYoWZL3YBrJmVL6nrMtwKjNFin1F"
315 .to_string(),
316 slot: 123456789,
317 block_time: Some(1234567890),
318 transaction: TransactionMessage {
319 account_keys: vec![
320 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(),
321 "11111111111111111111111111111111".to_string(),
322 ],
323 recent_blockhash: "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZAMdL4VZHirAn".to_string(),
324 instructions: vec![Instruction {
325 program_id_index: 0,
326 accounts: vec![1],
327 data: "3Bxs4h24hBtQy9rw".to_string(),
328 parsed: None,
329 program: None,
330 program_id: None,
331 }],
332 address_table_lookups: vec![],
333 },
334 meta: Some(TransactionMeta {
335 err: if success {
336 None
337 } else {
338 Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
339 },
340 fee: 5000,
341 pre_balances: vec![1000000000, 0],
342 post_balances: vec![999995000, 0],
343 pre_token_balances: vec![],
344 post_token_balances: vec![],
345 inner_instructions: vec![],
346 log_messages: vec![
347 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]".to_string(),
348 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success".to_string(),
349 ],
350 compute_units_consumed: Some(2000),
351 loaded_addresses: None,
352 }),
353 }
354 }
355
356 #[test]
357 fn test_transaction_wrapper_methods() {
358 let tx_info = create_test_transaction(true);
359 let transaction = Transaction(tx_info);
360
361 assert_eq!(
362 transaction.signature(),
363 "5wHu1qwD7q5ifaN5nwdcDqNFF53GJqa7nLp2BLPASe7FPYoWZL3YBrJmVL6nrMtwKjNFin1F"
364 );
365 assert_eq!(transaction.slot(), 123456789);
366 assert!(transaction.is_success());
367 assert_eq!(transaction.fee(), 5000);
368 assert_eq!(transaction.logs().len(), 2);
369 }
370
371 #[test]
372 fn test_failed_transaction() {
373 let tx_info = create_test_transaction(false);
374 let transaction = Transaction(tx_info);
375
376 assert!(!transaction.is_success());
377 }
378
379 #[test]
380 fn test_program_ids() {
381 let tx_info = create_test_transaction(true);
382 let transaction = Transaction(tx_info);
383
384 let program_ids = transaction.program_ids();
385 assert_eq!(program_ids.len(), 1);
386 assert_eq!(
387 program_ids[0],
388 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
389 );
390 }
391
392 #[test]
393 fn test_transaction_from_info() {
394 let tx_info = create_test_transaction(true);
395 let transaction = Transaction::from(tx_info);
396
397 assert_eq!(
398 transaction.signature(),
399 "5wHu1qwD7q5ifaN5nwdcDqNFF53GJqa7nLp2BLPASe7FPYoWZL3YBrJmVL6nrMtwKjNFin1F"
400 );
401 }
402
403 #[test]
404 fn test_transaction_deref() {
405 let tx_info = create_test_transaction(true);
406 let transaction = Transaction(tx_info);
407
408 assert_eq!(
410 transaction.signature,
411 "5wHu1qwD7q5ifaN5nwdcDqNFF53GJqa7nLp2BLPASe7FPYoWZL3YBrJmVL6nrMtwKjNFin1F"
412 );
413 assert_eq!(transaction.slot, 123456789);
414 }
415
416 #[test]
417 fn test_default_implementation() {
418 let transaction = Transaction::default();
419
420 assert_eq!(transaction.signature(), "");
421 assert_eq!(transaction.slot(), 0);
422 assert!(!transaction.is_success());
423 assert_eq!(transaction.fee(), 0);
424 assert!(transaction.logs().is_empty());
425 assert!(transaction.fee_payer().is_none());
426 assert!(transaction.accounts().is_empty());
427 }
428
429 #[test]
430 fn test_fee_payer() {
431 let tx_info = create_test_transaction(true);
432 let transaction = Transaction(tx_info);
433
434 let fee_payer = transaction.fee_payer();
436 assert!(fee_payer.is_some());
437 assert_eq!(
438 fee_payer.unwrap(),
439 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
440 );
441 }
442
443 #[test]
444 fn test_accounts() {
445 let tx_info = create_test_transaction(true);
446 let transaction = Transaction(tx_info);
447
448 let accounts = transaction.accounts();
449 assert_eq!(accounts.len(), 2);
450 assert_eq!(accounts[0], "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
451 assert_eq!(accounts[1], "11111111111111111111111111111111");
452 }
453
454 #[test]
455 fn test_accounts_with_loaded_addresses() {
456 let mut tx_info = create_test_transaction(true);
457
458 if let Some(ref mut meta) = tx_info.meta {
460 meta.loaded_addresses = Some(LoadedAddresses {
461 writable: vec!["WritableALTAddress111111111111111111".to_string()],
462 readonly: vec!["ReadonlyALTAddress111111111111111111".to_string()],
463 });
464 }
465
466 let transaction = Transaction(tx_info);
467 let accounts = transaction.accounts();
468
469 assert_eq!(accounts.len(), 4);
471 assert_eq!(accounts[0], "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
472 assert_eq!(accounts[1], "11111111111111111111111111111111");
473 assert_eq!(accounts[2], "WritableALTAddress111111111111111111");
474 assert_eq!(accounts[3], "ReadonlyALTAddress111111111111111111");
475 }
476
477 #[test]
478 fn test_serde_serialization() {
479 let tx_info = create_test_transaction(true);
480 let transaction = Transaction(tx_info);
481
482 let serialized = serde_json::to_string(&transaction).unwrap();
484
485 let deserialized: Transaction = serde_json::from_str(&serialized).unwrap();
487
488 assert_eq!(
489 deserialized.signature(),
490 "5wHu1qwD7q5ifaN5nwdcDqNFF53GJqa7nLp2BLPASe7FPYoWZL3YBrJmVL6nrMtwKjNFin1F"
491 );
492 assert_eq!(deserialized.slot(), 123456789);
493 assert!(deserialized.is_success());
494 }
495
496 #[test]
497 fn test_parsed_instruction() {
498 let parsed = ParsedInstruction {
499 instruction_type: "transfer".to_string(),
500 info: serde_json::json!({
501 "source": "ABC123",
502 "destination": "DEF456",
503 "amount": "1000000"
504 }),
505 };
506
507 assert_eq!(parsed.instruction_type, "transfer");
508 assert_eq!(parsed.info["amount"], "1000000");
509 }
510}