1use std::cmp;
4use std::num::NonZeroUsize;
5
6use hiero_sdk_proto::services;
7use hiero_sdk_proto::services::consensus_service_client::ConsensusServiceClient;
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 TopicId,
30 Transaction,
31 ValidateChecksums,
32};
33
34pub type TopicMessageSubmitTransaction = Transaction<TopicMessageSubmitTransactionData>;
45
46#[derive(Debug, Default, Clone)]
47pub struct TopicMessageSubmitTransactionData {
48 topic_id: Option<TopicId>,
50
51 chunk_data: ChunkData,
52}
53
54impl TopicMessageSubmitTransaction {
55 #[must_use]
57 pub fn get_topic_id(&self) -> Option<TopicId> {
58 self.data().topic_id
59 }
60
61 pub fn topic_id(&mut self, id: impl Into<TopicId>) -> &mut Self {
63 self.data_mut().topic_id = Some(id.into());
64 self
65 }
66
67 pub fn get_message(&self) -> Option<&[u8]> {
69 Some(self.data().chunk_data.data.as_slice())
70 }
71
72 pub fn message(&mut self, bytes: impl Into<Vec<u8>>) -> &mut Self {
74 self.data_mut().chunk_data_mut().data = bytes.into();
75 self
76 }
77}
78
79impl TransactionData for TopicMessageSubmitTransactionData {
80 fn maybe_chunk_data(&self) -> Option<&ChunkData> {
81 Some(self.chunk_data())
82 }
83
84 fn wait_for_receipt(&self) -> bool {
85 false
86 }
87}
88
89impl ChunkedTransactionData for TopicMessageSubmitTransactionData {
90 fn chunk_data(&self) -> &ChunkData {
91 &self.chunk_data
92 }
93
94 fn chunk_data_mut(&mut self) -> &mut ChunkData {
95 &mut self.chunk_data
96 }
97}
98
99impl TransactionExecute for TopicMessageSubmitTransactionData {
100 fn execute(
101 &self,
102 channel: Channel,
103 request: services::Transaction,
104 ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
105 Box::pin(async { ConsensusServiceClient::new(channel).submit_message(request).await })
106 }
107}
108
109impl TransactionExecuteChunked for TopicMessageSubmitTransactionData {}
110
111impl ValidateChecksums for TopicMessageSubmitTransactionData {
112 fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
113 self.topic_id.validate_checksums(ledger_id)
114 }
115}
116
117impl ToTransactionDataProtobuf for TopicMessageSubmitTransactionData {
118 fn to_transaction_data_protobuf(
119 &self,
120 chunk_info: &ChunkInfo,
121 ) -> services::transaction_body::Data {
122 services::transaction_body::Data::ConsensusSubmitMessage(
123 services::ConsensusSubmitMessageTransactionBody {
124 topic_id: self.topic_id.to_protobuf(),
125 message: self.chunk_data.message_chunk(chunk_info).to_vec(),
126 chunk_info: (chunk_info.total > 1).then(|| services::ConsensusMessageChunkInfo {
127 initial_transaction_id: Some(chunk_info.initial_transaction_id.to_protobuf()),
128 number: (chunk_info.current + 1) as i32,
129 total: chunk_info.total as i32,
130 }),
131 },
132 )
133 }
134}
135
136impl ToSchedulableTransactionDataProtobuf for TopicMessageSubmitTransactionData {
137 fn to_schedulable_transaction_data_protobuf(
138 &self,
139 ) -> services::schedulable_transaction_body::Data {
140 assert!(
141 self.chunk_data.used_chunks() == 1,
142 "Cannot schedule a `TopicMessageSubmitTransaction` with multiple chunks"
143 );
144
145 let data = services::ConsensusSubmitMessageTransactionBody {
146 topic_id: self.topic_id.to_protobuf(),
147 message: self.chunk_data.data.clone(),
148 chunk_info: None,
149 };
150
151 services::schedulable_transaction_body::Data::ConsensusSubmitMessage(data)
152 }
153}
154
155impl From<TopicMessageSubmitTransactionData> for AnyTransactionData {
156 fn from(transaction: TopicMessageSubmitTransactionData) -> Self {
157 Self::TopicMessageSubmit(transaction)
158 }
159}
160
161impl FromProtobuf<services::ConsensusSubmitMessageTransactionBody>
162 for TopicMessageSubmitTransactionData
163{
164 fn from_protobuf(pb: services::ConsensusSubmitMessageTransactionBody) -> crate::Result<Self> {
165 Self::from_protobuf(Vec::from([pb]))
166 }
167}
168
169impl FromProtobuf<Vec<services::ConsensusSubmitMessageTransactionBody>>
170 for TopicMessageSubmitTransactionData
171{
172 fn from_protobuf(
173 pb: Vec<services::ConsensusSubmitMessageTransactionBody>,
174 ) -> crate::Result<Self> {
175 let total_chunks = pb.len();
176
177 let mut iter = pb.into_iter();
178 let pb_first = iter.next().expect("Empty transaction (should've been handled earlier)");
179
180 let topic_id = Option::from_protobuf(pb_first.topic_id)?;
181
182 let mut largest_chunk_size = pb_first.message.len();
183 let mut message = pb_first.message;
184
185 for item in iter {
188 largest_chunk_size = cmp::max(largest_chunk_size, item.message.len());
189 message.extend_from_slice(&item.message);
190 }
191
192 Ok(Self {
193 topic_id,
194 chunk_data: ChunkData {
195 max_chunks: total_chunks,
196 chunk_size: NonZeroUsize::new(largest_chunk_size)
197 .unwrap_or_else(|| NonZeroUsize::new(1).unwrap()),
198 data: message,
199 },
200 })
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use expect_test::expect;
207
208 use crate::transaction::test_helpers::{
209 check_body,
210 transaction_bodies,
211 };
212 use crate::{
213 AnyTransaction,
214 TopicId,
215 TopicMessageSubmitTransaction,
216 };
217
218 const TOPIC_ID: TopicId = TopicId::new(0, 0, 10);
219
220 const MESSAGE: &[u8] = br#"{"foo": 231}"#;
221
222 fn make_transaction() -> TopicMessageSubmitTransaction {
223 let mut tx = TopicMessageSubmitTransaction::new_for_tests();
224 tx.topic_id(TOPIC_ID).message(MESSAGE).freeze().unwrap();
225
226 tx
227 }
228
229 #[test]
230 fn serialize() {
231 let tx = make_transaction();
232
233 let txes = transaction_bodies(tx);
236
237 let txes: Vec<_> = txes.into_iter().map(check_body).collect();
239
240 expect![[r#"
241 [
242 ConsensusSubmitMessage(
243 ConsensusSubmitMessageTransactionBody {
244 topic_id: Some(
245 TopicId {
246 shard_num: 0,
247 realm_num: 0,
248 topic_num: 10,
249 },
250 ),
251 message: [
252 123,
253 34,
254 102,
255 111,
256 111,
257 34,
258 58,
259 32,
260 50,
261 51,
262 49,
263 125,
264 ],
265 chunk_info: None,
266 },
267 ),
268 ConsensusSubmitMessage(
269 ConsensusSubmitMessageTransactionBody {
270 topic_id: Some(
271 TopicId {
272 shard_num: 0,
273 realm_num: 0,
274 topic_num: 10,
275 },
276 ),
277 message: [
278 123,
279 34,
280 102,
281 111,
282 111,
283 34,
284 58,
285 32,
286 50,
287 51,
288 49,
289 125,
290 ],
291 chunk_info: None,
292 },
293 ),
294 ]
295 "#]]
296 .assert_debug_eq(&txes);
297 }
298
299 #[test]
300 fn to_from_bytes() {
301 let tx = make_transaction();
302
303 let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
304
305 let tx = transaction_bodies(tx);
306 let tx2 = transaction_bodies(tx2);
307
308 assert_eq!(tx, tx2);
309 }
310
311 #[test]
312 fn get_set_topic_id() {
313 let mut tx = TopicMessageSubmitTransaction::new();
314 tx.topic_id(TOPIC_ID);
315
316 assert_eq!(tx.get_topic_id(), Some(TOPIC_ID));
317 }
318
319 #[test]
320 fn get_set_message() {
321 let mut tx = TopicMessageSubmitTransaction::new();
322 tx.message(MESSAGE);
323
324 assert_eq!(tx.get_message(), Some(MESSAGE));
325 }
326
327 #[test]
328 #[should_panic]
329 fn get_set_topic_id_frozen_panics() {
330 let mut tx = make_transaction();
331 tx.topic_id(TOPIC_ID);
332 }
333
334 #[test]
335 #[should_panic]
336 fn get_set_message_frozen_panics() {
337 let mut tx = make_transaction();
338 tx.message(MESSAGE);
339 }
340}