1use solana_sdk::{
4 hash::Hash,
5 instruction::{AccountMeta, Instruction},
6 pubkey::Pubkey,
7 transaction::Transaction as SolanaTransaction,
8};
9use std::str::FromStr;
10
11use crate::{
12 constants::MAXIMUM_SOLANA_TX_ATTEMPTS,
13 models::{
14 EncodedSerializedTransaction, SolanaInstructionSpec, SolanaTransactionStatus,
15 TransactionError, TransactionRepoModel, TransactionStatus,
16 },
17 utils::base64_decode,
18};
19
20pub fn too_many_solana_attempts(tx: &TransactionRepoModel) -> bool {
27 tx.hashes.len() >= MAXIMUM_SOLANA_TX_ATTEMPTS
28}
29
30pub fn is_resubmitable(tx: &SolanaTransaction) -> bool {
44 tx.message.header.num_required_signatures <= 1
45}
46
47pub fn map_solana_status_to_transaction_status(
56 solana_status: SolanaTransactionStatus,
57) -> TransactionStatus {
58 match solana_status {
59 SolanaTransactionStatus::Processed => TransactionStatus::Mined,
60 SolanaTransactionStatus::Confirmed => TransactionStatus::Mined,
61 SolanaTransactionStatus::Finalized => TransactionStatus::Confirmed,
62 SolanaTransactionStatus::Failed => TransactionStatus::Failed,
63 }
64}
65
66pub fn decode_solana_transaction(
74 tx: &TransactionRepoModel,
75) -> Result<SolanaTransaction, TransactionError> {
76 let solana_data = tx.network_data.get_solana_transaction_data()?;
77
78 if let Some(transaction_str) = &solana_data.transaction {
79 decode_solana_transaction_from_string(transaction_str)
80 } else {
81 Err(TransactionError::ValidationError(
82 "Transaction not yet built - only available after preparation".to_string(),
83 ))
84 }
85}
86
87pub fn decode_solana_transaction_from_string(
89 encoded: &str,
90) -> Result<SolanaTransaction, TransactionError> {
91 let encoded_tx = EncodedSerializedTransaction::new(encoded.to_string());
92 SolanaTransaction::try_from(encoded_tx)
93 .map_err(|e| TransactionError::ValidationError(format!("Invalid transaction: {e}")))
94}
95
96pub fn convert_instruction_specs_to_instructions(
108 instructions: &[SolanaInstructionSpec],
109) -> Result<Vec<Instruction>, TransactionError> {
110 let mut solana_instructions = Vec::new();
111
112 for (idx, spec) in instructions.iter().enumerate() {
113 let program_id = Pubkey::from_str(&spec.program_id).map_err(|e| {
114 TransactionError::ValidationError(format!("Instruction {idx}: Invalid program_id: {e}"))
115 })?;
116
117 let accounts = spec
118 .accounts
119 .iter()
120 .enumerate()
121 .map(|(acc_idx, a)| {
122 let pubkey = Pubkey::from_str(&a.pubkey).map_err(|e| {
123 TransactionError::ValidationError(format!(
124 "Instruction {idx} account {acc_idx}: Invalid pubkey: {e}"
125 ))
126 })?;
127 Ok(AccountMeta {
128 pubkey,
129 is_signer: a.is_signer,
130 is_writable: a.is_writable,
131 })
132 })
133 .collect::<Result<Vec<_>, TransactionError>>()?;
134
135 let data = base64_decode(&spec.data).map_err(|e| {
136 TransactionError::ValidationError(format!(
137 "Instruction {idx}: Invalid base64 data: {e}"
138 ))
139 })?;
140
141 solana_instructions.push(Instruction {
142 program_id,
143 accounts,
144 data,
145 });
146 }
147
148 Ok(solana_instructions)
149}
150
151pub fn build_transaction_from_instructions(
161 instructions: &[SolanaInstructionSpec],
162 payer: &Pubkey,
163 recent_blockhash: Hash,
164) -> Result<SolanaTransaction, TransactionError> {
165 let solana_instructions = convert_instruction_specs_to_instructions(instructions)?;
166
167 let mut tx = SolanaTransaction::new_with_payer(&solana_instructions, Some(payer));
168 tx.message.recent_blockhash = recent_blockhash;
169 Ok(tx)
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::{
176 models::{
177 NetworkTransactionData, NetworkType, SolanaAccountMeta, SolanaTransactionData,
178 TransactionStatus,
179 },
180 utils::base64_encode,
181 };
182 use chrono::Utc;
183 use solana_sdk::message::Message;
184 use solana_system_interface::instruction as system_instruction;
185
186 #[test]
187 fn test_decode_solana_transaction_invalid_data() {
188 let tx = TransactionRepoModel {
190 id: "test-tx".to_string(),
191 relayer_id: "test-relayer".to_string(),
192 status: TransactionStatus::Pending,
193 status_reason: None,
194 created_at: Utc::now().to_rfc3339(),
195 sent_at: None,
196 confirmed_at: None,
197 valid_until: None,
198 delete_at: None,
199 network_type: NetworkType::Solana,
200 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
201 transaction: Some("invalid-base64!!!".to_string()),
202 ..Default::default()
203 }),
204 priced_at: None,
205 hashes: Vec::new(),
206 noop_count: None,
207 is_canceled: Some(false),
208 };
209
210 let result = decode_solana_transaction(&tx);
211 assert!(result.is_err());
212
213 if let Err(TransactionError::ValidationError(msg)) = result {
214 assert!(msg.contains("Invalid transaction"));
215 } else {
216 panic!("Expected ValidationError");
217 }
218 }
219
220 #[test]
221 fn test_decode_solana_transaction_not_built() {
222 let tx = TransactionRepoModel {
224 id: "test-tx".to_string(),
225 relayer_id: "test-relayer".to_string(),
226 status: TransactionStatus::Pending,
227 status_reason: None,
228 created_at: Utc::now().to_rfc3339(),
229 sent_at: None,
230 confirmed_at: None,
231 valid_until: None,
232 delete_at: None,
233 network_type: NetworkType::Solana,
234 network_data: NetworkTransactionData::Solana(SolanaTransactionData {
235 transaction: None, ..Default::default()
237 }),
238 priced_at: None,
239 hashes: Vec::new(),
240 noop_count: None,
241 is_canceled: Some(false),
242 };
243
244 let result = decode_solana_transaction(&tx);
245 assert!(result.is_err());
246
247 if let Err(TransactionError::ValidationError(msg)) = result {
248 assert!(msg.contains("not yet built"));
249 } else {
250 panic!("Expected ValidationError");
251 }
252 }
253
254 #[test]
255 fn test_convert_instruction_specs_to_instructions_success() {
256 let program_id = Pubkey::new_unique();
257 let account = Pubkey::new_unique();
258
259 let specs = vec![SolanaInstructionSpec {
260 program_id: program_id.to_string(),
261 accounts: vec![SolanaAccountMeta {
262 pubkey: account.to_string(),
263 is_signer: false,
264 is_writable: true,
265 }],
266 data: base64_encode(b"test data"),
267 }];
268
269 let result = convert_instruction_specs_to_instructions(&specs);
270 assert!(result.is_ok());
271
272 let instructions = result.unwrap();
273 assert_eq!(instructions.len(), 1);
274 assert_eq!(instructions[0].program_id, program_id);
275 assert_eq!(instructions[0].accounts.len(), 1);
276 assert_eq!(instructions[0].accounts[0].pubkey, account);
277 assert!(!instructions[0].accounts[0].is_signer);
278 assert!(instructions[0].accounts[0].is_writable);
279 }
280
281 #[test]
282 fn test_build_transaction_from_instructions_success() {
283 let payer = Pubkey::new_unique();
284 let program_id = Pubkey::new_unique();
285 let account = Pubkey::new_unique();
286 let blockhash = Hash::new_unique();
287
288 let instructions = vec![SolanaInstructionSpec {
289 program_id: program_id.to_string(),
290 accounts: vec![SolanaAccountMeta {
291 pubkey: account.to_string(),
292 is_signer: false,
293 is_writable: true,
294 }],
295 data: base64_encode(b"test data"),
296 }];
297
298 let result = build_transaction_from_instructions(&instructions, &payer, blockhash);
299 assert!(result.is_ok());
300
301 let tx = result.unwrap();
302 assert_eq!(tx.message.account_keys[0], payer);
303 assert_eq!(tx.message.recent_blockhash, blockhash);
304 }
305
306 #[test]
307 fn test_build_transaction_invalid_program_id() {
308 let payer = Pubkey::new_unique();
309 let blockhash = Hash::new_unique();
310
311 let instructions = vec![SolanaInstructionSpec {
312 program_id: "invalid".to_string(),
313 accounts: vec![],
314 data: base64_encode(b"test"),
315 }];
316
317 let result = build_transaction_from_instructions(&instructions, &payer, blockhash);
318 assert!(result.is_err());
319 }
320
321 #[test]
322 fn test_build_transaction_invalid_base64_data() {
323 let payer = Pubkey::new_unique();
324 let program_id = Pubkey::new_unique();
325 let blockhash = Hash::new_unique();
326
327 let instructions = vec![SolanaInstructionSpec {
328 program_id: program_id.to_string(),
329 accounts: vec![],
330 data: "not-valid-base64!!!".to_string(),
331 }];
332
333 let result = build_transaction_from_instructions(&instructions, &payer, blockhash);
334 assert!(result.is_err());
335 }
336
337 #[test]
338 fn test_is_resubmitable_single_signer() {
339 let payer = Pubkey::new_unique();
340 let recipient = Pubkey::new_unique();
341 let instruction = system_instruction::transfer(&payer, &recipient, 1000);
342
343 let tx = SolanaTransaction::new_with_payer(&[instruction], Some(&payer));
345
346 assert!(is_resubmitable(&tx));
348 assert_eq!(tx.message.header.num_required_signatures, 1);
349 }
350
351 #[test]
352 fn test_is_resubmitable_multi_signer() {
353 let payer = Pubkey::new_unique();
354 let recipient = Pubkey::new_unique();
355 let additional_signer = Pubkey::new_unique();
356 let instruction = system_instruction::transfer(&payer, &recipient, 1000);
357
358 let mut message = Message::new(&[instruction], Some(&payer));
360 message.account_keys.push(additional_signer);
362 message.header.num_required_signatures = 2;
363
364 let tx = SolanaTransaction::new_unsigned(message);
365
366 assert!(!is_resubmitable(&tx));
368 assert_eq!(tx.message.header.num_required_signatures, 2);
369 }
370
371 #[test]
372 fn test_is_resubmitable_no_signers() {
373 let payer = Pubkey::new_unique();
374 let recipient = Pubkey::new_unique();
375 let instruction = system_instruction::transfer(&payer, &recipient, 1000);
376
377 let mut message = Message::new(&[instruction], Some(&payer));
379 message.header.num_required_signatures = 0;
380
381 let tx = SolanaTransaction::new_unsigned(message);
382
383 assert!(is_resubmitable(&tx));
385 assert_eq!(tx.message.header.num_required_signatures, 0);
386 }
387
388 #[test]
389 fn test_too_many_solana_attempts_under_limit() {
390 let tx = TransactionRepoModel {
391 id: "test-tx".to_string(),
392 relayer_id: "test-relayer".to_string(),
393 status: TransactionStatus::Pending,
394 status_reason: None,
395 created_at: Utc::now().to_rfc3339(),
396 sent_at: None,
397 confirmed_at: None,
398 valid_until: None,
399 delete_at: None,
400 network_type: NetworkType::Solana,
401 network_data: NetworkTransactionData::Solana(SolanaTransactionData::default()),
402 priced_at: None,
403 hashes: vec!["hash1".to_string(), "hash2".to_string()], noop_count: None,
405 is_canceled: Some(false),
406 };
407
408 assert!(!too_many_solana_attempts(&tx));
410 }
411
412 #[test]
413 fn test_too_many_solana_attempts_at_limit() {
414 let tx = TransactionRepoModel {
415 id: "test-tx".to_string(),
416 relayer_id: "test-relayer".to_string(),
417 status: TransactionStatus::Pending,
418 status_reason: None,
419 created_at: Utc::now().to_rfc3339(),
420 sent_at: None,
421 confirmed_at: None,
422 valid_until: None,
423 delete_at: None,
424 network_type: NetworkType::Solana,
425 network_data: NetworkTransactionData::Solana(SolanaTransactionData::default()),
426 priced_at: None,
427 hashes: vec!["hash".to_string(); MAXIMUM_SOLANA_TX_ATTEMPTS], noop_count: None,
429 is_canceled: Some(false),
430 };
431
432 assert!(too_many_solana_attempts(&tx));
434 }
435
436 #[test]
437 fn test_too_many_solana_attempts_over_limit() {
438 let tx = TransactionRepoModel {
439 id: "test-tx".to_string(),
440 relayer_id: "test-relayer".to_string(),
441 status: TransactionStatus::Pending,
442 status_reason: None,
443 created_at: Utc::now().to_rfc3339(),
444 sent_at: None,
445 confirmed_at: None,
446 valid_until: None,
447 delete_at: None,
448 network_type: NetworkType::Solana,
449 network_data: NetworkTransactionData::Solana(SolanaTransactionData::default()),
450 priced_at: None,
451 hashes: vec!["hash".to_string(); MAXIMUM_SOLANA_TX_ATTEMPTS + 1], noop_count: None,
453 is_canceled: Some(false),
454 };
455
456 assert!(too_many_solana_attempts(&tx));
458 }
459
460 #[test]
461 fn test_map_solana_status_to_transaction_status_processed() {
462 let result = map_solana_status_to_transaction_status(SolanaTransactionStatus::Processed);
463 assert_eq!(result, TransactionStatus::Mined);
464 }
465
466 #[test]
467 fn test_map_solana_status_to_transaction_status_confirmed() {
468 let result = map_solana_status_to_transaction_status(SolanaTransactionStatus::Confirmed);
469 assert_eq!(result, TransactionStatus::Mined);
470 }
471
472 #[test]
473 fn test_map_solana_status_to_transaction_status_finalized() {
474 let result = map_solana_status_to_transaction_status(SolanaTransactionStatus::Finalized);
475 assert_eq!(result, TransactionStatus::Confirmed);
476 }
477
478 #[test]
479 fn test_map_solana_status_to_transaction_status_failed() {
480 let result = map_solana_status_to_transaction_status(SolanaTransactionStatus::Failed);
481 assert_eq!(result, TransactionStatus::Failed);
482 }
483}