Skip to main content

hopper_runtime/
token_metadata_ix.rs

1//! Token-metadata and token-group interface instructions.
2//!
3//! Both interfaces are implemented by Token-2022 itself (a mint that
4//! carries the `TokenMetadata` or `TokenGroup` extension is its own
5//! metadata or group account) and can be implemented by any other program,
6//! so every builder here targets Token-2022 from `invoke()` and any
7//! interface program from `invoke_on_program`.
8//!
9//! Instructions are selected by an 8-byte discriminator (the first eight
10//! bytes of the SHA-256 of the interface's hash input); the payload is
11//! Borsh: a string is a little-endian `u32` length followed by its bytes,
12//! an `Option<u64>` is a tag byte and, when present, the value, and an
13//! optional address is 32 bytes that are all zero when absent.
14//!
15//! The variable-length instructions are encoded on the stack. The whole
16//! instruction must fit [`MAX_METADATA_INSTRUCTION_DATA`] bytes; a longer
17//! one is refused with `InvalidArgument` before the CPI. Token-2022
18//! reallocates the mint to hold the metadata and does not fund it: the
19//! caller transfers the additional rent to the mint first.
20
21use crate::account::AccountView;
22use crate::address::Address;
23use crate::error::ProgramError;
24use crate::instruction::{InstructionAccount, Signer};
25use crate::token::{Invoke, TokenInstruction, TokenSink, TOKEN_2022_PROGRAM_ID};
26use crate::ProgramResult;
27
28/// The largest instruction data a metadata builder encodes.
29pub const MAX_METADATA_INSTRUCTION_DATA: usize = 512;
30
31/// `spl_token_metadata_interface:initialize_account`.
32pub const DISC_METADATA_INITIALIZE: [u8; 8] = [210, 225, 30, 162, 88, 184, 77, 141];
33/// `spl_token_metadata_interface:updating_field`.
34pub const DISC_METADATA_UPDATE_FIELD: [u8; 8] = [221, 233, 49, 45, 181, 202, 220, 200];
35/// `spl_token_metadata_interface:remove_key_ix`.
36pub const DISC_METADATA_REMOVE_KEY: [u8; 8] = [234, 18, 32, 56, 89, 141, 37, 181];
37/// `spl_token_metadata_interface:update_the_authority`.
38pub const DISC_METADATA_UPDATE_AUTHORITY: [u8; 8] = [215, 228, 166, 228, 84, 100, 86, 123];
39/// `spl_token_metadata_interface:emitter`.
40pub const DISC_METADATA_EMIT: [u8; 8] = [250, 166, 180, 250, 13, 12, 184, 70];
41/// `spl_token_group_interface:initialize_token_group`.
42pub const DISC_GROUP_INITIALIZE: [u8; 8] = [121, 113, 108, 39, 54, 51, 0, 4];
43/// `spl_token_group_interface:update_group_max_size`.
44pub const DISC_GROUP_UPDATE_MAX_SIZE: [u8; 8] = [108, 37, 171, 143, 248, 30, 18, 110];
45/// `spl_token_group_interface:update_authority`.
46pub const DISC_GROUP_UPDATE_AUTHORITY: [u8; 8] = [161, 105, 88, 1, 237, 221, 216, 203];
47/// `spl_token_group_interface:initialize_member`.
48pub const DISC_GROUP_INITIALIZE_MEMBER: [u8; 8] = [152, 32, 222, 176, 223, 237, 116, 134];
49
50/// A bounded instruction-data writer. Every push checks the remaining
51/// room, so an oversized string is an error, never a truncation.
52pub struct Data {
53    buf: [u8; MAX_METADATA_INSTRUCTION_DATA],
54    len: usize,
55}
56
57impl Data {
58    #[inline(always)]
59    fn new(discriminator: &[u8; 8]) -> Self {
60        let mut buf = [0u8; MAX_METADATA_INSTRUCTION_DATA];
61        buf[..8].copy_from_slice(discriminator);
62        Self { buf, len: 8 }
63    }
64
65    #[inline(always)]
66    fn bytes(&mut self, bytes: &[u8]) -> ProgramResult {
67        let end = self
68            .len
69            .checked_add(bytes.len())
70            .ok_or(ProgramError::InvalidArgument)?;
71        if end > MAX_METADATA_INSTRUCTION_DATA {
72            return Err(ProgramError::InvalidArgument);
73        }
74        self.buf[self.len..end].copy_from_slice(bytes);
75        self.len = end;
76        Ok(())
77    }
78
79    /// A Borsh string: `u32` length, then the bytes.
80    #[inline(always)]
81    fn string(&mut self, text: &str) -> ProgramResult {
82        let len = u32::try_from(text.len()).map_err(|_| ProgramError::InvalidArgument)?;
83        self.bytes(&len.to_le_bytes())?;
84        self.bytes(text.as_bytes())
85    }
86
87    /// An optional address: the address, or 32 zero bytes. A present
88    /// all-zero address would read back as absent, so it is refused.
89    #[inline(always)]
90    fn nullable(&mut self, address: Option<&Address>) -> ProgramResult {
91        match address {
92            Some(address) if address.as_array() == &[0u8; 32] => Err(ProgramError::InvalidArgument),
93            Some(address) => self.bytes(address.as_array()),
94            None => self.bytes(&[0u8; 32]),
95        }
96    }
97
98    /// A Borsh `Option<u64>`.
99    #[inline(always)]
100    fn option_u64(&mut self, value: Option<u64>) -> ProgramResult {
101        match value {
102            Some(value) => {
103                self.bytes(&[1])?;
104                self.bytes(&value.to_le_bytes())
105            }
106            None => self.bytes(&[0]),
107        }
108    }
109
110    /// The encoded bytes.
111    #[inline(always)]
112    pub fn as_slice(&self) -> &[u8] {
113        &self.buf[..self.len]
114    }
115}
116
117/// The field an [`UpdateMetadataField`] writes: one of the three base
118/// fields or an additional key.
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120pub enum MetadataField<'a> {
121    Name,
122    Symbol,
123    Uri,
124    Key(&'a str),
125}
126
127/// Byte-exact encoders, shared by the builders and the tests.
128pub mod encoders {
129    use super::*;
130
131    /// `[disc][name][symbol][uri]`, each a Borsh string.
132    #[inline(always)]
133    pub fn encode_initialize(name: &str, symbol: &str, uri: &str) -> Result<Data, ProgramError> {
134        let mut data = Data::new(&DISC_METADATA_INITIALIZE);
135        data.string(name)?;
136        data.string(symbol)?;
137        data.string(uri)?;
138        Ok(data)
139    }
140
141    /// `[disc][field][value]`: the field is a variant byte (0 name, 1
142    /// symbol, 2 URI, 3 key followed by the key string).
143    #[inline(always)]
144    pub fn encode_update_field(
145        field: MetadataField<'_>,
146        value: &str,
147    ) -> Result<Data, ProgramError> {
148        let mut data = Data::new(&DISC_METADATA_UPDATE_FIELD);
149        match field {
150            MetadataField::Name => data.bytes(&[0])?,
151            MetadataField::Symbol => data.bytes(&[1])?,
152            MetadataField::Uri => data.bytes(&[2])?,
153            MetadataField::Key(key) => {
154                data.bytes(&[3])?;
155                data.string(key)?;
156            }
157        }
158        data.string(value)?;
159        Ok(data)
160    }
161
162    /// `[disc][idempotent][key]`.
163    #[inline(always)]
164    pub fn encode_remove_key(idempotent: bool, key: &str) -> Result<Data, ProgramError> {
165        let mut data = Data::new(&DISC_METADATA_REMOVE_KEY);
166        data.bytes(&[u8::from(idempotent)])?;
167        data.string(key)?;
168        Ok(data)
169    }
170
171    /// `[disc][new_authority: nullable 32]`, for the metadata
172    /// (`DISC_METADATA_UPDATE_AUTHORITY`) and the group
173    /// (`DISC_GROUP_UPDATE_AUTHORITY`) authority.
174    #[inline(always)]
175    pub fn encode_update_authority(
176        discriminator: &[u8; 8],
177        new_authority: Option<&Address>,
178    ) -> Result<Data, ProgramError> {
179        let mut data = Data::new(discriminator);
180        data.nullable(new_authority)?;
181        Ok(data)
182    }
183
184    /// `[disc][start: Option<u64>][end: Option<u64>]`.
185    #[inline(always)]
186    pub fn encode_emit(start: Option<u64>, end: Option<u64>) -> Result<Data, ProgramError> {
187        let mut data = Data::new(&DISC_METADATA_EMIT);
188        data.option_u64(start)?;
189        data.option_u64(end)?;
190        Ok(data)
191    }
192
193    /// `[disc][update_authority: nullable 32][max_size: u64 LE]`.
194    #[inline(always)]
195    pub fn encode_initialize_group(
196        update_authority: Option<&Address>,
197        max_size: u64,
198    ) -> Result<Data, ProgramError> {
199        let mut data = Data::new(&DISC_GROUP_INITIALIZE);
200        data.nullable(update_authority)?;
201        data.bytes(&max_size.to_le_bytes())?;
202        Ok(data)
203    }
204
205    /// `[disc][max_size: u64 LE]`.
206    #[inline(always)]
207    pub fn encode_update_group_max_size(max_size: u64) -> Result<Data, ProgramError> {
208        let mut data = Data::new(&DISC_GROUP_UPDATE_MAX_SIZE);
209        data.bytes(&max_size.to_le_bytes())?;
210        Ok(data)
211    }
212
213    /// `[disc]`.
214    #[inline(always)]
215    pub fn encode_initialize_member() -> Data {
216        Data::new(&DISC_GROUP_INITIALIZE_MEMBER)
217    }
218}
219
220/// The entry points of an interface builder: Token-2022 by default, any
221/// program that implements the interface by address.
222macro_rules! interface_methods {
223    ($name:ident $(, signer = $signer:ident)?) => {
224        impl $name<'_> {
225            /// Send to Token-2022.
226            #[inline]
227            pub fn invoke(&self) -> ProgramResult {
228                self.invoke_on_program(&TOKEN_2022_PROGRAM_ID, &[])
229            }
230
231            /// Send to Token-2022 with PDA signers.
232            #[inline]
233            pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
234                self.invoke_on_program(&TOKEN_2022_PROGRAM_ID, signers)
235            }
236
237            /// Send to any program that implements the interface. With no
238            /// PDA `signers`, the instruction's signer must have signed
239            /// the transaction directly.
240            #[inline]
241            pub fn invoke_on_program(
242                &self,
243                program: &Address,
244                signers: &[Signer<'_, '_>],
245            ) -> ProgramResult {
246                $(
247                    if signers.is_empty() && !self.$signer.is_signer() {
248                        return Err(ProgramError::MissingRequiredSignature);
249                    }
250                )?
251                self.emit(&[], &mut Invoke { program, signers })
252            }
253        }
254    };
255}
256
257// ---------------------------------------------------------------------
258// Token metadata
259
260/// `Initialize`: write the base metadata (name, symbol, URI) into
261/// `metadata`, which for Token-2022 is the mint itself. The mint authority
262/// signs. The account must already hold the rent for its new size.
263pub struct InitializeTokenMetadata<'a> {
264    pub metadata: &'a AccountView<'a>,
265    pub update_authority: &'a AccountView<'a>,
266    pub mint: &'a AccountView<'a>,
267    pub mint_authority: &'a AccountView<'a>,
268    pub name: &'a str,
269    pub symbol: &'a str,
270    pub uri: &'a str,
271}
272
273impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeTokenMetadata<'x> {
274    #[inline(always)]
275    fn emit(
276        &self,
277        _multisig_signers: &[&'a AccountView<'a>],
278        sink: &mut impl TokenSink<'a>,
279    ) -> ProgramResult {
280        let data = encoders::encode_initialize(self.name, self.symbol, self.uri)?;
281        let accounts = [
282            InstructionAccount::writable(self.metadata.address()),
283            InstructionAccount::readonly(self.update_authority.address()),
284            InstructionAccount::readonly(self.mint.address()),
285            InstructionAccount::readonly_signer(self.mint_authority.address()),
286        ];
287        let views = [
288            self.metadata,
289            self.update_authority,
290            self.mint,
291            self.mint_authority,
292        ];
293        sink.emit(data.as_slice(), accounts, views, &[])
294    }
295}
296
297interface_methods!(InitializeTokenMetadata, signer = mint_authority);
298
299/// A builder over a writable target and one signing authority.
300macro_rules! authority_instruction {
301    ($(#[$doc:meta])* $name:ident { target = $target:ident, authority = $auth:ident $(, $field:ident : $ty:ty)* $(,)? } data = |$s:ident| $data:expr;) => {
302        $(#[$doc])*
303        pub struct $name<'a> {
304            pub $target: &'a AccountView<'a>,
305            pub $auth: &'a AccountView<'a>,
306            $(pub $field: $ty,)*
307        }
308
309        impl<'a, 'x: 'a> TokenInstruction<'a> for $name<'x> {
310            #[inline(always)]
311            fn emit(
312                &self,
313                _multisig_signers: &[&'a AccountView<'a>],
314                sink: &mut impl TokenSink<'a>,
315            ) -> ProgramResult {
316                let $s = self;
317                let data: Data = $data;
318                let accounts = [
319                    InstructionAccount::writable(self.$target.address()),
320                    InstructionAccount::readonly_signer(self.$auth.address()),
321                ];
322                let views = [self.$target, self.$auth];
323                sink.emit(data.as_slice(), accounts, views, &[])
324            }
325        }
326
327        interface_methods!($name, signer = $auth);
328    };
329}
330
331authority_instruction! {
332    /// `UpdateField`: set the name, the symbol, the URI, or an additional
333    /// key to `value`. A new key or a longer value grows the account,
334    /// which must already hold the rent for it.
335    UpdateMetadataField { target = metadata, authority = update_authority, field: MetadataField<'a>, value: &'a str }
336    data = |s| encoders::encode_update_field(s.field, s.value)?;
337}
338
339authority_instruction! {
340    /// `RemoveKey`: remove an additional key. With `idempotent` a missing
341    /// key is not an error.
342    RemoveMetadataKey { target = metadata, authority = update_authority, idempotent: bool, key: &'a str }
343    data = |s| encoders::encode_remove_key(s.idempotent, s.key)?;
344}
345
346authority_instruction! {
347    /// `UpdateAuthority`: hand the metadata's update authority to
348    /// `new_authority`, or remove it with `None`.
349    UpdateMetadataAuthority { target = metadata, authority = current_authority, new_authority: Option<&'a Address> }
350    data = |s| encoders::encode_update_authority(&DISC_METADATA_UPDATE_AUTHORITY, s.new_authority)?;
351}
352
353/// `Emit`: return the serialized metadata (or the `start..end` slice of
354/// it) as the program's return data.
355pub struct EmitTokenMetadata<'a> {
356    pub metadata: &'a AccountView<'a>,
357    pub start: Option<u64>,
358    pub end: Option<u64>,
359}
360
361impl<'a, 'x: 'a> TokenInstruction<'a> for EmitTokenMetadata<'x> {
362    #[inline(always)]
363    fn emit(
364        &self,
365        _multisig_signers: &[&'a AccountView<'a>],
366        sink: &mut impl TokenSink<'a>,
367    ) -> ProgramResult {
368        let data = encoders::encode_emit(self.start, self.end)?;
369        let accounts = [InstructionAccount::readonly(self.metadata.address())];
370        let views = [self.metadata];
371        sink.emit(data.as_slice(), accounts, views, &[])
372    }
373}
374
375interface_methods!(EmitTokenMetadata);
376
377// ---------------------------------------------------------------------
378// Token group
379
380/// `InitializeGroup`: make `group` (for Token-2022, the mint itself) a
381/// group of at most `max_size` members. The mint authority signs.
382pub struct InitializeTokenGroup<'a> {
383    pub group: &'a AccountView<'a>,
384    pub mint: &'a AccountView<'a>,
385    pub mint_authority: &'a AccountView<'a>,
386    pub update_authority: Option<&'a Address>,
387    pub max_size: u64,
388}
389
390impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeTokenGroup<'x> {
391    #[inline(always)]
392    fn emit(
393        &self,
394        _multisig_signers: &[&'a AccountView<'a>],
395        sink: &mut impl TokenSink<'a>,
396    ) -> ProgramResult {
397        let data = encoders::encode_initialize_group(self.update_authority, self.max_size)?;
398        let accounts = [
399            InstructionAccount::writable(self.group.address()),
400            InstructionAccount::readonly(self.mint.address()),
401            InstructionAccount::readonly_signer(self.mint_authority.address()),
402        ];
403        let views = [self.group, self.mint, self.mint_authority];
404        sink.emit(data.as_slice(), accounts, views, &[])
405    }
406}
407
408interface_methods!(InitializeTokenGroup, signer = mint_authority);
409
410authority_instruction! {
411    /// `UpdateGroupMaxSize`: change the largest number of members.
412    UpdateTokenGroupMaxSize { target = group, authority = update_authority, max_size: u64 }
413    data = |s| encoders::encode_update_group_max_size(s.max_size)?;
414}
415
416authority_instruction! {
417    /// `UpdateGroupAuthority`: hand the group's update authority to
418    /// `new_authority`, or remove it with `None`.
419    UpdateTokenGroupAuthority { target = group, authority = current_authority, new_authority: Option<&'a Address> }
420    data = |s| encoders::encode_update_authority(&DISC_GROUP_UPDATE_AUTHORITY, s.new_authority)?;
421}
422
423/// `InitializeMember`: make `member` (for Token-2022, the member's mint)
424/// a member of `group`. The member's mint authority and the group's
425/// update authority both sign.
426pub struct InitializeTokenGroupMember<'a> {
427    pub member: &'a AccountView<'a>,
428    pub member_mint: &'a AccountView<'a>,
429    pub member_mint_authority: &'a AccountView<'a>,
430    pub group: &'a AccountView<'a>,
431    pub group_update_authority: &'a AccountView<'a>,
432}
433
434impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeTokenGroupMember<'x> {
435    #[inline(always)]
436    fn emit(
437        &self,
438        _multisig_signers: &[&'a AccountView<'a>],
439        sink: &mut impl TokenSink<'a>,
440    ) -> ProgramResult {
441        let data = encoders::encode_initialize_member();
442        let accounts = [
443            InstructionAccount::writable(self.member.address()),
444            InstructionAccount::readonly(self.member_mint.address()),
445            InstructionAccount::readonly_signer(self.member_mint_authority.address()),
446            InstructionAccount::writable(self.group.address()),
447            InstructionAccount::readonly_signer(self.group_update_authority.address()),
448        ];
449        let views = [
450            self.member,
451            self.member_mint,
452            self.member_mint_authority,
453            self.group,
454            self.group_update_authority,
455        ];
456        sink.emit(data.as_slice(), accounts, views, &[])
457    }
458}
459
460impl InitializeTokenGroupMember<'_> {
461    /// Send to Token-2022; both authorities signed directly.
462    #[inline]
463    pub fn invoke(&self) -> ProgramResult {
464        self.invoke_on_program(&TOKEN_2022_PROGRAM_ID, &[])
465    }
466
467    /// Send to Token-2022 with PDA signers.
468    #[inline]
469    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
470        self.invoke_on_program(&TOKEN_2022_PROGRAM_ID, signers)
471    }
472
473    /// Send to any program that implements the group interface. With no
474    /// PDA `signers`, both authorities must have signed directly.
475    #[inline]
476    pub fn invoke_on_program(
477        &self,
478        program: &Address,
479        signers: &[Signer<'_, '_>],
480    ) -> ProgramResult {
481        if signers.is_empty()
482            && !(self.member_mint_authority.is_signer() && self.group_update_authority.is_signer())
483        {
484            return Err(ProgramError::MissingRequiredSignature);
485        }
486        self.emit(&[], &mut Invoke { program, signers })
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::encoders::*;
493    use super::*;
494
495    #[test]
496    fn discriminators_are_the_hash_prefixes_of_the_interface_inputs() {
497        for (input, expected) in [
498            (
499                "spl_token_metadata_interface:initialize_account",
500                DISC_METADATA_INITIALIZE,
501            ),
502            (
503                "spl_token_metadata_interface:updating_field",
504                DISC_METADATA_UPDATE_FIELD,
505            ),
506            (
507                "spl_token_metadata_interface:remove_key_ix",
508                DISC_METADATA_REMOVE_KEY,
509            ),
510            (
511                "spl_token_metadata_interface:update_the_authority",
512                DISC_METADATA_UPDATE_AUTHORITY,
513            ),
514            ("spl_token_metadata_interface:emitter", DISC_METADATA_EMIT),
515            (
516                "spl_token_group_interface:initialize_token_group",
517                DISC_GROUP_INITIALIZE,
518            ),
519            (
520                "spl_token_group_interface:update_group_max_size",
521                DISC_GROUP_UPDATE_MAX_SIZE,
522            ),
523            (
524                "spl_token_group_interface:update_authority",
525                DISC_GROUP_UPDATE_AUTHORITY,
526            ),
527            (
528                "spl_token_group_interface:initialize_member",
529                DISC_GROUP_INITIALIZE_MEMBER,
530            ),
531        ] {
532            let digest = hopper_native::hash::sha256(&[input.as_bytes()]).unwrap();
533            assert_eq!(&digest[..8], &expected, "{input}");
534        }
535    }
536
537    #[test]
538    fn strings_are_length_prefixed_and_bounded() {
539        let data = encode_initialize("Hopper", "HOP", "https://hopperzero.dev/t.json").unwrap();
540        let bytes = data.as_slice();
541        assert_eq!(&bytes[..8], &DISC_METADATA_INITIALIZE);
542        assert_eq!(&bytes[8..12], &6u32.to_le_bytes());
543        assert_eq!(&bytes[12..18], b"Hopper");
544        assert_eq!(&bytes[18..22], &3u32.to_le_bytes());
545        assert_eq!(&bytes[22..25], b"HOP");
546        assert_eq!(bytes.len(), 8 + 4 + 6 + 4 + 3 + 4 + 29);
547
548        let long = "x".repeat(MAX_METADATA_INSTRUCTION_DATA);
549        assert_eq!(
550            encode_initialize(&long, "", "").err(),
551            Some(ProgramError::InvalidArgument)
552        );
553        // Exactly at the limit is accepted.
554        let fits = "x".repeat(MAX_METADATA_INSTRUCTION_DATA - 8 - 12);
555        assert_eq!(
556            encode_initialize(&fits, "", "").unwrap().as_slice().len(),
557            MAX_METADATA_INSTRUCTION_DATA
558        );
559    }
560
561    #[test]
562    fn fields_options_and_authorities_encode_as_borsh() {
563        assert_eq!(
564            &encode_update_field(MetadataField::Uri, "u")
565                .unwrap()
566                .as_slice()[8..],
567            &[2, 1, 0, 0, 0, b'u']
568        );
569        assert_eq!(
570            &encode_update_field(MetadataField::Key("k"), "v")
571                .unwrap()
572                .as_slice()[8..],
573            &[3, 1, 0, 0, 0, b'k', 1, 0, 0, 0, b'v']
574        );
575        assert_eq!(
576            &encode_remove_key(true, "k").unwrap().as_slice()[8..],
577            &[1, 1, 0, 0, 0, b'k']
578        );
579        assert_eq!(&encode_emit(None, None).unwrap().as_slice()[8..], &[0, 0]);
580        assert_eq!(
581            &encode_emit(Some(2), None).unwrap().as_slice()[8..],
582            &[1, 2, 0, 0, 0, 0, 0, 0, 0, 0]
583        );
584        let authority = Address::new_from_array([7; 32]);
585        assert_eq!(
586            &encode_update_authority(&DISC_GROUP_UPDATE_AUTHORITY, Some(&authority))
587                .unwrap()
588                .as_slice()[8..],
589            &[7u8; 32]
590        );
591        assert_eq!(
592            &encode_update_authority(&DISC_METADATA_UPDATE_AUTHORITY, None)
593                .unwrap()
594                .as_slice()[8..],
595            &[0u8; 32]
596        );
597        assert!(encode_update_authority(
598            &DISC_METADATA_UPDATE_AUTHORITY,
599            Some(&Address::new_from_array([0; 32]))
600        )
601        .is_err());
602        assert_eq!(
603            &encode_initialize_group(None, 9).unwrap().as_slice()[40..],
604            &9u64.to_le_bytes()
605        );
606        assert_eq!(encode_initialize_member().as_slice().len(), 8);
607    }
608}