hiero_sdk/file/
file_append_transaction.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::cmp;
4use std::num::NonZeroUsize;
5
6use hiero_sdk_proto::services;
7use hiero_sdk_proto::services::file_service_client::FileServiceClient;
8use tonic::transport::Channel;
9
10use crate::ledger_id::RefLedgerId;
11use crate::protobuf::{
12    FromProtobuf,
13    ToProtobuf,
14};
15use crate::transaction::{
16    AnyTransactionData,
17    ChunkData,
18    ChunkInfo,
19    ChunkedTransactionData,
20    ToSchedulableTransactionDataProtobuf,
21    ToTransactionDataProtobuf,
22    TransactionData,
23    TransactionExecute,
24    TransactionExecuteChunked,
25};
26use crate::{
27    BoxGrpcFuture,
28    Error,
29    FileId,
30    Transaction,
31    ValidateChecksums,
32};
33
34/// Append the given contents to the end of the specified file.
35///
36pub type FileAppendTransaction = Transaction<FileAppendTransactionData>;
37
38#[derive(Debug, Clone)]
39pub struct FileAppendTransactionData {
40    /// The file to which the bytes will be appended.
41    file_id: Option<FileId>,
42
43    chunk_data: ChunkData,
44}
45
46impl Default for FileAppendTransactionData {
47    fn default() -> Self {
48        Self {
49            file_id: None,
50            chunk_data: ChunkData {
51                chunk_size: NonZeroUsize::new(4096).unwrap(),
52                ..Default::default()
53            },
54        }
55    }
56}
57impl FileAppendTransaction {
58    /// Returns the file to which the bytes will be appended.
59    #[must_use]
60    pub fn get_file_id(&self) -> Option<FileId> {
61        self.data().file_id
62    }
63
64    /// Sets the file to which the bytes will be appended.
65    pub fn file_id(&mut self, id: impl Into<FileId>) -> &mut Self {
66        self.data_mut().file_id = Some(id.into());
67        self
68    }
69
70    /// Retuns the bytes that will be appended to the end of the specified file.
71    pub fn get_contents(&self) -> Option<&[u8]> {
72        Some(self.data().chunk_data.data.as_slice())
73    }
74
75    /// Sets the bytes that will be appended to the end of the specified file.
76    pub fn contents(&mut self, contents: impl Into<Vec<u8>>) -> &mut Self {
77        self.data_mut().chunk_data.data = contents.into();
78        self
79    }
80}
81
82impl TransactionData for FileAppendTransactionData {
83    fn default_max_transaction_fee(&self) -> crate::Hbar {
84        crate::Hbar::new(5)
85    }
86
87    fn maybe_chunk_data(&self) -> Option<&ChunkData> {
88        Some(self.chunk_data())
89    }
90
91    fn wait_for_receipt(&self) -> bool {
92        true
93    }
94}
95
96impl ChunkedTransactionData for FileAppendTransactionData {
97    fn chunk_data(&self) -> &ChunkData {
98        &self.chunk_data
99    }
100
101    fn chunk_data_mut(&mut self) -> &mut ChunkData {
102        &mut self.chunk_data
103    }
104}
105
106impl TransactionExecute for FileAppendTransactionData {
107    fn execute(
108        &self,
109        channel: Channel,
110        request: services::Transaction,
111    ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
112        Box::pin(async { FileServiceClient::new(channel).append_content(request).await })
113    }
114}
115
116impl TransactionExecuteChunked for FileAppendTransactionData {}
117
118impl ValidateChecksums for FileAppendTransactionData {
119    fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
120        self.file_id.validate_checksums(ledger_id)
121    }
122}
123
124impl ToTransactionDataProtobuf for FileAppendTransactionData {
125    fn to_transaction_data_protobuf(
126        &self,
127        chunk_info: &ChunkInfo,
128    ) -> services::transaction_body::Data {
129        services::transaction_body::Data::FileAppend(services::FileAppendTransactionBody {
130            file_id: self.file_id.to_protobuf(),
131            contents: self.chunk_data.message_chunk(chunk_info).to_vec(),
132        })
133    }
134}
135
136impl ToSchedulableTransactionDataProtobuf for FileAppendTransactionData {
137    fn to_schedulable_transaction_data_protobuf(
138        &self,
139    ) -> services::schedulable_transaction_body::Data {
140        assert!(self.chunk_data.used_chunks() == 1);
141
142        services::schedulable_transaction_body::Data::FileAppend(
143            services::FileAppendTransactionBody {
144                file_id: self.file_id.to_protobuf(),
145                contents: self.chunk_data.data.clone(),
146            },
147        )
148    }
149}
150
151impl From<FileAppendTransactionData> for AnyTransactionData {
152    fn from(transaction: FileAppendTransactionData) -> Self {
153        Self::FileAppend(transaction)
154    }
155}
156
157impl FromProtobuf<services::FileAppendTransactionBody> for FileAppendTransactionData {
158    fn from_protobuf(pb: services::FileAppendTransactionBody) -> crate::Result<Self> {
159        Self::from_protobuf(Vec::from([pb]))
160    }
161}
162
163impl FromProtobuf<Vec<services::FileAppendTransactionBody>> for FileAppendTransactionData {
164    fn from_protobuf(pb: Vec<services::FileAppendTransactionBody>) -> crate::Result<Self> {
165        let total_chunks = pb.len();
166
167        let mut iter = pb.into_iter();
168        let pb_first = iter.next().expect("Empty transaction (should've been handled earlier)");
169
170        let file_id = Option::from_protobuf(pb_first.file_id)?;
171
172        let mut largest_chunk_size = pb_first.contents.len();
173        let mut contents = pb_first.contents;
174
175        // note: no other SDK checks for correctness here... so let's not do it here either?
176
177        for item in iter {
178            largest_chunk_size = cmp::max(largest_chunk_size, item.contents.len());
179            contents.extend_from_slice(&item.contents);
180        }
181
182        Ok(Self {
183            file_id,
184            chunk_data: ChunkData {
185                max_chunks: total_chunks,
186                chunk_size: NonZeroUsize::new(largest_chunk_size)
187                    .unwrap_or_else(|| NonZeroUsize::new(1).unwrap()),
188                data: contents,
189            },
190        })
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use expect_test::expect;
197
198    use crate::transaction::test_helpers::{
199        check_body,
200        transaction_bodies,
201    };
202    use crate::{
203        AnyTransaction,
204        FileAppendTransaction,
205        FileId,
206    };
207
208    const FILE_ID: FileId = FileId::new(0, 0, 10);
209
210    const CONTENTS: &[u8] = br#"{"foo": 231}"#;
211
212    fn make_transaction() -> FileAppendTransaction {
213        let mut tx = FileAppendTransaction::new_for_tests();
214        tx.file_id(FILE_ID).contents(CONTENTS).freeze().unwrap();
215
216        tx
217    }
218
219    #[test]
220    fn serialize() {
221        let tx = make_transaction();
222
223        // unlike most transactions we *do* need to do this like in case it's chunked.
224        // granted, trying to do anything with a chunked transaction without a Client is hard.
225        let txes = transaction_bodies(tx);
226
227        // this is kinda a mess... But it works.
228        let txes: Vec<_> = txes.into_iter().map(check_body).collect();
229
230        expect![[r#"
231            [
232                FileAppend(
233                    FileAppendTransactionBody {
234                        file_id: Some(
235                            FileId {
236                                shard_num: 0,
237                                realm_num: 0,
238                                file_num: 10,
239                            },
240                        ),
241                        contents: [
242                            123,
243                            34,
244                            102,
245                            111,
246                            111,
247                            34,
248                            58,
249                            32,
250                            50,
251                            51,
252                            49,
253                            125,
254                        ],
255                    },
256                ),
257                FileAppend(
258                    FileAppendTransactionBody {
259                        file_id: Some(
260                            FileId {
261                                shard_num: 0,
262                                realm_num: 0,
263                                file_num: 10,
264                            },
265                        ),
266                        contents: [
267                            123,
268                            34,
269                            102,
270                            111,
271                            111,
272                            34,
273                            58,
274                            32,
275                            50,
276                            51,
277                            49,
278                            125,
279                        ],
280                    },
281                ),
282            ]
283        "#]]
284        .assert_debug_eq(&txes);
285    }
286
287    #[test]
288    fn to_from_bytes() {
289        let tx = make_transaction();
290
291        let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
292
293        let tx = transaction_bodies(tx);
294        let tx2 = transaction_bodies(tx2);
295
296        assert_eq!(tx, tx2);
297    }
298
299    #[test]
300    fn get_set_file_id() {
301        let mut tx = FileAppendTransaction::new();
302        tx.file_id(FILE_ID);
303
304        assert_eq!(tx.get_file_id(), Some(FILE_ID));
305    }
306
307    #[test]
308    fn get_set_contents() {
309        let mut tx = FileAppendTransaction::new();
310        tx.contents(CONTENTS);
311
312        assert_eq!(tx.get_contents(), Some(CONTENTS));
313    }
314
315    #[test]
316    #[should_panic]
317    fn get_set_file_id_frozen_panics() {
318        let mut tx = make_transaction();
319        tx.file_id(FILE_ID);
320    }
321
322    #[test]
323    #[should_panic]
324    fn get_set_contents_frozen_panics() {
325        let mut tx = make_transaction();
326        tx.contents(CONTENTS);
327    }
328}