1#[cfg(not(target_os = "solana"))]
2use crate::{
3 v0::{LoadedAddresses, MessageAddressTableLookup},
4 AddressLookupTableAccount,
5};
6use {
7 crate::{inline_nonce::is_advance_nonce_instruction_data, MessageHeader},
8 core::fmt,
9 solana_address::Address,
10 solana_instruction::Instruction,
11 solana_sdk_ids::system_program,
12 std::collections::BTreeMap,
13};
14
15#[derive(Default, Debug, Clone, PartialEq, Eq)]
17pub(crate) struct CompiledKeys {
18 payer: Option<Address>,
19 key_meta_map: BTreeMap<Address, CompiledKeyMeta>,
20}
21
22#[cfg_attr(target_os = "solana", allow(dead_code))]
23#[derive(PartialEq, Debug, Eq, Clone)]
24pub enum CompileError {
25 AccountIndexOverflow,
26 AddressTableLookupIndexOverflow,
27 UnknownInstructionKey(Address),
28}
29
30impl core::error::Error for CompileError {}
31
32impl fmt::Display for CompileError {
33 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
34 match self {
35 CompileError::AccountIndexOverflow => {
36 f.write_str("account index overflowed during compilation")
37 }
38 CompileError::AddressTableLookupIndexOverflow => {
39 f.write_str("address lookup table index overflowed during compilation")
40 }
41 CompileError::UnknownInstructionKey(key) => f.write_fmt(format_args!(
42 "encountered unknown account key `{key}` during instruction compilation",
43 )),
44 }
45 }
46}
47
48#[derive(Default, Debug, Clone, PartialEq, Eq)]
49struct CompiledKeyMeta {
50 is_signer: bool,
51 is_writable: bool,
52 is_invoked: bool,
53 is_nonce: bool,
54}
55
56impl CompiledKeys {
57 pub(crate) fn compile(instructions: &[Instruction], payer: Option<Address>) -> Self {
60 let mut key_meta_map = BTreeMap::<Address, CompiledKeyMeta>::new();
61 for ix in instructions {
62 let meta = key_meta_map.entry(ix.program_id).or_default();
63 meta.is_invoked = true;
64 for account_meta in &ix.accounts {
65 let meta = key_meta_map.entry(account_meta.pubkey).or_default();
66 meta.is_signer |= account_meta.is_signer;
67 meta.is_writable |= account_meta.is_writable;
68 }
69 }
70 if let Some(nonce_pubkey) = get_nonce_pubkey(instructions) {
71 let meta = key_meta_map.entry(*nonce_pubkey).or_default();
72 meta.is_nonce = true;
73 }
74 if let Some(payer) = &payer {
75 let meta = key_meta_map.entry(*payer).or_default();
76 meta.is_signer = true;
77 meta.is_writable = true;
78 }
79 Self {
80 payer,
81 key_meta_map,
82 }
83 }
84
85 pub(crate) fn try_into_message_components(
86 self,
87 ) -> Result<(MessageHeader, Vec<Address>), CompileError> {
88 let try_into_u8 = |num: usize| -> Result<u8, CompileError> {
89 u8::try_from(num).map_err(|_| CompileError::AccountIndexOverflow)
90 };
91
92 let Self {
93 payer,
94 mut key_meta_map,
95 } = self;
96
97 if let Some(payer) = &payer {
98 key_meta_map.remove_entry(payer);
99 }
100
101 let writable_signer_keys: Vec<Address> = payer
102 .into_iter()
103 .chain(
104 key_meta_map
105 .iter()
106 .filter_map(|(key, meta)| (meta.is_signer && meta.is_writable).then_some(*key)),
107 )
108 .collect();
109 let readonly_signer_keys: Vec<Address> = key_meta_map
110 .iter()
111 .filter_map(|(key, meta)| (meta.is_signer && !meta.is_writable).then_some(*key))
112 .collect();
113 let writable_non_signer_keys: Vec<Address> = key_meta_map
114 .iter()
115 .filter_map(|(key, meta)| (!meta.is_signer && meta.is_writable).then_some(*key))
116 .collect();
117 let readonly_non_signer_keys: Vec<Address> = key_meta_map
118 .iter()
119 .filter_map(|(key, meta)| (!meta.is_signer && !meta.is_writable).then_some(*key))
120 .collect();
121
122 let signers_len = writable_signer_keys
123 .len()
124 .saturating_add(readonly_signer_keys.len());
125
126 let header = MessageHeader {
127 num_required_signatures: try_into_u8(signers_len)?,
128 num_readonly_signed_accounts: try_into_u8(readonly_signer_keys.len())?,
129 num_readonly_unsigned_accounts: try_into_u8(readonly_non_signer_keys.len())?,
130 };
131
132 let static_account_keys = std::iter::empty()
133 .chain(writable_signer_keys)
134 .chain(readonly_signer_keys)
135 .chain(writable_non_signer_keys)
136 .chain(readonly_non_signer_keys)
137 .collect();
138
139 Ok((header, static_account_keys))
140 }
141
142 #[cfg(not(target_os = "solana"))]
143 pub(crate) fn try_extract_table_lookup(
144 &mut self,
145 lookup_table_account: &AddressLookupTableAccount,
146 ) -> Result<Option<(MessageAddressTableLookup, LoadedAddresses)>, CompileError> {
147 let (writable_indexes, drained_writable_keys) = self
148 .try_drain_keys_found_in_lookup_table(&lookup_table_account.addresses, |meta| {
149 !meta.is_signer && !meta.is_invoked && !meta.is_nonce && meta.is_writable
150 })?;
151 let (readonly_indexes, drained_readonly_keys) = self
152 .try_drain_keys_found_in_lookup_table(&lookup_table_account.addresses, |meta| {
153 !meta.is_signer && !meta.is_invoked && !meta.is_nonce && !meta.is_writable
154 })?;
155
156 if writable_indexes.is_empty() && readonly_indexes.is_empty() {
158 return Ok(None);
159 }
160
161 Ok(Some((
162 MessageAddressTableLookup {
163 account_key: lookup_table_account.key,
164 writable_indexes,
165 readonly_indexes,
166 },
167 LoadedAddresses {
168 writable: drained_writable_keys,
169 readonly: drained_readonly_keys,
170 },
171 )))
172 }
173
174 #[cfg(not(target_os = "solana"))]
175 fn try_drain_keys_found_in_lookup_table(
176 &mut self,
177 lookup_table_addresses: &[Address],
178 key_meta_filter: impl Fn(&CompiledKeyMeta) -> bool,
179 ) -> Result<(Vec<u8>, Vec<Address>), CompileError> {
180 let mut lookup_table_indexes = Vec::new();
181 let mut drained_keys = Vec::new();
182
183 for search_key in self
184 .key_meta_map
185 .iter()
186 .filter_map(|(key, meta)| key_meta_filter(meta).then_some(key))
187 {
188 for (key_index, key) in lookup_table_addresses.iter().enumerate() {
189 if key == search_key {
190 let lookup_table_index = u8::try_from(key_index)
191 .map_err(|_| CompileError::AddressTableLookupIndexOverflow)?;
192
193 lookup_table_indexes.push(lookup_table_index);
194 drained_keys.push(*search_key);
195 break;
196 }
197 }
198 }
199
200 for key in &drained_keys {
201 self.key_meta_map.remove_entry(key);
202 }
203
204 Ok((lookup_table_indexes, drained_keys))
205 }
206}
207
208const NONCED_TX_MARKER_IX_INDEX: usize = 0;
210
211fn get_nonce_pubkey(instructions: &[Instruction]) -> Option<&Address> {
212 let ix = instructions.get(NONCED_TX_MARKER_IX_INDEX)?;
213 if !system_program::check_id(&ix.program_id) {
214 return None;
215 }
216
217 if !is_advance_nonce_instruction_data(&ix.data) {
218 return None;
219 }
220
221 ix.accounts.first().map(|meta| &meta.pubkey)
222}
223
224#[cfg(test)]
225mod tests {
226 use {
227 super::*, bitflags::bitflags, solana_instruction::AccountMeta,
228 solana_sdk_ids::sysvar::recent_blockhashes,
229 solana_system_interface::instruction::advance_nonce_account,
230 };
231
232 static_assertions::const_assert_eq!(
233 NONCED_TX_MARKER_IX_INDEX,
234 solana_nonce::NONCED_TX_MARKER_IX_INDEX as usize
235 );
236
237 bitflags! {
238 #[derive(Clone, Copy)]
239 pub struct KeyFlags: u8 {
240 const SIGNER = 0b00000001;
241 const WRITABLE = 0b00000010;
242 const INVOKED = 0b00000100;
243 const NONCE = 0b00001000;
244 }
245 }
246
247 impl From<KeyFlags> for CompiledKeyMeta {
248 fn from(flags: KeyFlags) -> Self {
249 Self {
250 is_signer: flags.contains(KeyFlags::SIGNER),
251 is_writable: flags.contains(KeyFlags::WRITABLE),
252 is_invoked: flags.contains(KeyFlags::INVOKED),
253 is_nonce: flags.contains(KeyFlags::NONCE),
254 }
255 }
256 }
257
258 #[test]
259 fn test_compile_with_dups() {
260 let program_id0 = Address::new_unique();
261 let program_id1 = Address::new_unique();
262 let program_id2 = Address::new_unique();
263 let program_id3 = Address::new_unique();
264 let id0 = Address::new_unique();
265 let id1 = Address::new_unique();
266 let id2 = Address::new_unique();
267 let id3 = Address::new_unique();
268 let compiled_keys = CompiledKeys::compile(
269 &[
270 Instruction::new_with_bincode(
271 program_id0,
272 &0,
273 vec![
274 AccountMeta::new_readonly(id0, false),
275 AccountMeta::new_readonly(id1, true),
276 AccountMeta::new(id2, false),
277 AccountMeta::new(id3, true),
278 AccountMeta::new_readonly(id0, false),
280 AccountMeta::new_readonly(id1, true),
281 AccountMeta::new(id2, false),
282 AccountMeta::new(id3, true),
283 AccountMeta::new_readonly(program_id0, false),
285 AccountMeta::new_readonly(program_id1, true),
286 AccountMeta::new(program_id2, false),
287 AccountMeta::new(program_id3, true),
288 ],
289 ),
290 Instruction::new_with_bincode(program_id1, &0, vec![]),
291 Instruction::new_with_bincode(program_id2, &0, vec![]),
292 Instruction::new_with_bincode(program_id3, &0, vec![]),
293 ],
294 None,
295 );
296
297 assert_eq!(
298 compiled_keys,
299 CompiledKeys {
300 payer: None,
301 key_meta_map: BTreeMap::from([
302 (id0, KeyFlags::empty().into()),
303 (id1, KeyFlags::SIGNER.into()),
304 (id2, KeyFlags::WRITABLE.into()),
305 (id3, (KeyFlags::SIGNER | KeyFlags::WRITABLE).into()),
306 (program_id0, KeyFlags::INVOKED.into()),
307 (program_id1, (KeyFlags::INVOKED | KeyFlags::SIGNER).into()),
308 (program_id2, (KeyFlags::INVOKED | KeyFlags::WRITABLE).into()),
309 (
310 program_id3,
311 (KeyFlags::INVOKED | KeyFlags::WRITABLE | KeyFlags::SIGNER).into()
312 ),
313 ]),
314 }
315 );
316 }
317
318 #[test]
319 fn test_compile_with_dup_payer() {
320 let program_id = Address::new_unique();
321 let payer = Address::new_unique();
322 let compiled_keys = CompiledKeys::compile(
323 &[Instruction::new_with_bincode(
324 program_id,
325 &0,
326 vec![AccountMeta::new_readonly(payer, false)],
327 )],
328 Some(payer),
329 );
330 assert_eq!(
331 compiled_keys,
332 CompiledKeys {
333 payer: Some(payer),
334 key_meta_map: BTreeMap::from([
335 (payer, (KeyFlags::SIGNER | KeyFlags::WRITABLE).into()),
336 (program_id, KeyFlags::INVOKED.into()),
337 ]),
338 }
339 );
340 }
341
342 #[test]
343 fn test_compile_with_dup_signer_mismatch() {
344 let program_id = Address::new_unique();
345 let id0 = Address::new_unique();
346 let compiled_keys = CompiledKeys::compile(
347 &[Instruction::new_with_bincode(
348 program_id,
349 &0,
350 vec![AccountMeta::new(id0, false), AccountMeta::new(id0, true)],
351 )],
352 None,
353 );
354
355 assert_eq!(
357 compiled_keys,
358 CompiledKeys {
359 payer: None,
360 key_meta_map: BTreeMap::from([
361 (id0, (KeyFlags::SIGNER | KeyFlags::WRITABLE).into()),
362 (program_id, KeyFlags::INVOKED.into()),
363 ]),
364 }
365 );
366 }
367
368 #[test]
369 fn test_compile_with_dup_signer_writable_mismatch() {
370 let program_id = Address::new_unique();
371 let id0 = Address::new_unique();
372 let compiled_keys = CompiledKeys::compile(
373 &[Instruction::new_with_bincode(
374 program_id,
375 &0,
376 vec![
377 AccountMeta::new_readonly(id0, true),
378 AccountMeta::new(id0, true),
379 ],
380 )],
381 None,
382 );
383
384 assert_eq!(
386 compiled_keys,
387 CompiledKeys {
388 payer: None,
389 key_meta_map: BTreeMap::from([
390 (id0, (KeyFlags::SIGNER | KeyFlags::WRITABLE).into()),
391 (program_id, KeyFlags::INVOKED.into()),
392 ]),
393 }
394 );
395 }
396
397 #[test]
398 fn test_compile_with_dup_nonsigner_writable_mismatch() {
399 let program_id = Address::new_unique();
400 let id0 = Address::new_unique();
401 let compiled_keys = CompiledKeys::compile(
402 &[
403 Instruction::new_with_bincode(
404 program_id,
405 &0,
406 vec![
407 AccountMeta::new_readonly(id0, false),
408 AccountMeta::new(id0, false),
409 ],
410 ),
411 Instruction::new_with_bincode(program_id, &0, vec![AccountMeta::new(id0, false)]),
412 ],
413 None,
414 );
415
416 assert_eq!(
418 compiled_keys,
419 CompiledKeys {
420 payer: None,
421 key_meta_map: BTreeMap::from([
422 (id0, KeyFlags::WRITABLE.into()),
423 (program_id, KeyFlags::INVOKED.into()),
424 ]),
425 }
426 );
427 }
428
429 #[test]
430 fn test_compile_with_nonce_instruction() {
431 let nonce_pubkey = Address::new_unique();
432 let nonce_authority = Address::new_unique();
433 let compiled_keys = CompiledKeys::compile(
434 &[advance_nonce_account(&nonce_pubkey, &nonce_authority)],
435 Some(nonce_authority),
436 );
437
438 assert_eq!(
439 compiled_keys,
440 CompiledKeys {
441 payer: Some(nonce_authority),
442 key_meta_map: BTreeMap::from([
443 (nonce_pubkey, (KeyFlags::NONCE | KeyFlags::WRITABLE).into()),
444 (
445 nonce_authority,
446 (KeyFlags::SIGNER | KeyFlags::WRITABLE).into()
447 ),
448 (system_program::id(), KeyFlags::INVOKED.into()),
449 (recent_blockhashes::id(), CompiledKeyMeta::default())
450 ]),
451 }
452 );
453 }
454
455 #[test]
456 fn test_try_into_message_components() {
457 let keys = vec![
458 Address::new_unique(),
459 Address::new_unique(),
460 Address::new_unique(),
461 Address::new_unique(),
462 ];
463
464 let compiled_keys = CompiledKeys {
465 payer: None,
466 key_meta_map: BTreeMap::from([
467 (keys[0], (KeyFlags::SIGNER | KeyFlags::WRITABLE).into()),
468 (keys[1], KeyFlags::SIGNER.into()),
469 (keys[2], KeyFlags::WRITABLE.into()),
470 (keys[3], KeyFlags::empty().into()),
471 ]),
472 };
473
474 let result = compiled_keys.try_into_message_components();
475 assert_eq!(result.as_ref().err(), None);
476 let (header, static_keys) = result.unwrap();
477
478 assert_eq!(static_keys, keys);
479 assert_eq!(
480 header,
481 MessageHeader {
482 num_required_signatures: 2,
483 num_readonly_signed_accounts: 1,
484 num_readonly_unsigned_accounts: 1,
485 }
486 );
487 }
488
489 #[test]
490 fn test_try_into_message_components_with_too_many_keys() {
491 const TOO_MANY_KEYS: usize = 257;
492
493 for key_flags in [
494 KeyFlags::WRITABLE | KeyFlags::SIGNER,
495 KeyFlags::SIGNER,
496 KeyFlags::empty(),
498 ] {
499 let test_keys = CompiledKeys {
500 payer: None,
501 key_meta_map: BTreeMap::from_iter(
502 (0..TOO_MANY_KEYS).map(|_| (Address::new_unique(), key_flags.into())),
503 ),
504 };
505
506 assert_eq!(
507 test_keys.try_into_message_components(),
508 Err(CompileError::AccountIndexOverflow)
509 );
510 }
511 }
512
513 #[test]
514 fn test_try_extract_table_lookup() {
515 let keys = vec![
516 Address::new_unique(),
517 Address::new_unique(),
518 Address::new_unique(),
519 Address::new_unique(),
520 Address::new_unique(),
521 Address::new_unique(),
522 ];
523
524 let mut compiled_keys = CompiledKeys {
525 payer: None,
526 key_meta_map: BTreeMap::from([
527 (keys[0], (KeyFlags::SIGNER | KeyFlags::WRITABLE).into()),
528 (keys[1], KeyFlags::SIGNER.into()),
529 (keys[2], KeyFlags::WRITABLE.into()),
530 (keys[3], KeyFlags::empty().into()),
531 (keys[4], (KeyFlags::INVOKED | KeyFlags::WRITABLE).into()),
532 (keys[5], (KeyFlags::INVOKED).into()),
533 ]),
534 };
535
536 let addresses = [keys.clone(), keys.clone()].concat();
538 let lookup_table_account = AddressLookupTableAccount {
539 key: Address::new_unique(),
540 addresses,
541 };
542
543 assert_eq!(
544 compiled_keys.try_extract_table_lookup(&lookup_table_account),
545 Ok(Some((
546 MessageAddressTableLookup {
547 account_key: lookup_table_account.key,
548 writable_indexes: vec![2],
549 readonly_indexes: vec![3],
550 },
551 LoadedAddresses {
552 writable: vec![keys[2]],
553 readonly: vec![keys[3]],
554 },
555 )))
556 );
557
558 assert_eq!(compiled_keys.key_meta_map.len(), 4);
559 assert!(!compiled_keys.key_meta_map.contains_key(&keys[2]));
560 assert!(!compiled_keys.key_meta_map.contains_key(&keys[3]));
561 }
562
563 #[test]
564 fn test_try_extract_table_lookup_returns_none() {
565 let mut compiled_keys = CompiledKeys {
566 payer: None,
567 key_meta_map: BTreeMap::from([
568 (Address::new_unique(), KeyFlags::WRITABLE.into()),
569 (Address::new_unique(), KeyFlags::empty().into()),
570 ]),
571 };
572
573 let lookup_table_account = AddressLookupTableAccount {
574 key: Address::new_unique(),
575 addresses: vec![],
576 };
577
578 let expected_compiled_keys = compiled_keys.clone();
579 assert_eq!(
580 compiled_keys.try_extract_table_lookup(&lookup_table_account),
581 Ok(None)
582 );
583 assert_eq!(compiled_keys, expected_compiled_keys);
584 }
585
586 #[test]
587 fn test_try_extract_table_lookup_for_invalid_table() {
588 let writable_key = Address::new_unique();
589 let mut compiled_keys = CompiledKeys {
590 payer: None,
591 key_meta_map: BTreeMap::from([
592 (writable_key, KeyFlags::WRITABLE.into()),
593 (Address::new_unique(), KeyFlags::empty().into()),
594 ]),
595 };
596
597 const MAX_LENGTH_WITHOUT_OVERFLOW: usize = u8::MAX as usize + 1;
598 let mut addresses = vec![Address::default(); MAX_LENGTH_WITHOUT_OVERFLOW];
599 addresses.push(writable_key);
600
601 let lookup_table_account = AddressLookupTableAccount {
602 key: Address::new_unique(),
603 addresses,
604 };
605
606 let expected_compiled_keys = compiled_keys.clone();
607 assert_eq!(
608 compiled_keys.try_extract_table_lookup(&lookup_table_account),
609 Err(CompileError::AddressTableLookupIndexOverflow),
610 );
611 assert_eq!(compiled_keys, expected_compiled_keys);
612 }
613
614 #[test]
615 fn test_try_drain_keys_found_in_lookup_table() {
616 let orig_keys = [
617 Address::new_unique(),
618 Address::new_unique(),
619 Address::new_unique(),
620 Address::new_unique(),
621 Address::new_unique(),
622 ];
623
624 let mut compiled_keys = CompiledKeys {
625 payer: None,
626 key_meta_map: BTreeMap::from([
627 (orig_keys[0], KeyFlags::empty().into()),
628 (orig_keys[1], KeyFlags::WRITABLE.into()),
629 (orig_keys[2], KeyFlags::WRITABLE.into()),
630 (orig_keys[3], KeyFlags::empty().into()),
631 (orig_keys[4], KeyFlags::empty().into()),
632 ]),
633 };
634
635 let lookup_table_addresses = vec![
636 Address::new_unique(),
637 orig_keys[0],
638 Address::new_unique(),
639 orig_keys[4],
640 Address::new_unique(),
641 orig_keys[2],
642 Address::new_unique(),
643 ];
644
645 let drain_result = compiled_keys
646 .try_drain_keys_found_in_lookup_table(&lookup_table_addresses, |meta| {
647 !meta.is_writable
648 });
649 assert_eq!(drain_result.as_ref().err(), None);
650 let (lookup_table_indexes, drained_keys) = drain_result.unwrap();
651
652 assert_eq!(
653 compiled_keys.key_meta_map.keys().collect::<Vec<&_>>(),
654 vec![&orig_keys[1], &orig_keys[2], &orig_keys[3]]
655 );
656 assert_eq!(drained_keys, vec![orig_keys[0], orig_keys[4]]);
657 assert_eq!(lookup_table_indexes, vec![1, 3]);
658 }
659
660 #[test]
661 fn test_try_drain_keys_found_in_lookup_table_with_empty_keys() {
662 let mut compiled_keys = CompiledKeys::default();
663
664 let lookup_table_addresses = vec![
665 Address::new_unique(),
666 Address::new_unique(),
667 Address::new_unique(),
668 ];
669
670 let drain_result =
671 compiled_keys.try_drain_keys_found_in_lookup_table(&lookup_table_addresses, |_| true);
672 assert_eq!(drain_result.as_ref().err(), None);
673 let (lookup_table_indexes, drained_keys) = drain_result.unwrap();
674
675 assert!(drained_keys.is_empty());
676 assert!(lookup_table_indexes.is_empty());
677 }
678
679 #[test]
680 fn test_try_drain_keys_found_in_lookup_table_with_empty_table() {
681 let original_keys = [
682 Address::new_unique(),
683 Address::new_unique(),
684 Address::new_unique(),
685 ];
686
687 let mut compiled_keys = CompiledKeys {
688 payer: None,
689 key_meta_map: BTreeMap::from_iter(
690 original_keys
691 .iter()
692 .map(|key| (*key, CompiledKeyMeta::default())),
693 ),
694 };
695
696 let lookup_table_addresses = vec![];
697
698 let drain_result =
699 compiled_keys.try_drain_keys_found_in_lookup_table(&lookup_table_addresses, |_| true);
700 assert_eq!(drain_result.as_ref().err(), None);
701 let (lookup_table_indexes, drained_keys) = drain_result.unwrap();
702
703 assert_eq!(compiled_keys.key_meta_map.len(), original_keys.len());
704 assert!(drained_keys.is_empty());
705 assert!(lookup_table_indexes.is_empty());
706 }
707
708 #[test]
709 fn test_try_drain_keys_found_in_lookup_table_with_too_many_addresses() {
710 let key = Address::new_unique();
711 let mut compiled_keys = CompiledKeys {
712 payer: None,
713 key_meta_map: BTreeMap::from([(key, CompiledKeyMeta::default())]),
714 };
715
716 const MAX_LENGTH_WITHOUT_OVERFLOW: usize = u8::MAX as usize + 1;
717 let mut lookup_table_addresses = vec![Address::default(); MAX_LENGTH_WITHOUT_OVERFLOW];
718 lookup_table_addresses.push(key);
719
720 let drain_result =
721 compiled_keys.try_drain_keys_found_in_lookup_table(&lookup_table_addresses, |_| true);
722 assert_eq!(
723 drain_result.err(),
724 Some(CompileError::AddressTableLookupIndexOverflow)
725 );
726 }
727}