hiero_sdk/file/
file_update_transaction.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use hiero_sdk_proto::services;
4use hiero_sdk_proto::services::file_service_client::FileServiceClient;
5use time::{
6    Duration,
7    OffsetDateTime,
8};
9use tonic::transport::Channel;
10
11use crate::ledger_id::RefLedgerId;
12use crate::protobuf::{
13    FromProtobuf,
14    ToProtobuf,
15};
16use crate::transaction::{
17    AnyTransactionData,
18    ChunkInfo,
19    ToSchedulableTransactionDataProtobuf,
20    ToTransactionDataProtobuf,
21    TransactionData,
22    TransactionExecute,
23};
24use crate::{
25    AccountId,
26    BoxGrpcFuture,
27    Error,
28    FileId,
29    Key,
30    KeyList,
31    Transaction,
32    ValidateChecksums,
33};
34
35/// Modify the metadata and/or the contents of a file.
36///
37/// If a field is not set in the transaction body, the
38/// corresponding file attribute will be unchanged.
39///
40pub type FileUpdateTransaction = Transaction<FileUpdateTransactionData>;
41
42#[derive(Debug, Clone, Default)]
43pub struct FileUpdateTransactionData {
44    /// The file ID which is being updated in this transaction.
45    file_id: Option<FileId>,
46
47    /// The memo associated with the file.
48    file_memo: Option<String>,
49
50    /// All keys at the top level of a key list must sign to create or
51    /// modify the file. Any one of the keys at the top level key list
52    /// can sign to delete the file.
53    keys: Option<KeyList>,
54
55    /// The bytes that are to be the contents of the file.
56    contents: Option<Vec<u8>>,
57
58    /// The time at which this file should expire.
59    expiration_time: Option<OffsetDateTime>,
60
61    auto_renew_account_id: Option<AccountId>,
62
63    auto_renew_period: Option<Duration>,
64}
65
66impl FileUpdateTransaction {
67    /// Returns the ID of the file which is being updated.
68    #[must_use]
69    pub fn get_file_id(&self) -> Option<FileId> {
70        self.data().file_id
71    }
72
73    /// Sets the ID of the file which is being updated.
74    pub fn file_id(&mut self, id: impl Into<FileId>) -> &mut Self {
75        self.data_mut().file_id = Some(id.into());
76        self
77    }
78
79    /// Returns the new memo for the file.
80    #[must_use]
81    pub fn get_file_memo(&self) -> Option<&str> {
82        self.data().file_memo.as_deref()
83    }
84
85    /// Sets the new memo to be associated with the file.
86    pub fn file_memo(&mut self, memo: impl Into<String>) -> &mut Self {
87        self.data_mut().file_memo = Some(memo.into());
88        self
89    }
90
91    /// Returns the bytes that are to be the contents of the file.
92    #[must_use]
93    pub fn get_contents(&self) -> Option<&[u8]> {
94        self.data().contents.as_deref()
95    }
96
97    /// Sets the bytes that are to be the contents of the file.
98    pub fn contents(&mut self, contents: Vec<u8>) -> &mut Self {
99        self.data_mut().contents = Some(contents);
100        self
101    }
102
103    /// Returns the keys for this file.
104    #[must_use]
105    pub fn get_keys(&self) -> Option<&KeyList> {
106        self.data().keys.as_ref()
107    }
108
109    /// Sets the keys for this file.
110    ///
111    /// All keys at the top level of a key list must sign to create or
112    /// modify the file. Any one of the keys at the top level key list
113    /// can sign to delete the file.
114    pub fn keys<K: Into<Key>>(&mut self, keys: impl IntoIterator<Item = K>) -> &mut Self {
115        self.data_mut().keys = Some(keys.into_iter().map(Into::into).collect());
116        self
117    }
118
119    /// Returns the time at which this file should expire.
120    #[must_use]
121    pub fn get_expiration_time(&self) -> Option<OffsetDateTime> {
122        self.data().expiration_time
123    }
124
125    /// Sets the time at which this file should expire.
126    pub fn expiration_time(&mut self, at: OffsetDateTime) -> &mut Self {
127        self.data_mut().expiration_time = Some(at);
128        self
129    }
130
131    /// Returns the account to be used at the file's expiration time to extend the
132    /// life of the file.
133    ///
134    /// # Network Support
135    /// Please note that this not supported on any hedera network at this time.
136    #[must_use]
137    pub fn get_auto_renew_account_id(&self) -> Option<AccountId> {
138        self.data().auto_renew_account_id
139    }
140
141    /// Sets the account to be used at the files's expiration time to extend the
142    /// life of the file.
143    ///
144    /// # Network Support
145    /// Please note that this not supported on any hedera network at this time.
146    pub fn auto_renew_account_id(&mut self, id: AccountId) -> &mut Self {
147        self.data_mut().auto_renew_account_id = Some(id);
148        self
149    }
150
151    /// Returns the auto renew period for this file.
152    ///
153    /// # Network Support
154    /// Please note that this not supported on any hedera network at this time.
155    #[must_use]
156    pub fn get_auto_renew_period(&self) -> Option<Duration> {
157        self.data().auto_renew_period
158    }
159
160    /// Sets the auto renew period for this file.
161    ///
162    /// # Network Support
163    /// Please note that this not supported on any hedera network at this time.
164    pub fn auto_renew_period(&mut self, duration: Duration) -> &mut Self {
165        self.data_mut().auto_renew_period = Some(duration);
166        self
167    }
168}
169
170impl TransactionData for FileUpdateTransactionData {}
171
172impl TransactionExecute for FileUpdateTransactionData {
173    fn execute(
174        &self,
175        channel: Channel,
176        request: services::Transaction,
177    ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
178        Box::pin(async { FileServiceClient::new(channel).update_file(request).await })
179    }
180}
181
182impl ValidateChecksums for FileUpdateTransactionData {
183    fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
184        self.file_id.validate_checksums(ledger_id)
185    }
186}
187
188impl ToTransactionDataProtobuf for FileUpdateTransactionData {
189    fn to_transaction_data_protobuf(
190        &self,
191        chunk_info: &ChunkInfo,
192    ) -> services::transaction_body::Data {
193        let _ = chunk_info.assert_single_transaction();
194
195        services::transaction_body::Data::FileUpdate(self.to_protobuf())
196    }
197}
198
199impl ToSchedulableTransactionDataProtobuf for FileUpdateTransactionData {
200    fn to_schedulable_transaction_data_protobuf(
201        &self,
202    ) -> services::schedulable_transaction_body::Data {
203        services::schedulable_transaction_body::Data::FileUpdate(self.to_protobuf())
204    }
205}
206
207impl From<FileUpdateTransactionData> for AnyTransactionData {
208    fn from(transaction: FileUpdateTransactionData) -> Self {
209        Self::FileUpdate(transaction)
210    }
211}
212
213impl FromProtobuf<services::FileUpdateTransactionBody> for FileUpdateTransactionData {
214    fn from_protobuf(pb: services::FileUpdateTransactionBody) -> crate::Result<Self> {
215        Ok(Self {
216            file_id: Option::from_protobuf(pb.file_id)?,
217            file_memo: pb.memo,
218            keys: Option::from_protobuf(pb.keys)?,
219            contents: Some(pb.contents),
220            expiration_time: pb.expiration_time.map(Into::into),
221            auto_renew_account_id: None,
222            auto_renew_period: None,
223        })
224    }
225}
226
227impl ToProtobuf for FileUpdateTransactionData {
228    type Protobuf = services::FileUpdateTransactionBody;
229
230    fn to_protobuf(&self) -> Self::Protobuf {
231        services::FileUpdateTransactionBody {
232            file_id: self.file_id.to_protobuf(),
233            expiration_time: self.expiration_time.to_protobuf(),
234            keys: self.keys.to_protobuf(),
235            contents: self.contents.clone().unwrap_or_default(),
236            memo: self.file_memo.clone(),
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use expect_test::expect;
244    use hiero_sdk_proto::services;
245    use time::OffsetDateTime;
246
247    use crate::file::FileUpdateTransactionData;
248    use crate::protobuf::{
249        FromProtobuf,
250        ToProtobuf,
251    };
252    use crate::transaction::test_helpers::{
253        check_body,
254        transaction_body,
255        unused_private_key,
256    };
257    use crate::{
258        AnyTransaction,
259        FileId,
260        FileUpdateTransaction,
261        Key,
262        KeyList,
263    };
264
265    const FILE_ID: FileId = FileId::new(0, 0, 6006);
266
267    const CONTENTS: [u8; 5] = [1, 2, 3, 4, 5];
268
269    const EXPIRATION_TIME: OffsetDateTime = match OffsetDateTime::from_unix_timestamp(1554158728) {
270        Ok(it) => it,
271        Err(_) => panic!("Panic in `const` unwrap"),
272    };
273
274    fn keys() -> impl IntoIterator<Item = Key> {
275        [unused_private_key().public_key().into()]
276    }
277
278    const FILE_MEMO: &str = "new memo";
279
280    fn make_transaction() -> FileUpdateTransaction {
281        let mut tx = FileUpdateTransaction::new_for_tests();
282
283        tx.file_id(FILE_ID)
284            .expiration_time(EXPIRATION_TIME)
285            .contents(CONTENTS.into())
286            .keys(keys())
287            .file_memo(FILE_MEMO)
288            .freeze()
289            .unwrap();
290
291        tx
292    }
293
294    #[test]
295    fn serialize() {
296        let tx = make_transaction();
297
298        let tx = transaction_body(tx);
299
300        let tx = check_body(tx);
301
302        expect![[r#"
303            FileUpdate(
304                FileUpdateTransactionBody {
305                    file_id: Some(
306                        FileId {
307                            shard_num: 0,
308                            realm_num: 0,
309                            file_num: 6006,
310                        },
311                    ),
312                    expiration_time: Some(
313                        Timestamp {
314                            seconds: 1554158728,
315                            nanos: 0,
316                        },
317                    ),
318                    keys: Some(
319                        KeyList {
320                            keys: [
321                                Key {
322                                    key: Some(
323                                        Ed25519(
324                                            [
325                                                224,
326                                                200,
327                                                236,
328                                                39,
329                                                88,
330                                                165,
331                                                135,
332                                                159,
333                                                250,
334                                                194,
335                                                38,
336                                                161,
337                                                60,
338                                                12,
339                                                81,
340                                                107,
341                                                121,
342                                                158,
343                                                114,
344                                                227,
345                                                81,
346                                                65,
347                                                160,
348                                                221,
349                                                130,
350                                                143,
351                                                148,
352                                                211,
353                                                121,
354                                                136,
355                                                164,
356                                                183,
357                                            ],
358                                        ),
359                                    ),
360                                },
361                            ],
362                        },
363                    ),
364                    contents: [
365                        1,
366                        2,
367                        3,
368                        4,
369                        5,
370                    ],
371                    memo: Some(
372                        "new memo",
373                    ),
374                },
375            )
376        "#]]
377        .assert_debug_eq(&tx)
378    }
379
380    #[test]
381    fn to_from_bytes() {
382        let tx = make_transaction();
383
384        let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
385
386        let tx = transaction_body(tx);
387
388        let tx2 = transaction_body(tx2);
389
390        assert_eq!(tx, tx2);
391    }
392
393    #[test]
394    fn from_proto_body() {
395        let tx = services::FileUpdateTransactionBody {
396            file_id: Some(FILE_ID.to_protobuf()),
397            expiration_time: Some(EXPIRATION_TIME.to_protobuf()),
398            keys: Some(KeyList::from_iter(keys()).to_protobuf()),
399            contents: CONTENTS.into(),
400            memo: Some(FILE_MEMO.to_owned()),
401        };
402
403        let tx = FileUpdateTransactionData::from_protobuf(tx).unwrap();
404
405        assert_eq!(tx.contents.as_deref(), Some(CONTENTS.as_slice()));
406        assert_eq!(tx.expiration_time, Some(EXPIRATION_TIME));
407        assert_eq!(tx.keys, Some(KeyList::from_iter(keys())));
408        assert_eq!(tx.file_memo.as_deref(), Some(FILE_MEMO));
409    }
410
411    mod get_set {
412        use super::*;
413
414        #[test]
415        fn contents() {
416            let mut tx = FileUpdateTransaction::new();
417            tx.contents(CONTENTS.into());
418
419            assert_eq!(tx.get_contents(), Some(CONTENTS.as_slice()));
420        }
421
422        #[test]
423        #[should_panic]
424        fn contents_frozen_panics() {
425            make_transaction().contents(CONTENTS.into());
426        }
427
428        #[test]
429        fn expiration_time() {
430            let mut tx = FileUpdateTransaction::new();
431            tx.expiration_time(EXPIRATION_TIME);
432
433            assert_eq!(tx.get_expiration_time(), Some(EXPIRATION_TIME));
434        }
435
436        #[test]
437        #[should_panic]
438        fn expiration_time_frozen_panics() {
439            make_transaction().expiration_time(EXPIRATION_TIME);
440        }
441
442        #[test]
443        fn keys() {
444            let mut tx = FileUpdateTransaction::new();
445            tx.keys(super::keys());
446
447            assert_eq!(tx.get_keys(), Some(&KeyList::from_iter(super::keys())));
448        }
449
450        #[test]
451        #[should_panic]
452        fn keys_frozen_panics() {
453            make_transaction().keys(super::keys());
454        }
455
456        #[test]
457        fn file_memo() {
458            let mut tx = FileUpdateTransaction::new();
459            tx.file_memo(FILE_MEMO);
460
461            assert_eq!(tx.get_file_memo(), Some(FILE_MEMO));
462        }
463
464        #[test]
465        #[should_panic]
466        fn file_memo_frozen_panics() {
467            make_transaction().file_memo(FILE_MEMO);
468        }
469    }
470}