Skip to main content

hiero_sdk/topic/
topic_message_submit_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::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
34/// Submit a message for consensus.
35///
36/// Valid and authorized messages on valid topics will be ordered by the consensus service, gossipped to the
37/// mirror net, and published (in order) to all subscribers (from the mirror net) on this topic.
38///
39/// The `submit_key` (if any) must sign this transaction.
40///
41/// On success, the resulting `TransactionReceipt` contains the topic's updated `topic_sequence_number` and
42/// `topic_running_hash`.
43///
44pub type TopicMessageSubmitTransaction = Transaction<TopicMessageSubmitTransactionData>;
45
46#[derive(Debug, Default, Clone)]
47pub struct TopicMessageSubmitTransactionData {
48    /// The topic ID to submit this message to.
49    topic_id: Option<TopicId>,
50
51    chunk_data: ChunkData,
52}
53
54impl TopicMessageSubmitTransaction {
55    /// Returns the ID of the topic this message will be submitted to.
56    #[must_use]
57    pub fn get_topic_id(&self) -> Option<TopicId> {
58        self.data().topic_id
59    }
60
61    /// Sets the topic ID to submit this message to.
62    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    /// Returns the message to be submitted.
68    pub fn get_message(&self) -> Option<&[u8]> {
69        Some(self.data().chunk_data.data.as_slice())
70    }
71
72    /// Sets the message to be submitted.
73    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        // note: no other SDK checks for correctness here... so let's not do it here either?
186
187        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        // unlike most transactions we *do* need to do this like in case it's chunked.
234        // granted, trying to do anything with a chunked transaction without a Client is hard.
235        let txes = transaction_bodies(tx);
236
237        // this is kinda a mess... But it works.
238        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}