1use 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
34pub type FileAppendTransaction = Transaction<FileAppendTransactionData>;
37
38#[derive(Debug, Clone)]
39pub struct FileAppendTransactionData {
40 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 #[must_use]
60 pub fn get_file_id(&self) -> Option<FileId> {
61 self.data().file_id
62 }
63
64 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 pub fn get_contents(&self) -> Option<&[u8]> {
72 Some(self.data().chunk_data.data.as_slice())
73 }
74
75 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 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 let txes = transaction_bodies(tx);
226
227 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}