Skip to main content

miden_client/transaction/request/
mod.rs

1//! Contains structures and functions related to transaction creation.
2
3use alloc::boxed::Box;
4use alloc::collections::{BTreeMap, BTreeSet};
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7
8use miden_protocol::Word;
9use miden_protocol::account::AccountId;
10use miden_protocol::asset::{Asset, NonFungibleAsset};
11use miden_protocol::crypto::merkle::MerkleError;
12use miden_protocol::crypto::merkle::store::MerkleStore;
13use miden_protocol::errors::{
14    AccountError,
15    AssetVaultError,
16    NoteError,
17    StorageMapError,
18    TransactionInputError,
19    TransactionScriptError,
20};
21use miden_protocol::note::{
22    Note,
23    NoteDetails,
24    NoteId,
25    NoteRecipient,
26    NoteScript,
27    NoteTag,
28    PartialNote,
29};
30use miden_protocol::transaction::{InputNote, InputNotes, TransactionArgs, TransactionScript};
31use miden_protocol::vm::AdviceMap;
32use miden_standards::account::interface::{AccountInterface, AccountInterfaceError};
33use miden_standards::code_builder::CodeBuilder;
34use miden_standards::errors::CodeBuilderError;
35use miden_tx::utils::serde::{
36    ByteReader,
37    ByteWriter,
38    Deserializable,
39    DeserializationError,
40    Serializable,
41};
42use thiserror::Error;
43
44mod builder;
45pub use builder::{PaymentNoteDescription, SwapTransactionData, TransactionRequestBuilder};
46
47mod foreign;
48pub use foreign::ForeignAccount;
49pub(crate) use foreign::account_proof_into_inputs;
50
51use crate::store::InputNoteRecord;
52
53// TRANSACTION REQUEST
54// ================================================================================================
55
56pub type NoteArgs = Word;
57
58/// Specifies a transaction script to be executed in a transaction.
59///
60/// A transaction script is a program which is executed after scripts of all input notes have been
61/// executed.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub enum TransactionScriptTemplate {
64    /// Specifies the exact transaction script to be executed in a transaction.
65    CustomScript(TransactionScript),
66    /// Specifies that the transaction script must create the specified output notes.
67    ///
68    /// It is up to the client to determine how the output notes will be created and this will
69    /// depend on the capabilities of the account the transaction request will be applied to.
70    /// For example, for Basic Wallets, this may involve invoking `create_note` procedure.
71    SendNotes(Vec<PartialNote>),
72}
73
74/// Specifies a transaction request that can be executed by an account.
75///
76/// A request contains information about input notes to be consumed by the transaction (if any),
77/// description of the transaction script to be executed (if any), and a set of notes expected
78/// to be generated by the transaction or by consuming notes generated by the transaction.
79#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct TransactionRequest {
81    /// Notes to be consumed by the transaction.
82    /// includes both authenticated and unauthenticated notes.
83    /// Notes which ID is present in the store are considered authenticated,
84    /// the ones which ID is does not exist are considered unauthenticated.
85    input_notes: Vec<Note>,
86    /// Optional arguments of the input notes to be consumed by the transaction. This
87    /// includes both authenticated and unauthenticated notes.
88    input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
89    /// Template for the creation of the transaction script.
90    script_template: Option<TransactionScriptTemplate>,
91    /// A map of recipients of the output notes expected to be generated by the transaction.
92    expected_output_recipients: BTreeMap<Word, NoteRecipient>,
93    /// A map of details and tags of notes we expect to be created as part of future transactions
94    /// with their respective tags.
95    ///
96    /// For example, after a swap note is consumed, a payback note is expected to be created.
97    expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
98    /// Initial state of the `AdviceMap` that provides data during runtime.
99    advice_map: AdviceMap,
100    /// Initial state of the `MerkleStore` that provides data during runtime.
101    merkle_store: MerkleStore,
102    /// Foreign account data requirements keyed by account ID. At execution time, account data
103    /// will be retrieved from the network, and injected as advice inputs. Additionally, the
104    /// account's code will be added to the executor and prover.
105    foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
106    /// The number of blocks in relation to the transaction's reference block after which the
107    /// transaction will expire. If `None`, the transaction will not expire.
108    expiration_delta: Option<u16>,
109    /// Indicates whether to **silently** ignore invalid input notes when executing the
110    /// transaction. This will allow the transaction to be executed even if some input notes
111    /// are invalid.
112    ignore_invalid_input_notes: bool,
113    /// Optional [`Word`] that will be pushed to the operand stack before the transaction script
114    /// execution.
115    script_arg: Option<Word>,
116    /// Optional [`Word`] that will be pushed to the stack for the authentication procedure
117    /// during transaction execution.
118    auth_arg: Option<Word>,
119    /// Note scripts that the node's NTX builder will need in its script registry.
120    ///
121    /// See [`TransactionRequestBuilder::expected_ntx_scripts`] for details.
122    expected_ntx_scripts: Vec<NoteScript>,
123}
124
125impl TransactionRequest {
126    // PUBLIC ACCESSORS
127    // --------------------------------------------------------------------------------------------
128
129    /// Returns a reference to the transaction request's input note list.
130    pub fn input_notes(&self) -> &[Note] {
131        &self.input_notes
132    }
133
134    /// Returns a list of all input note IDs.
135    pub fn input_note_ids(&self) -> impl Iterator<Item = NoteId> {
136        self.input_notes.iter().map(Note::id)
137    }
138
139    /// Returns the assets held by the transaction's input notes.
140    pub fn incoming_assets(&self) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
141        collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter()))
142    }
143
144    /// Returns a map of note IDs to their respective [`NoteArgs`]. The result will include
145    /// exclusively note IDs for notes for which [`NoteArgs`] have been defined.
146    pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
147        self.input_notes_args
148            .iter()
149            .filter_map(|(note, args)| args.map(|a| (*note, a)))
150            .collect()
151    }
152
153    /// Returns the expected output own notes of the transaction.
154    ///
155    /// In this context "own notes" refers to notes that are expected to be created directly by the
156    /// transaction script, rather than notes that are created as a result of consuming other
157    /// notes.
158    pub fn expected_output_own_notes(&self) -> Vec<Note> {
159        match &self.script_template {
160            Some(TransactionScriptTemplate::SendNotes(notes)) => notes
161                .iter()
162                .map(|partial| {
163                    Note::new(
164                        partial.assets().clone(),
165                        partial.metadata().clone(),
166                        self.expected_output_recipients
167                            .get(&partial.recipient_digest())
168                            .expect("Recipient should be included if it's an own note")
169                            .clone(),
170                    )
171                })
172                .collect(),
173            _ => vec![],
174        }
175    }
176
177    /// Returns an iterator over the expected output notes.
178    pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
179        self.expected_output_recipients.values()
180    }
181
182    /// Returns an iterator over expected future notes.
183    pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
184        self.expected_future_notes.values()
185    }
186
187    /// Returns the [`TransactionScriptTemplate`].
188    pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
189        &self.script_template
190    }
191
192    /// Returns the [`AdviceMap`] for the transaction request.
193    pub fn advice_map(&self) -> &AdviceMap {
194        &self.advice_map
195    }
196
197    /// Returns a mutable reference to the [`AdviceMap`] for the transaction request.
198    pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
199        &mut self.advice_map
200    }
201
202    /// Returns the [`MerkleStore`] for the transaction request.
203    pub fn merkle_store(&self) -> &MerkleStore {
204        &self.merkle_store
205    }
206
207    /// Returns the required foreign accounts keyed by account ID.
208    pub fn foreign_accounts(&self) -> &BTreeMap<AccountId, ForeignAccount> {
209        &self.foreign_accounts
210    }
211
212    /// Returns whether to ignore invalid input notes or not.
213    pub fn ignore_invalid_input_notes(&self) -> bool {
214        self.ignore_invalid_input_notes
215    }
216
217    /// Returns the script argument for the transaction request.
218    pub fn script_arg(&self) -> &Option<Word> {
219        &self.script_arg
220    }
221
222    /// Returns the auth argument for the transaction request.
223    pub fn auth_arg(&self) -> &Option<Word> {
224        &self.auth_arg
225    }
226
227    /// Returns the expected NTX scripts that the node's NTX builder will need in its registry.
228    pub fn expected_ntx_scripts(&self) -> &[NoteScript] {
229        &self.expected_ntx_scripts
230    }
231
232    /// Builds the [`InputNotes`] needed for the transaction execution.
233    ///
234    /// Authenticated input notes are provided by the caller (typically fetched from the store).
235    /// Any requested notes not present in that authenticated set are treated as unauthenticated.
236    /// The transaction input notes will include both authenticated and unauthenticated notes in
237    /// the order they were provided in the transaction request.
238    pub(crate) fn build_input_notes(
239        &self,
240        authenticated_note_records: Vec<InputNoteRecord>,
241    ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
242        let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
243
244        // Add provided authenticated input notes to the input notes map.
245        for authenticated_note_record in authenticated_note_records {
246            if !authenticated_note_record.is_authenticated() {
247                return Err(TransactionRequestError::InputNoteNotAuthenticated(
248                    authenticated_note_record.id(),
249                ));
250            }
251
252            if authenticated_note_record.is_consumed() {
253                return Err(TransactionRequestError::InputNoteAlreadyConsumed(
254                    authenticated_note_record.id(),
255                ));
256            }
257
258            let authenticated_note_id = authenticated_note_record.id();
259            input_notes.insert(
260                authenticated_note_id,
261                authenticated_note_record
262                    .try_into()
263                    .expect("Authenticated note record should be convertible to InputNote"),
264            );
265        }
266
267        // Add unauthenticated input notes to the input notes map.
268        let authenticated_note_ids: BTreeSet<NoteId> = input_notes.keys().copied().collect();
269        for note in self.input_notes().iter().filter(|n| !authenticated_note_ids.contains(&n.id()))
270        {
271            input_notes.insert(note.id(), InputNote::Unauthenticated { note: note.clone() });
272        }
273
274        Ok(InputNotes::new(
275            self.input_note_ids()
276                .map(|note_id| {
277                    input_notes
278                        .remove(&note_id)
279                        .expect("The input note map was checked to contain all input notes")
280                })
281                .collect(),
282        )?)
283    }
284
285    /// Converts the [`TransactionRequest`] into [`TransactionArgs`] in order to be executed by a
286    /// Miden host.
287    pub(crate) fn into_transaction_args(self, tx_script: TransactionScript) -> TransactionArgs {
288        let note_args = self.get_note_args();
289        let TransactionRequest {
290            expected_output_recipients,
291            advice_map,
292            merkle_store,
293            ..
294        } = self;
295
296        let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
297
298        tx_args = if let Some(argument) = self.script_arg {
299            tx_args.with_tx_script_and_args(tx_script, argument)
300        } else {
301            tx_args.with_tx_script(tx_script)
302        };
303
304        if let Some(auth_argument) = self.auth_arg {
305            tx_args = tx_args.with_auth_args(auth_argument);
306        }
307
308        tx_args
309            .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
310        tx_args.extend_merkle_store(merkle_store.inner_nodes());
311
312        tx_args
313    }
314
315    /// Builds the transaction script based on the account capabilities and the transaction request.
316    /// The debug mode enables the script debug logs.
317    pub(crate) fn build_transaction_script(
318        &self,
319        account_interface: &AccountInterface,
320    ) -> Result<TransactionScript, TransactionRequestError> {
321        match &self.script_template {
322            Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
323            Some(TransactionScriptTemplate::SendNotes(notes)) => {
324                Ok(account_interface.build_send_notes_script(notes, self.expiration_delta)?)
325            },
326            None => {
327                let empty_script = CodeBuilder::new().compile_tx_script("begin nop end")?;
328
329                Ok(empty_script)
330            },
331        }
332    }
333}
334
335// SERIALIZATION
336// ================================================================================================
337
338impl Serializable for TransactionRequest {
339    fn write_into<W: ByteWriter>(&self, target: &mut W) {
340        self.input_notes.write_into(target);
341        self.input_notes_args.write_into(target);
342        match &self.script_template {
343            None => target.write_u8(0),
344            Some(TransactionScriptTemplate::CustomScript(script)) => {
345                target.write_u8(1);
346                script.write_into(target);
347            },
348            Some(TransactionScriptTemplate::SendNotes(notes)) => {
349                target.write_u8(2);
350                notes.write_into(target);
351            },
352        }
353        self.expected_output_recipients.write_into(target);
354        self.expected_future_notes.write_into(target);
355        self.advice_map.write_into(target);
356        self.merkle_store.write_into(target);
357        let foreign_accounts: Vec<_> = self.foreign_accounts.values().cloned().collect();
358        foreign_accounts.write_into(target);
359        self.expiration_delta.write_into(target);
360        target.write_u8(u8::from(self.ignore_invalid_input_notes));
361        self.script_arg.write_into(target);
362        self.auth_arg.write_into(target);
363        self.expected_ntx_scripts.write_into(target);
364    }
365}
366
367impl Deserializable for TransactionRequest {
368    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
369        let input_notes = Vec::<Note>::read_from(source)?;
370        let input_notes_args = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
371
372        let script_template = match source.read_u8()? {
373            0 => None,
374            1 => {
375                let transaction_script = TransactionScript::read_from(source)?;
376                Some(TransactionScriptTemplate::CustomScript(transaction_script))
377            },
378            2 => {
379                let notes = Vec::<PartialNote>::read_from(source)?;
380                Some(TransactionScriptTemplate::SendNotes(notes))
381            },
382            _ => {
383                return Err(DeserializationError::InvalidValue(
384                    "Invalid script template type".to_string(),
385                ));
386            },
387        };
388
389        let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
390        let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
391
392        let advice_map = AdviceMap::read_from(source)?;
393        let merkle_store = MerkleStore::read_from(source)?;
394        let mut foreign_accounts = BTreeMap::new();
395        for foreign_account in Vec::<ForeignAccount>::read_from(source)? {
396            foreign_accounts.entry(foreign_account.account_id()).or_insert(foreign_account);
397        }
398        let expiration_delta = Option::<u16>::read_from(source)?;
399        let ignore_invalid_input_notes = source.read_u8()? == 1;
400        let script_arg = Option::<Word>::read_from(source)?;
401        let auth_arg = Option::<Word>::read_from(source)?;
402        let expected_ntx_scripts = Vec::<NoteScript>::read_from(source)?;
403
404        Ok(TransactionRequest {
405            input_notes,
406            input_notes_args,
407            script_template,
408            expected_output_recipients,
409            expected_future_notes,
410            advice_map,
411            merkle_store,
412            foreign_accounts,
413            expiration_delta,
414            ignore_invalid_input_notes,
415            script_arg,
416            auth_arg,
417            expected_ntx_scripts,
418        })
419    }
420}
421
422// HELPERS
423// ================================================================================================
424
425/// Accumulates fungible totals and collectable non-fungible assets from an iterator of assets.
426pub(crate) fn collect_assets<'a>(
427    assets: impl Iterator<Item = &'a Asset>,
428) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
429    let mut fungible_balance_map = BTreeMap::new();
430    let mut non_fungible_set = Vec::new();
431
432    assets.for_each(|asset| match asset {
433        Asset::Fungible(fungible) => {
434            fungible_balance_map
435                .entry(fungible.faucet_id())
436                .and_modify(|balance| *balance += fungible.amount())
437                .or_insert(fungible.amount());
438        },
439        Asset::NonFungible(non_fungible) => {
440            if !non_fungible_set.contains(non_fungible) {
441                non_fungible_set.push(*non_fungible);
442            }
443        },
444    });
445
446    (fungible_balance_map, non_fungible_set)
447}
448
449impl Default for TransactionRequestBuilder {
450    fn default() -> Self {
451        Self::new()
452    }
453}
454
455// TRANSACTION REQUEST ERROR
456// ================================================================================================
457
458// Errors related to a [TransactionRequest]
459#[derive(Debug, Error)]
460pub enum TransactionRequestError {
461    #[error("account interface error")]
462    AccountInterfaceError(#[from] AccountInterfaceError),
463    #[error("account error")]
464    AccountError(#[from] AccountError),
465    #[error("duplicate input note: note {0} was added more than once to the transaction")]
466    DuplicateInputNote(NoteId),
467    #[error(
468        "the account proof does not contain the required foreign account data; re-fetch the proof and retry"
469    )]
470    ForeignAccountDataMissing,
471    #[error(
472        "foreign account {0} has an incompatible storage mode; use `ForeignAccount::public()` for public accounts and `ForeignAccount::private()` for private accounts"
473    )]
474    InvalidForeignAccountId(AccountId),
475    #[error(
476        "note {0} cannot be used as an authenticated input: it does not have a valid inclusion proof"
477    )]
478    InputNoteNotAuthenticated(NoteId),
479    #[error("note {0} has already been consumed")]
480    InputNoteAlreadyConsumed(NoteId),
481    #[error("sender account {0} is not tracked by this client or does not exist")]
482    InvalidSenderAccount(AccountId),
483    #[error("invalid transaction script")]
484    InvalidTransactionScript(#[from] TransactionScriptError),
485    #[error("merkle proof error")]
486    MerkleError(#[from] MerkleError),
487    #[error("empty transaction: the request has no input notes and no account state changes")]
488    NoInputNotesNorAccountChange,
489    #[error("note not found: {0}")]
490    NoteNotFound(String),
491    #[error("failed to create note")]
492    NoteCreationError(#[from] NoteError),
493    #[error("pay-to-ID note must contain at least one asset to transfer")]
494    P2IDNoteWithoutAsset,
495    #[error("error building script")]
496    CodeBuilderError(#[from] CodeBuilderError),
497    #[error("transaction script template error: {0}")]
498    ScriptTemplateError(String),
499    #[error("storage slot {0} not found in account ID {1}")]
500    StorageSlotNotFound(u8, AccountId),
501    #[error("error while building the input notes")]
502    TransactionInputError(#[from] TransactionInputError),
503    #[error("account storage map error")]
504    StorageMapError(#[from] StorageMapError),
505    #[error("asset vault error")]
506    AssetVaultError(#[from] AssetVaultError),
507    #[error(
508        "unsupported authentication scheme ID {0}; supported schemes are: RpoFalcon512 (0) and EcdsaK256Keccak (1)"
509    )]
510    UnsupportedAuthSchemeId(u8),
511}
512
513// TESTS
514// ================================================================================================
515
516#[cfg(test)]
517mod tests {
518    use std::vec::Vec;
519
520    use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
521    use miden_protocol::account::{
522        AccountBuilder,
523        AccountComponent,
524        AccountId,
525        AccountType,
526        StorageMapKey,
527        StorageSlotName,
528    };
529    use miden_protocol::asset::FungibleAsset;
530    use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
531    use miden_protocol::note::{NoteAttachment, NoteTag, NoteType};
532    use miden_protocol::testing::account_id::{
533        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
534        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
535        ACCOUNT_ID_SENDER,
536    };
537    use miden_protocol::{EMPTY_WORD, Felt, Word};
538    use miden_standards::account::auth::AuthSingleSig;
539    use miden_standards::note::P2idNote;
540    use miden_standards::testing::account_component::MockAccountComponent;
541    use miden_tx::utils::serde::{Deserializable, Serializable};
542
543    use super::{TransactionRequest, TransactionRequestBuilder};
544    use crate::rpc::domain::account::AccountStorageRequirements;
545    use crate::transaction::ForeignAccount;
546
547    #[test]
548    fn transaction_request_serialization() {
549        assert_transaction_request_serialization_with(|| {
550            AuthSingleSig::new(
551                PublicKeyCommitment::from(EMPTY_WORD),
552                AuthScheme::Falcon512Poseidon2,
553            )
554            .into()
555        });
556    }
557
558    #[test]
559    fn transaction_request_serialization_ecdsa() {
560        assert_transaction_request_serialization_with(|| {
561            AuthSingleSig::new(PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::EcdsaK256Keccak)
562                .into()
563        });
564    }
565
566    fn assert_transaction_request_serialization_with<F>(auth_component: F)
567    where
568        F: FnOnce() -> AccountComponent,
569    {
570        let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
571        let target_id =
572            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
573        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
574        let mut rng = RandomCoin::new(Word::default());
575
576        let mut notes = vec![];
577        for i in 0..6 {
578            let note = P2idNote::create(
579                sender_id,
580                target_id,
581                vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
582                NoteType::Private,
583                NoteAttachment::default(),
584                &mut rng,
585            )
586            .unwrap();
587            notes.push(note);
588        }
589
590        let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
591        for i in 0..10 {
592            advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
593        }
594
595        let account = AccountBuilder::new(Default::default())
596            .with_component(MockAccountComponent::with_empty_slots())
597            .with_auth_component(auth_component())
598            .account_type(AccountType::RegularAccountImmutableCode)
599            .storage_mode(miden_protocol::account::AccountStorageMode::Private)
600            .build_existing()
601            .unwrap();
602
603        // This transaction request wouldn't be valid in a real scenario, it's intended for testing
604        let tx_request = TransactionRequestBuilder::new()
605            .input_notes(vec![(notes.pop().unwrap(), None)])
606            .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
607            .expected_future_notes(vec![(
608                notes.pop().unwrap().into(),
609                NoteTag::with_account_target(sender_id),
610            )])
611            .extend_advice_map(advice_vec)
612            .foreign_accounts([
613                ForeignAccount::public(
614                    target_id,
615                    AccountStorageRequirements::new([(
616                        StorageSlotName::new("demo::storage_slot").unwrap(),
617                        &[StorageMapKey::new(Word::default())],
618                    )]),
619                )
620                .unwrap(),
621                ForeignAccount::private(&account).unwrap(),
622            ])
623            .own_output_notes(vec![notes.pop().unwrap(), notes.pop().unwrap()])
624            .script_arg(rng.draw_word())
625            .auth_arg(rng.draw_word())
626            .expected_ntx_scripts(vec![notes.first().unwrap().recipient().script().clone()])
627            .build()
628            .unwrap();
629
630        let mut buffer = Vec::new();
631        tx_request.write_into(&mut buffer);
632
633        let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
634        assert_eq!(tx_request, deserialized_tx_request);
635    }
636}