1use crate::{
2 AccountMeta, AddressLookupTableAccount, CompiledInstruction, Instruction, Message,
3 MessageAddressTableLookup, MessageHeader, Pubkey, Result, SignatureBytes, SolanaError,
4 Transaction, VersionedMessageV0, VersionedTransaction,
5};
6use std::collections::{HashMap, HashSet};
7
8#[derive(Debug)]
10pub struct TransactionBuilder {
11 fee_payer: Pubkey,
13 instructions: Vec<Instruction>,
15 recent_blockhash: [u8; 32],
17 account_metas: HashMap<Pubkey, AccountMeta>,
19}
20
21impl TransactionBuilder {
22 pub fn new(fee_payer: Pubkey, recent_blockhash: [u8; 32]) -> Self {
24 let mut account_metas = HashMap::new();
25 account_metas.insert(
26 fee_payer,
27 AccountMeta {
28 pubkey: fee_payer,
29 is_signer: true,
30 is_writable: true,
31 },
32 );
33
34 Self {
35 fee_payer, instructions: Vec::new(),
37 recent_blockhash,
38 account_metas,
39 }
40 }
41
42 pub fn add_instruction(&mut self, instruction: Instruction) -> &mut Self {
44 self.account_metas
46 .entry(instruction.program_id)
47 .or_insert_with(|| AccountMeta {
48 pubkey: instruction.program_id,
49 is_signer: false,
50 is_writable: false,
51 });
52
53 for account_meta in &instruction.accounts {
56 self.account_metas
57 .entry(account_meta.pubkey)
58 .and_modify(|existing_meta| {
59 existing_meta.is_signer = existing_meta.is_signer || account_meta.is_signer;
60 existing_meta.is_writable =
61 existing_meta.is_writable || account_meta.is_writable;
62 })
63 .or_insert_with(|| account_meta.clone());
64 }
65 self.instructions.push(instruction);
66 self
67 }
68
69 pub fn add_instructions<I>(&mut self, instructions: I) -> &mut Self
71 where
72 I: IntoIterator<Item = Instruction>,
73 {
74 for instruction in instructions {
75 self.add_instruction(instruction);
76 }
77 self
78 }
79
80 pub fn build(self) -> Result<Transaction> {
82 let mut final_account_keys = Vec::new();
83 let mut processed_keys = std::collections::HashSet::new();
86
87 final_account_keys.push(self.fee_payer);
89 processed_keys.insert(self.fee_payer);
90
91 let mut writable_signers = Vec::new();
92 let mut readonly_signers = Vec::new();
93 let mut writable_non_signers = Vec::new();
94 let mut readonly_non_signers = Vec::new();
95
96 for (pubkey, meta) in &self.account_metas {
98 if *pubkey == self.fee_payer {
99 continue;
101 }
102 if meta.is_signer {
103 if meta.is_writable {
104 writable_signers.push(*pubkey);
105 } else {
106 readonly_signers.push(*pubkey);
107 }
108 } else if meta.is_writable {
109 writable_non_signers.push(*pubkey);
110 } else {
111 readonly_non_signers.push(*pubkey);
112 }
113 }
114
115 writable_signers.sort();
117 readonly_signers.sort();
118 writable_non_signers.sort();
119 readonly_non_signers.sort();
120
121 for key in writable_signers {
123 if processed_keys.insert(key) {
124 final_account_keys.push(key);
126 }
127 }
128 for key in readonly_signers {
129 if processed_keys.insert(key) {
130 final_account_keys.push(key);
131 }
132 }
133 for key in writable_non_signers {
134 if processed_keys.insert(key) {
135 final_account_keys.push(key);
136 }
137 }
138 for key in readonly_non_signers {
139 if processed_keys.insert(key) {
140 final_account_keys.push(key);
141 }
142 }
143
144 let account_keys: Vec<Pubkey> = final_account_keys;
145
146 if account_keys.len() > u8::MAX as usize + 1 {
148 return Err(SolanaError::InvalidMessage);
149 }
150
151 let key_to_index: HashMap<Pubkey, u8> = account_keys
153 .iter()
154 .enumerate()
155 .map(|(i, &key)| (key, i as u8))
156 .collect();
157
158 let compiled_instructions: Vec<CompiledInstruction> = self
160 .instructions
161 .iter()
162 .map(|instruction| {
163 let program_id_index = key_to_index[&instruction.program_id];
164 let accounts: Vec<u8> = instruction
165 .accounts
166 .iter()
167 .map(|meta| key_to_index[&meta.pubkey])
168 .collect();
169
170 CompiledInstruction {
171 program_id_index,
172 accounts,
173 data: instruction.data.clone(),
174 }
175 })
176 .collect();
177
178 let num_required_signatures = self
180 .account_metas
181 .values()
182 .filter(|meta| meta.is_signer)
183 .count();
184
185 let num_readonly_signed_accounts = self
186 .account_metas
187 .values()
188 .filter(|meta| meta.is_signer && !meta.is_writable)
189 .count();
190
191 let num_readonly_unsigned_accounts = self
192 .account_metas
193 .values()
194 .filter(|meta| !meta.is_signer && !meta.is_writable)
195 .count();
196
197 if num_required_signatures > u8::MAX as usize
198 || num_readonly_signed_accounts > u8::MAX as usize
199 || num_readonly_unsigned_accounts > u8::MAX as usize
200 {
201 return Err(SolanaError::InvalidMessage);
202 }
203
204 let header = MessageHeader {
205 num_required_signatures: num_required_signatures as u8,
206 num_readonly_signed_accounts: num_readonly_signed_accounts as u8,
207 num_readonly_unsigned_accounts: num_readonly_unsigned_accounts as u8,
208 };
209
210 let message = Message {
212 header,
213 account_keys,
214 recent_blockhash: self.recent_blockhash,
215 instructions: compiled_instructions,
216 };
217
218 let signatures = vec![SignatureBytes::new([0u8; 64]); num_required_signatures];
220
221 Ok(Transaction {
222 signatures,
223 message,
224 })
225 }
226
227 pub fn build_v0(
229 self,
230 address_lookup_tables: &[AddressLookupTableAccount],
231 ) -> Result<VersionedTransaction> {
232 let mut lookup_map: HashMap<Pubkey, (usize, u8)> = HashMap::new();
233 for (table_index, table) in address_lookup_tables.iter().enumerate().rev() {
234 for (entry_index, address) in table.addresses.iter().enumerate() {
235 if let Ok(entry_index_u8) = u8::try_from(entry_index) {
236 lookup_map.insert(*address, (table_index, entry_index_u8));
237 } else {
238 break;
239 }
240 }
241 }
242
243 let program_ids: HashSet<Pubkey> = self
244 .instructions
245 .iter()
246 .map(|instruction| instruction.program_id)
247 .collect();
248
249 let mut flags: HashMap<Pubkey, (bool, bool)> = HashMap::new();
250 let mut order: Vec<Pubkey> = Vec::new();
251 let mut merge = |pubkey: Pubkey, is_signer: bool, is_writable: bool| {
252 flags
253 .entry(pubkey)
254 .and_modify(|(existing_signer, existing_writable)| {
255 *existing_signer |= is_signer;
256 *existing_writable |= is_writable;
257 })
258 .or_insert_with(|| {
259 order.push(pubkey);
260 (is_signer, is_writable)
261 });
262 };
263
264 merge(self.fee_payer, true, true);
265 for instruction in &self.instructions {
266 merge(instruction.program_id, false, false);
267 for account_meta in &instruction.accounts {
268 merge(
269 account_meta.pubkey,
270 account_meta.is_signer,
271 account_meta.is_writable,
272 );
273 }
274 }
275
276 let mut static_keys: [Vec<Pubkey>; 4] = Default::default();
277 let mut lookup_writable: Vec<Vec<(Pubkey, u8)>> =
278 vec![Vec::new(); address_lookup_tables.len()];
279 let mut lookup_readonly: Vec<Vec<(Pubkey, u8)>> =
280 vec![Vec::new(); address_lookup_tables.len()];
281
282 for pubkey in &order {
283 let (is_signer, is_writable) = flags
284 .get(pubkey)
285 .copied()
286 .ok_or(SolanaError::InvalidMessage)?;
287
288 if is_signer || program_ids.contains(pubkey) || !lookup_map.contains_key(pubkey) {
289 let bucket = match (is_signer, is_writable) {
290 (true, true) => 0,
291 (true, false) => 1,
292 (false, true) => 2,
293 (false, false) => 3,
294 };
295 static_keys[bucket].push(*pubkey);
296 } else {
297 let (table_index, entry_index) = lookup_map
298 .get(pubkey)
299 .copied()
300 .ok_or(SolanaError::InvalidMessage)?;
301 if is_writable {
302 lookup_writable[table_index].push((*pubkey, entry_index));
303 } else {
304 lookup_readonly[table_index].push((*pubkey, entry_index));
305 }
306 }
307 }
308
309 let mut account_keys = Vec::with_capacity(static_keys.iter().map(Vec::len).sum());
310 account_keys.push(self.fee_payer);
311
312 account_keys.extend(
313 static_keys[0]
314 .iter()
315 .copied()
316 .filter(|pubkey| *pubkey != self.fee_payer),
317 );
318
319 for bucket in &static_keys[1..] {
320 account_keys.extend(bucket.iter().copied());
321 }
322
323 if account_keys.len() > u8::MAX as usize {
324 return Err(SolanaError::InvalidMessage);
325 }
326
327 let header = MessageHeader {
328 num_required_signatures: (static_keys[0].len() + static_keys[1].len()) as u8,
329 num_readonly_signed_accounts: static_keys[1].len() as u8,
330 num_readonly_unsigned_accounts: static_keys[3].len() as u8,
331 };
332
333 let mut virtual_index_map: HashMap<Pubkey, u8> = HashMap::new();
334 for (next_virtual_index, (pubkey, _)) in (account_keys.len()..).zip(
335 lookup_writable
336 .iter()
337 .flat_map(|entries| entries.iter())
338 .chain(lookup_readonly.iter().flat_map(|entries| entries.iter())),
339 ) {
340 let virtual_index =
341 u8::try_from(next_virtual_index).map_err(|_| SolanaError::InvalidMessage)?;
342 virtual_index_map.insert(*pubkey, virtual_index);
343 }
344
345 let address_table_lookups: Vec<MessageAddressTableLookup> = address_lookup_tables
346 .iter()
347 .enumerate()
348 .filter_map(|(table_index, table)| {
349 let writable_indexes: Vec<u8> = lookup_writable[table_index]
350 .iter()
351 .map(|(_, entry_index)| *entry_index)
352 .collect();
353 let readonly_indexes: Vec<u8> = lookup_readonly[table_index]
354 .iter()
355 .map(|(_, entry_index)| *entry_index)
356 .collect();
357
358 if writable_indexes.is_empty() && readonly_indexes.is_empty() {
359 return None;
360 }
361
362 Some(MessageAddressTableLookup::new(
363 table.key,
364 writable_indexes,
365 readonly_indexes,
366 ))
367 })
368 .collect();
369
370 let static_index_map: HashMap<Pubkey, u8> = account_keys
371 .iter()
372 .enumerate()
373 .map(|(index, pubkey)| (*pubkey, index as u8))
374 .collect();
375
376 let compiled_instructions: Vec<CompiledInstruction> = self
377 .instructions
378 .iter()
379 .map(|instruction| {
380 let program_id_index = static_index_map
381 .get(&instruction.program_id)
382 .copied()
383 .ok_or(SolanaError::InvalidMessage)?;
384
385 let accounts = instruction
386 .accounts
387 .iter()
388 .map(|account_meta| {
389 static_index_map
390 .get(&account_meta.pubkey)
391 .copied()
392 .or_else(|| virtual_index_map.get(&account_meta.pubkey).copied())
393 .ok_or(SolanaError::InvalidMessage)
394 })
395 .collect::<Result<Vec<_>>>()?;
396
397 Ok(CompiledInstruction {
398 program_id_index,
399 accounts,
400 data: instruction.data.clone(),
401 })
402 })
403 .collect::<Result<Vec<_>>>()?;
404
405 let signatures = vec![SignatureBytes::default(); header.num_required_signatures as usize];
406
407 Ok(VersionedTransaction::V0 {
408 signatures,
409 message: VersionedMessageV0 {
410 header,
411 account_keys,
412 recent_blockhash: self.recent_blockhash,
413 instructions: compiled_instructions,
414 address_table_lookups,
415 },
416 })
417 }
418
419 pub fn build_v0_transaction(
421 fee_payer: Pubkey,
422 recent_blockhash: [u8; 32],
423 instructions: &[Instruction],
424 address_lookup_tables: &[AddressLookupTableAccount],
425 ) -> Result<VersionedTransaction> {
426 let mut builder = TransactionBuilder::new(fee_payer, recent_blockhash);
427 builder.add_instructions(instructions.iter().cloned());
428 builder.build_v0(address_lookup_tables)
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::TransactionBuilder;
435 use crate::Pubkey;
436 use crate::SolanaError;
437 use crate::builder::InstructionBuilder;
438 use crate::instructions::{
439 program_ids::{system_program, token_program},
440 system::{create_account, transfer},
441 token::transfer_checked,
442 };
443 use crate::types::instruction::AccountMeta;
444 use crate::types::{
445 AddressLookupTableAccount, Instruction, SignatureBytes, VersionedTransaction,
446 };
447 use base64::Engine;
448 use base64::engine::general_purpose::STANDARD;
449
450 fn mint_pubkey() -> Pubkey {
451 Pubkey::from_base58("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap()
452 }
453
454 fn token_pubkey() -> Pubkey {
455 Pubkey::from_base58("4q2wPZuZwQTB1dEU9sMGsJK1d8NSL1hpBjTGHBsLQNDh").unwrap()
456 }
457
458 fn authority_pubkey() -> Pubkey {
459 Pubkey::from_base58("Hozo7TadHq6PMMiGLGNvgk79Hvj5VTAM7Ny2bamQ2m8q").unwrap()
460 }
461
462 fn payer_pubkey() -> Pubkey {
463 Pubkey::from_base58("7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z").unwrap()
464 }
465
466 fn new_account_pubkey() -> Pubkey {
467 Pubkey::from_base58("DShWnroshVbeUp28oopA3Pu7oFPDBtC1DBmPECXXAQ9n").unwrap()
468 }
469
470 fn random_pubkey() -> Pubkey {
471 let mut bytes = [0u8; 32];
472 bytes
473 .iter_mut()
474 .enumerate()
475 .for_each(|(i, byte)| *byte = i as u8);
476 Pubkey::new(bytes)
477 }
478
479 fn test_blockhash() -> [u8; 32] {
480 let mut bytes = [0u8; 32];
481 bytes
482 .iter_mut()
483 .enumerate()
484 .for_each(|(i, byte)| *byte = i as u8);
485 bytes
486 }
487
488 fn lookup_table_from_sparse_entries(
489 table_key: &str,
490 entries: &[(u8, &str)],
491 ) -> AddressLookupTableAccount {
492 let max_index = entries.iter().map(|(index, _)| *index).max().unwrap_or(0) as usize;
493 let mut addresses: Vec<Pubkey> = (0..=max_index)
494 .map(|entry_index| {
495 let mut bytes = [0u8; 32];
496 bytes[0] = 0xFE;
497 bytes[1] = (entry_index & 0xFF) as u8;
498 bytes[2] = ((entry_index >> 8) & 0xFF) as u8;
499 Pubkey::new(bytes)
500 })
501 .collect();
502
503 for (index, value) in entries {
504 addresses[*index as usize] = Pubkey::from_base58(value).unwrap();
505 }
506
507 AddressLookupTableAccount::new(Pubkey::from_base58(table_key).unwrap(), addresses)
508 }
509
510 fn account_meta_for_combined_index(index: usize, pubkey: Pubkey) -> AccountMeta {
511 let is_signer = index < 2;
512 let is_writable = index < 2 || (2..5).contains(&index) || (13..19).contains(&index);
513 AccountMeta::new(pubkey, is_signer, is_writable)
514 }
515
516 fn instruction_from_decoded_tx(
518 program_id_index: usize,
519 account_indexes: &[u8],
520 data_base58: &str,
521 combined_accounts: &[AccountMeta],
522 ) -> Instruction {
523 Instruction {
524 program_id: combined_accounts[program_id_index].pubkey,
525 accounts: account_indexes
526 .iter()
527 .map(|index| combined_accounts[*index as usize].clone())
528 .collect(),
529 data: bs58::decode(data_base58).into_vec().unwrap(),
530 }
531 }
532
533 #[test]
534 fn test_transaction_builder() {
535 let recent_blockhash = "9U2ogLjDt479wubHbEtPLGBF84DijmWggA4KoXSwcivd";
536 let recent_blockhash_bytes = bs58::decode(recent_blockhash).into_vec().unwrap();
537 let fee_payer: Pubkey = "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC"
538 .parse()
539 .unwrap();
540 let program_id =
541 Pubkey::from_base58("J88B7gmadHzTNGiy54c9Ms8BsEXNdB2fntFyhKpk3qoT").unwrap();
542 let data = hex::decode("a3265ce2f3698dc400000070000000000100000014000000514bcb1f9aabb904e6106bd1052b66d2706dbbb701000000006c000000000a00000085fba93ee29c604fa858a351688c01290841eafb19c63a70a475d3c7bc3bef9f000000000000000000008489b9cc07af97add00300000000000000000000000000001e83d2972d3dca3a330d60c2777ee5b8d25683c63fa359116985609830f42054050004002d16000000f0314f0cffdf8d00b6a7ce61f86164ca47c1b8b1bc2e").unwrap();
543 let instruction = InstructionBuilder::new(program_id)
544 .data(data)
545 .accounts(vec![
546 AccountMeta::new_readonly(
547 "ACLMuTFvDAb3oecQQGkTVqpUbhCKHG3EZ9uNXHK1W9ka"
548 .parse()
549 .unwrap(),
550 ),
551 AccountMeta::new_writable(
552 "3tJ67qa2GDfvv2wcMYNUfN5QBZrFpTwcU8ASZKMvCTVU"
553 .parse()
554 .unwrap(),
555 ),
556 AccountMeta::new_signer_writable(
557 "A21o4asMbFHYadqXdLusT9Bvx9xaC5YV9gcaidjqtdXC"
558 .parse()
559 .unwrap(),
560 ),
561 AccountMeta::new_writable(
562 "E8p6aiwuSDWEzQnjGjkNiMZrd1rpSsntWsaZCivdFz51"
563 .parse()
564 .unwrap(),
565 ),
566 AccountMeta::new_writable(
567 "FmAcjWaRFUxGWBfGT7G3CzcFeJFsewQ4KPJVG4f6fcob"
568 .parse()
569 .unwrap(),
570 ),
571 AccountMeta::new_readonly(system_program()),
572 ]);
573
574 let mut tx_builder =
575 TransactionBuilder::new(fee_payer, recent_blockhash_bytes.try_into().unwrap());
576 tx_builder.add_instruction(instruction.build());
577
578 let transaction = tx_builder.build().unwrap();
579 let tx_wire_bytes = transaction.serialize_legacy().unwrap();
580 let deserialized_vt = VersionedTransaction::deserialize_with_version(&tx_wire_bytes)
581 .expect("Failed to deserialize wire bytes into VersionedTransaction");
582
583 let _base64_tx = STANDARD.encode(&tx_wire_bytes);
584
585 match deserialized_vt {
586 VersionedTransaction::Legacy {
587 signatures: deserialized_signatures,
588 message: deserialized_legacy_message,
589 } => {
590 assert_eq!(deserialized_signatures, transaction.signatures);
591 assert_eq!(
592 deserialized_legacy_message.header,
593 transaction.message.header
594 );
595 assert_eq!(
596 deserialized_legacy_message.account_keys,
597 transaction.message.account_keys
598 );
599 assert_eq!(
600 deserialized_legacy_message.recent_blockhash,
601 transaction.message.recent_blockhash
602 );
603 assert_eq!(
604 deserialized_legacy_message.instructions,
605 transaction.message.instructions
606 );
607 }
608 _ => panic!("Deserialized transaction is not the expected Legacy variant"),
609 }
610 }
611
612 #[test]
613 fn test_complex_transaction() {
614 let payer = payer_pubkey();
615 let blockhash = test_blockhash();
616 let mut tx_builder = TransactionBuilder::new(payer, blockhash);
617
618 let from = payer_pubkey();
619 let new_account = new_account_pubkey();
620 let owner = system_program();
621 let lamports = 1_000_000_000;
622 let space = 165;
623
624 let create_account_ix = create_account(&from, &new_account, lamports, space, &owner);
625 tx_builder.add_instruction(create_account_ix);
626
627 let source = token_pubkey();
628 let dest = random_pubkey();
629 let owner = authority_pubkey();
630 let mint = mint_pubkey();
631 let amount = 1_000_000;
632 let decimals = 6;
633
634 let transfer_ix = transfer_checked(&source, &mint, &dest, &owner, amount, decimals);
635 tx_builder.add_instruction(transfer_ix);
636
637 let transaction = tx_builder.build().unwrap();
638
639 assert!(transaction.signatures.len() >= 2);
640
641 let account_keys = &transaction.message.account_keys;
642 assert!(account_keys.contains(&payer_pubkey()));
643 assert!(account_keys.contains(&new_account));
644 assert!(account_keys.contains(&system_program()));
645 assert!(account_keys.contains(&source));
646 assert!(account_keys.contains(&dest));
647 assert!(account_keys.contains(&owner));
648 assert!(account_keys.contains(&mint));
649 assert!(account_keys.contains(&token_program()));
650 assert_eq!(transaction.message.instructions.len(), 2);
651 assert!(!transaction.message.account_keys.is_empty());
652 }
653
654 #[test]
655 fn test_versioned_transaction_builder_without_lookup_tables() {
656 let fee_payer = payer_pubkey();
657 let recipient = random_pubkey();
658 let recent_blockhash = test_blockhash();
659
660 let transfer_ix = transfer(&fee_payer, &recipient, 123);
661
662 let mut builder = TransactionBuilder::new(fee_payer, recent_blockhash);
663 builder.add_instruction(transfer_ix);
664
665 let transaction = builder.build_v0(&[]).unwrap();
666 let wire_bytes = transaction.serialize().unwrap();
667 let parsed = VersionedTransaction::deserialize_with_version(&wire_bytes).unwrap();
668
669 match parsed {
670 VersionedTransaction::V0 {
671 signatures,
672 message,
673 } => {
674 assert_eq!(signatures.len(), 1);
675 assert_eq!(message.header.num_required_signatures, 1);
676 assert!(message.address_table_lookups.is_empty());
677 assert_eq!(message.instructions.len(), 1);
678 }
679 _ => panic!("expected v0 transaction"),
680 }
681 }
682
683 #[test]
684 fn test_versioned_transaction_builder_with_lookup_table() {
685 let fee_payer = payer_pubkey();
686 let recent_blockhash = test_blockhash();
687 let looked_up_account = Pubkey::new([42u8; 32]);
688 let program_id = Pubkey::new([7u8; 32]);
689
690 let instruction = InstructionBuilder::new(program_id)
691 .account(fee_payer, true, true)
692 .account(looked_up_account, false, true)
693 .data(vec![1, 2, 3])
694 .build();
695
696 let lookup_table = AddressLookupTableAccount::new(
697 Pubkey::new([99u8; 32]),
698 vec![looked_up_account, Pubkey::new([11u8; 32])],
699 );
700
701 let mut builder = TransactionBuilder::new(fee_payer, recent_blockhash);
702 builder.add_instruction(instruction);
703
704 let transaction = builder.build_v0(&[lookup_table]).unwrap();
705 let wire_bytes = transaction.serialize().unwrap();
706 let parsed = VersionedTransaction::deserialize_with_version(&wire_bytes).unwrap();
707
708 match parsed {
709 VersionedTransaction::V0 {
710 signatures,
711 message,
712 } => {
713 assert_eq!(signatures.len(), 1);
714 assert_eq!(message.address_table_lookups.len(), 1);
715 assert_eq!(message.address_table_lookups[0].writable_indexes, vec![0]);
716 assert_eq!(
717 message.address_table_lookups[0].readonly_indexes,
718 Vec::<u8>::new()
719 );
720 assert!(!message.account_keys.contains(&looked_up_account));
721 assert_eq!(message.instructions.len(), 1);
722 assert_eq!(message.instructions[0].data, vec![1, 2, 3]);
723 }
724 _ => panic!("expected v0 transaction"),
725 }
726 }
727
728 #[test]
729 fn test_add_instructions_helper() {
730 let fee_payer = payer_pubkey();
731 let recent_blockhash = test_blockhash();
732 let recipient = random_pubkey();
733
734 let ix1 = transfer(&fee_payer, &recipient, 1);
735 let ix2 = transfer(&fee_payer, &recipient, 2);
736
737 let mut builder = TransactionBuilder::new(fee_payer, recent_blockhash);
738 builder.add_instructions(vec![ix1, ix2]);
739
740 let tx = builder.build().unwrap();
741 assert_eq!(tx.message.instructions.len(), 2);
742 }
743
744 #[test]
745 fn test_v0_builder_real_world_regression_case() {
746 const REAL_TX_BASE64: &str = "AlF6Dlk4UjQD0xek1R2X8/hcORMjfzZ7/Vmql3hZcmM3+wwWrtvNkbqDFGZqJyFQxlNopEYLGJ3Oo/9gTDqylwOaaKU6sUi0z0x/4AIr2bEbk4F0Bb3eQnlZB2Pd4fwON80kvuBSbQPthCRffekiFXCnIXQUNFcuW3YDiZP0o0oBgAIACA2mI04pxqQuMUitv1NuRlK9ZWJWaV1k+p/LfT3tvKJ+fbIxWsd0GlHg175uFfLQ+Y+1DxMT48DDYU+4V77WYfZ1G4LkfQewG7EXCfCqmCEkGyByWhJU1GOFbK7yr0N338lnQQQP5AeqsFBGoH5xsx9hmNdlxN72v4J91uC6Ksvw/j23WlYbqpa0+YWZyJHXFuu3ghb5vWc1zPY3lpthsJywjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBHnVW/IxwG7udMVuzmgVB/2xst6j9I5RArHNola8E49RPixdukLDvYMw2r2DTumX5VA1ifoAVfXgkOTnLswDErQ/+if11/ZKdMCbHylYed5LCas238ndUUsyGqezjOXot/ord4dFsTTM4tnKRq1DlX7l6IZI9NWIfD9sANaw+DcFSlNamSkhBk0k6HFg2jh8fDW13bySu4HkH6hAQQVEjeSccqqE9cRgyD3i0H5PnVvX+q6L+uN2Xdbz16thksnaBgUGABAREwYHAQEGAgECDAIAAACApL8HAAAAAAcBAgERCBoHCQECAwQQFBMICAoIFRYNCQ4PAwQUEwcHFyXBIJszQdacgQMBAAAAWQFkAAGApL8HAAAAAGzHtAAAAAAAyAAACwwAAREYGRMQEhoHBQYonVNwIb8yqyWioGpTHjinCEcrRIIzlc3YWKd5g/z9UQsz2pcScMC7SQwAQjB4YzBiZDEyNDczNjVlM2Q2MTMyM2IxYTYyM2YwMzI0MDUzMzU0Yjk2MTJhODkzNTg3YjdlYTMyNjNlN2JhMTNiNAJ5QE4t+Dvx0UlyGT++v3V9s/1gQI0crEMfwbwNXZBmFgPb3xcF3gjcFuH5CleX0p1W2E0BwNC64/nFjEaXTuuVyg5P1Sf64f/vAgMMAwcDAQIA";
748 const STATIC_KEYS: [&str; 13] = [
749 "CBXuKTC3JAHjCvUeCXF2mXJazBqATDExQRxZi1iqQcDa",
750 "CzbDjxK4wqSpBuKfocC9vUgpzfhVEGPh8EihXbQkophA",
751 "2rPmeokZcYM8F3roghsoPYNqSYi32QyrNA9Lm7gN8TDa",
752 "7x4VcEX8aLd3kFsNWULTp1qFgVtDwyWSxpTGQkoMM6XX",
753 "59v2cSbCsnyaWymLnsq6TWzE6cEN5KJYNTBNrcP4smRH",
754 "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
755 "11111111111111111111111111111111",
756 "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
757 "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
758 "6U91aKa8pmMxkJwBCfPTmUEfZi6dHe7DcFq2ALvB2tbB",
759 "D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf",
760 "DPArtTLbEqa6EuXHfL5UFLBZhFjiEXWRudhvXDrjwXUr",
761 "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
762 ];
763 const LOADED_WRITABLE: [&str; 6] = [
764 "FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n",
765 "5pVN5XZB8cYBjNLFrsBCPWkCQBan5K5Mq2dWGzwPgGJV",
766 "9t4P5wMwfFkyn92Z7hf463qYKEZf8ERVZsGBEPNp8uJx",
767 "H2DG3qk1cRqBUmRNjJ2fsGrGs47NQk5VRBLt1AevW8m2",
768 "6GpvpHXBJA7pW8gP9KEXBJ2spNyydmbY5Q4nbdoo5TeT",
769 "4nvJ5zWdVspxJiNZzB127U6amPH98SFFkBx2JZrAduia",
770 ];
771 const LOADED_READONLY: [&str; 8] = [
772 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
773 "So11111111111111111111111111111111111111112",
774 "TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH",
775 "8ekCy2jHHUbW2yeNGFWYJT9Hm9FW7SvZcZK66dSZCDiF",
776 "Sysvar1nstructions1111111111111111111111111",
777 "Dodg2HifwU8rmaVVyMyUZDGTRbqAJTyVYxXPwcbNpBKc",
778 "7uTT8Xi5RWXzy7h9XL244GRgEycDYDhLjr3ZyNdXi8pZ",
779 "99vQwtBwYtrqqD9YSXbdum3KBdxPAVxYTaQ3cfnJSrN2",
780 ];
781
782 let wire = STANDARD.decode(REAL_TX_BASE64).unwrap();
783 let mut expected_tx = VersionedTransaction::deserialize_with_version(&wire).unwrap();
784 for signature in expected_tx.signatures_mut() {
785 *signature = SignatureBytes::default();
786 }
787
788 let (fee_payer, recent_blockhash) = match &expected_tx {
789 VersionedTransaction::V0 { message, .. } => {
790 (message.account_keys[0], message.recent_blockhash)
791 }
792 _ => panic!("expected V0 transaction fixture"),
793 };
794
795 let combined_accounts: Vec<AccountMeta> = STATIC_KEYS
796 .iter()
797 .chain(LOADED_WRITABLE.iter())
798 .chain(LOADED_READONLY.iter())
799 .copied()
800 .map(|value| Pubkey::from_base58(value).unwrap())
801 .enumerate()
802 .map(|(index, key)| account_meta_for_combined_index(index, key))
803 .collect();
804
805 let lookup_tables = vec![
806 lookup_table_from_sparse_entries(
807 "9AKCoNoAGYLW71TwTHY9e7KrZUWWL3c7VtHKb66NT3EV",
808 &[
809 (219, "FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n"),
810 (223, "5pVN5XZB8cYBjNLFrsBCPWkCQBan5K5Mq2dWGzwPgGJV"),
811 (23, "9t4P5wMwfFkyn92Z7hf463qYKEZf8ERVZsGBEPNp8uJx"),
812 (222, "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
813 (8, "So11111111111111111111111111111111111111112"),
814 (220, "TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH"),
815 (22, "8ekCy2jHHUbW2yeNGFWYJT9Hm9FW7SvZcZK66dSZCDiF"),
816 (225, "Sysvar1nstructions1111111111111111111111111"),
817 ],
818 ),
819 lookup_table_from_sparse_entries(
820 "Hm9fUgcn7qwDaiNTFiGh6pNtVATgnaRcmK6Bbx6EMZfP",
821 &[
822 (12, "H2DG3qk1cRqBUmRNjJ2fsGrGs47NQk5VRBLt1AevW8m2"),
823 (3, "6GpvpHXBJA7pW8gP9KEXBJ2spNyydmbY5Q4nbdoo5TeT"),
824 (7, "4nvJ5zWdVspxJiNZzB127U6amPH98SFFkBx2JZrAduia"),
825 (1, "Dodg2HifwU8rmaVVyMyUZDGTRbqAJTyVYxXPwcbNpBKc"),
826 (2, "7uTT8Xi5RWXzy7h9XL244GRgEycDYDhLjr3ZyNdXi8pZ"),
827 (0, "99vQwtBwYtrqqD9YSXbdum3KBdxPAVxYTaQ3cfnJSrN2"),
828 ],
829 ),
830 ];
831
832 let instructions = vec![
833 instruction_from_decoded_tx(5, &[0, 16, 17, 19, 6, 7], "2", &combined_accounts),
834 instruction_from_decoded_tx(6, &[1, 2], "3Bxs4NNfTBw5NH5H", &combined_accounts),
835 instruction_from_decoded_tx(7, &[2], "J", &combined_accounts),
836 instruction_from_decoded_tx(
837 8,
838 &[
839 7, 9, 1, 2, 3, 4, 16, 20, 19, 8, 8, 10, 8, 21, 22, 13, 9, 14, 15, 3, 4, 20, 19,
840 7, 7, 23,
841 ],
842 "7UR2vxkjV6WhbmWvkCZQvQJKVhT964yPqVRoTBAPv678iyHS8LF",
843 &combined_accounts,
844 ),
845 instruction_from_decoded_tx(
846 11,
847 &[0, 1, 17, 24, 25, 19, 16, 18, 26, 7, 5, 6],
848 "8pPpkivb1mTLA5APTUWQU2CsG1oYxcnh5C8fQsYux2BVVxSXuFvWtLx",
849 &combined_accounts,
850 ),
851 instruction_from_decoded_tx(
852 12,
853 &[],
854 "KszMTKrqxdHWZULtjrD9cmodXEnC1UboEfkgMRLSGuuPLDYWo8BrqcbfRddG4w18gsf1sZR69vK1mKhXyvNCTZxwsq",
855 &combined_accounts,
856 ),
857 ];
858
859 let mut builder = TransactionBuilder::new(fee_payer, recent_blockhash);
860 builder.add_instructions(instructions);
861 let rebuilt_tx = builder.build_v0(&lookup_tables).unwrap();
862
863 assert_eq!(
864 rebuilt_tx.serialize().unwrap(),
865 expected_tx.serialize().unwrap()
866 );
867 }
868
869 #[test]
870 fn test_build_rejects_more_than_256_distinct_accounts() {
871 let recent_blockhash = test_blockhash();
872
873 let distinct_pubkey = |index: u32| -> Pubkey {
874 let mut bytes = [0u8; 32];
875 bytes[0..4].copy_from_slice(&index.to_le_bytes());
876 Pubkey::new(bytes)
877 };
878
879 let fee_payer = distinct_pubkey(0);
880 let program_id = distinct_pubkey(1);
881
882 let accounts: Vec<AccountMeta> = (2..257)
883 .map(|index| AccountMeta::new_writable(distinct_pubkey(index)))
884 .collect();
885
886 let instruction = Instruction {
887 program_id,
888 accounts,
889 data: vec![],
890 };
891
892 let mut builder = TransactionBuilder::new(fee_payer, recent_blockhash);
893 builder.add_instruction(instruction);
894
895 let result = builder.build();
896 assert!(
897 matches!(result, Err(SolanaError::InvalidMessage)),
898 "expected build() to reject 257 distinct accounts with InvalidMessage, got {result:?}"
899 );
900 }
901
902 #[test]
903 fn test_build_rejects_256_required_signers() {
904 let recent_blockhash = test_blockhash();
905
906 let distinct_pubkey = |index: u32| -> Pubkey {
907 let mut bytes = [0u8; 32];
908 bytes[0..4].copy_from_slice(&index.to_le_bytes());
909 Pubkey::new(bytes)
910 };
911
912 let fee_payer = distinct_pubkey(0);
913 let signer_pubkeys: Vec<Pubkey> = (1..256).map(distinct_pubkey).collect();
915 let program_id = signer_pubkeys[0];
916
917 let accounts: Vec<AccountMeta> = signer_pubkeys
918 .iter()
919 .map(|pubkey| AccountMeta::new_signer_writable(*pubkey))
920 .collect();
921
922 let instruction = Instruction {
923 program_id,
924 accounts,
925 data: vec![],
926 };
927
928 let mut builder = TransactionBuilder::new(fee_payer, recent_blockhash);
929 builder.add_instruction(instruction);
930
931 let result = builder.build();
932 assert!(
933 matches!(result, Err(SolanaError::InvalidMessage)),
934 "expected build() to reject 256 required signers with InvalidMessage, got {result:?}"
935 );
936 }
937}