1use hiero_sdk_proto::services;
4use hiero_sdk_proto::services::consensus_service_client::ConsensusServiceClient;
5use time::Duration;
6use tonic::transport::Channel;
7
8use crate::custom_fixed_fee::CustomFixedFee;
9use crate::ledger_id::RefLedgerId;
10use crate::protobuf::{
11 FromProtobuf,
12 ToProtobuf,
13};
14use crate::transaction::{
15 AnyTransactionData,
16 ChunkInfo,
17 ToSchedulableTransactionDataProtobuf,
18 ToTransactionDataProtobuf,
19 TransactionData,
20 TransactionExecute,
21};
22use crate::{
23 AccountId,
24 BoxGrpcFuture,
25 Error,
26 Hbar,
27 Key,
28 Transaction,
29 ValidateChecksums,
30};
31
32pub type TopicCreateTransaction = Transaction<TopicCreateTransactionData>;
41
42#[derive(Debug, Clone)]
43pub struct TopicCreateTransactionData {
44 topic_memo: String,
46
47 admin_key: Option<Key>,
49
50 submit_key: Option<Key>,
52
53 auto_renew_period: Option<Duration>,
57
58 auto_renew_account_id: Option<AccountId>,
60
61 fee_schedule_key: Option<Key>,
63
64 fee_exempt_keys: Vec<Key>,
66
67 custom_fees: Vec<CustomFixedFee>,
69}
70
71impl Default for TopicCreateTransactionData {
72 fn default() -> Self {
73 Self {
74 topic_memo: String::new(),
75 admin_key: None,
76 submit_key: None,
77 auto_renew_period: Some(Duration::days(90)),
78 auto_renew_account_id: None,
79 fee_schedule_key: None,
80 fee_exempt_keys: vec![],
81 custom_fees: vec![],
82 }
83 }
84}
85
86impl TopicCreateTransaction {
87 #[must_use]
89 pub fn get_topic_memo(&self) -> &str {
90 &self.data().topic_memo
91 }
92
93 pub fn topic_memo(&mut self, memo: impl Into<String>) -> &mut Self {
97 self.data_mut().topic_memo = memo.into();
98 self
99 }
100
101 #[must_use]
104 pub fn get_admin_key(&self) -> Option<&Key> {
105 self.data().admin_key.as_ref()
106 }
107
108 pub fn admin_key(&mut self, key: impl Into<Key>) -> &mut Self {
111 self.data_mut().admin_key = Some(key.into());
112 self
113 }
114
115 #[must_use]
117 pub fn get_submit_key(&self) -> Option<&Key> {
118 self.data().submit_key.as_ref()
119 }
120
121 pub fn submit_key(&mut self, key: impl Into<Key>) -> &mut Self {
123 self.data_mut().submit_key = Some(key.into());
124 self
125 }
126
127 #[must_use]
130 pub fn get_auto_renew_period(&self) -> Option<Duration> {
131 self.data().auto_renew_period
132 }
133
134 pub fn auto_renew_period(&mut self, period: Duration) -> &mut Self {
137 self.data_mut().auto_renew_period = Some(period);
138 self
139 }
140
141 #[must_use]
143 pub fn get_auto_renew_account_id(&self) -> Option<AccountId> {
144 self.data().auto_renew_account_id
145 }
146
147 pub fn auto_renew_account_id(&mut self, id: AccountId) -> &mut Self {
149 self.data_mut().auto_renew_account_id = Some(id);
150 self
151 }
152
153 pub fn fee_schedule_key(&mut self, key: impl Into<Key>) -> &mut Self {
155 self.data_mut().fee_schedule_key = Some(key.into());
156 self
157 }
158
159 #[must_use]
161 pub fn get_fee_schedule_key(&self) -> Option<&Key> {
162 self.data().fee_schedule_key.as_ref()
163 }
164
165 pub fn fee_exempt_keys(&mut self, keys: Vec<Key>) -> &mut Self {
167 self.data_mut().fee_exempt_keys = keys;
168 self
169 }
170
171 #[must_use]
173 pub fn get_fee_exempt_keys(&self) -> &Vec<Key> {
174 &self.data().fee_exempt_keys
175 }
176
177 pub fn clear_fee_exempt_keys(&mut self) -> &mut Self {
179 self.data_mut().fee_exempt_keys.clear();
180 self
181 }
182
183 pub fn add_fee_exempt_key(&mut self, key: impl Into<Key>) -> &mut Self {
185 self.data_mut().fee_exempt_keys.push(key.into());
186 self
187 }
188
189 #[must_use]
191 pub fn get_custom_fees(&self) -> &Vec<CustomFixedFee> {
192 &self.data().custom_fees
193 }
194
195 pub fn custom_fees(&mut self, fees: Vec<CustomFixedFee>) -> &mut Self {
197 self.data_mut().custom_fees = fees;
198 self
199 }
200
201 pub fn clear_custom_fees(&mut self) -> &mut Self {
203 self.data_mut().custom_fees.clear();
204 self
205 }
206
207 pub fn add_custom_fee(&mut self, fee: CustomFixedFee) -> &mut Self {
209 self.data_mut().custom_fees.push(fee);
210 self
211 }
212}
213
214impl TransactionData for TopicCreateTransactionData {
215 fn default_max_transaction_fee(&self) -> Hbar {
216 Hbar::new(25)
217 }
218}
219
220impl TransactionExecute for TopicCreateTransactionData {
221 fn execute(
222 &self,
223 channel: Channel,
224 request: services::Transaction,
225 ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
226 Box::pin(async { ConsensusServiceClient::new(channel).create_topic(request).await })
227 }
228}
229
230impl ValidateChecksums for TopicCreateTransactionData {
231 fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
232 self.auto_renew_account_id.validate_checksums(ledger_id)
233 }
234}
235
236impl ToTransactionDataProtobuf for TopicCreateTransactionData {
237 fn to_transaction_data_protobuf(
238 &self,
239 chunk_info: &ChunkInfo,
240 ) -> services::transaction_body::Data {
241 let _ = chunk_info.assert_single_transaction();
242
243 let mut protobuf_data = self.to_protobuf();
245
246 if protobuf_data.auto_renew_account.is_none() {
248 let operator_id = chunk_info.current_transaction_id.account_id;
249 protobuf_data.auto_renew_account = Some(operator_id.to_protobuf());
250 }
251 services::transaction_body::Data::ConsensusCreateTopic(protobuf_data)
252 }
253}
254
255impl ToSchedulableTransactionDataProtobuf for TopicCreateTransactionData {
256 fn to_schedulable_transaction_data_protobuf(
257 &self,
258 ) -> services::schedulable_transaction_body::Data {
259 services::schedulable_transaction_body::Data::ConsensusCreateTopic(self.to_protobuf())
260 }
261}
262
263impl From<TopicCreateTransactionData> for AnyTransactionData {
264 fn from(transaction: TopicCreateTransactionData) -> Self {
265 Self::TopicCreate(transaction)
266 }
267}
268
269impl FromProtobuf<services::ConsensusCreateTopicTransactionBody> for TopicCreateTransactionData {
270 fn from_protobuf(pb: services::ConsensusCreateTopicTransactionBody) -> crate::Result<Self> {
271 let custom_fees = pb
272 .custom_fees
273 .into_iter()
274 .map(CustomFixedFee::from_protobuf)
275 .collect::<Result<Vec<_>, _>>()?;
276
277 let fee_exempt_keys = pb
278 .fee_exempt_key_list
279 .into_iter()
280 .map(Key::from_protobuf)
281 .collect::<Result<Vec<_>, _>>()?;
282
283 Ok(Self {
284 topic_memo: pb.memo,
285 admin_key: Option::from_protobuf(pb.admin_key)?,
286 submit_key: Option::from_protobuf(pb.submit_key)?,
287 auto_renew_period: pb.auto_renew_period.map(Into::into),
288 auto_renew_account_id: Option::from_protobuf(pb.auto_renew_account)?,
289 fee_schedule_key: Option::from_protobuf(pb.fee_schedule_key)?,
290 fee_exempt_keys,
291 custom_fees,
292 })
293 }
294}
295
296impl ToProtobuf for TopicCreateTransactionData {
297 type Protobuf = services::ConsensusCreateTopicTransactionBody;
298
299 fn to_protobuf(&self) -> Self::Protobuf {
300 let custom_fees = self.custom_fees.iter().map(|fee| fee.to_protobuf()).collect::<Vec<_>>();
301 let fee_exempt_key_list =
302 self.fee_exempt_keys.iter().map(|key| key.to_protobuf()).collect::<Vec<_>>();
303 let fee_schedule_key = self.fee_schedule_key.as_ref().map(|key| key.to_protobuf());
304
305 services::ConsensusCreateTopicTransactionBody {
306 auto_renew_account: self.auto_renew_account_id.to_protobuf(),
307 memo: self.topic_memo.clone(),
308 admin_key: self.admin_key.to_protobuf(),
309 submit_key: self.submit_key.to_protobuf(),
310 auto_renew_period: self.auto_renew_period.to_protobuf(),
311 custom_fees,
312 fee_exempt_key_list,
313 fee_schedule_key,
314 }
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use expect_test::expect;
321 use hiero_sdk_proto::services;
322 use time::Duration;
323
324 use super::TopicCreateTransactionData;
325 use crate::custom_fixed_fee::CustomFixedFee;
326 use crate::protobuf::{
327 FromProtobuf,
328 ToProtobuf,
329 };
330 use crate::transaction::test_helpers::{
331 check_body,
332 transaction_body,
333 unused_private_key,
334 };
335 use crate::{
336 AccountId,
337 AnyTransaction,
338 PrivateKey,
339 PublicKey,
340 TokenId,
341 TopicCreateTransaction,
342 };
343
344 fn key() -> PublicKey {
345 unused_private_key().public_key()
346 }
347
348 const AUTO_RENEW_ACCOUNT_ID: AccountId = AccountId::new(0, 0, 5007);
349 const AUTO_RENEW_PERIOD: Duration = Duration::days(1);
350
351 fn make_transaction() -> TopicCreateTransaction {
352 let mut tx = TopicCreateTransaction::new_for_tests();
353
354 tx.submit_key(key())
355 .admin_key(key())
356 .auto_renew_account_id(AUTO_RENEW_ACCOUNT_ID)
357 .auto_renew_period(AUTO_RENEW_PERIOD)
358 .freeze()
359 .unwrap();
360
361 tx
362 }
363
364 #[test]
365 fn serialize() {
366 let tx = make_transaction();
367
368 let tx = transaction_body(tx);
369
370 let tx = check_body(tx);
371
372 expect![[r#"
373 ConsensusCreateTopic(
374 ConsensusCreateTopicTransactionBody {
375 memo: "",
376 admin_key: Some(
377 Key {
378 key: Some(
379 Ed25519(
380 [
381 224,
382 200,
383 236,
384 39,
385 88,
386 165,
387 135,
388 159,
389 250,
390 194,
391 38,
392 161,
393 60,
394 12,
395 81,
396 107,
397 121,
398 158,
399 114,
400 227,
401 81,
402 65,
403 160,
404 221,
405 130,
406 143,
407 148,
408 211,
409 121,
410 136,
411 164,
412 183,
413 ],
414 ),
415 ),
416 },
417 ),
418 submit_key: Some(
419 Key {
420 key: Some(
421 Ed25519(
422 [
423 224,
424 200,
425 236,
426 39,
427 88,
428 165,
429 135,
430 159,
431 250,
432 194,
433 38,
434 161,
435 60,
436 12,
437 81,
438 107,
439 121,
440 158,
441 114,
442 227,
443 81,
444 65,
445 160,
446 221,
447 130,
448 143,
449 148,
450 211,
451 121,
452 136,
453 164,
454 183,
455 ],
456 ),
457 ),
458 },
459 ),
460 auto_renew_period: Some(
461 Duration {
462 seconds: 86400,
463 },
464 ),
465 auto_renew_account: Some(
466 AccountId {
467 shard_num: 0,
468 realm_num: 0,
469 account: Some(
470 AccountNum(
471 5007,
472 ),
473 ),
474 },
475 ),
476 fee_schedule_key: None,
477 fee_exempt_key_list: [],
478 custom_fees: [],
479 },
480 )
481 "#]]
482 .assert_debug_eq(&tx)
483 }
484
485 #[test]
486 fn to_from_bytes() {
487 let tx = make_transaction();
488
489 let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
490
491 let tx = transaction_body(tx);
492
493 let tx2 = transaction_body(tx2);
494
495 assert_eq!(tx, tx2);
496 }
497
498 #[test]
499 fn from_proto_body() {
500 let tx = services::ConsensusCreateTopicTransactionBody {
501 memo: String::new(),
502 admin_key: Some(key().to_protobuf()),
503 submit_key: Some(key().to_protobuf()),
504 auto_renew_period: Some(AUTO_RENEW_PERIOD.to_protobuf()),
505 auto_renew_account: Some(AUTO_RENEW_ACCOUNT_ID.to_protobuf()),
506 custom_fees: vec![],
507 fee_exempt_key_list: vec![],
508 fee_schedule_key: None,
509 };
510
511 let tx = TopicCreateTransactionData::from_protobuf(tx).unwrap();
512
513 assert_eq!(tx.admin_key, Some(key().into()));
514 assert_eq!(tx.submit_key, Some(key().into()));
515 assert_eq!(tx.auto_renew_period, Some(AUTO_RENEW_PERIOD));
516 assert_eq!(tx.auto_renew_account_id, Some(AUTO_RENEW_ACCOUNT_ID));
517 }
518
519 #[test]
520 fn get_set_admin_key() {
521 let mut tx = TopicCreateTransaction::new();
522 tx.admin_key(key());
523
524 assert_eq!(tx.get_admin_key(), Some(&key().into()));
525 }
526
527 #[test]
528 #[should_panic]
529 fn get_set_admin_key_frozen_panics() {
530 make_transaction().admin_key(key());
531 }
532
533 #[test]
534 fn get_set_submit_key() {
535 let mut tx = TopicCreateTransaction::new();
536 tx.submit_key(key());
537
538 assert_eq!(tx.get_submit_key(), Some(&key().into()));
539 }
540
541 #[test]
542 #[should_panic]
543 fn get_set_submit_key_frozen_panics() {
544 make_transaction().submit_key(key());
545 }
546
547 #[test]
548 fn get_set_auto_renew_period() {
549 let mut tx = TopicCreateTransaction::new();
550 tx.auto_renew_period(AUTO_RENEW_PERIOD);
551
552 assert_eq!(tx.get_auto_renew_period(), Some(AUTO_RENEW_PERIOD));
553 }
554
555 #[test]
556 #[should_panic]
557 fn get_set_auto_renew_period_frozen_panics() {
558 make_transaction().auto_renew_period(AUTO_RENEW_PERIOD);
559 }
560
561 #[test]
562 fn get_set_auto_renew_account_id() {
563 let mut tx = TopicCreateTransaction::new();
564 tx.auto_renew_account_id(AUTO_RENEW_ACCOUNT_ID);
565
566 assert_eq!(tx.get_auto_renew_account_id(), Some(AUTO_RENEW_ACCOUNT_ID));
567 }
568
569 #[test]
570 #[should_panic]
571 fn get_set_auto_renew_account_id_frozen_panics() {
572 make_transaction().auto_renew_account_id(AUTO_RENEW_ACCOUNT_ID);
573 }
574
575 #[test]
576 fn get_set_fee_schedule_key() {
577 let mut tx = TopicCreateTransaction::new();
578 tx.fee_schedule_key(key());
579
580 assert_eq!(tx.get_fee_schedule_key(), Some(&key().into()));
581 }
582
583 #[test]
584 #[should_panic]
585 fn get_set_fee_schedule_key_frozen_panics() {
586 make_transaction().fee_schedule_key(key());
587 }
588
589 #[test]
590 fn get_set_fee_exempt_keys() {
591 let keys = vec![PrivateKey::generate_ecdsa(), PrivateKey::generate_ecdsa()];
592 let mut tx = TopicCreateTransaction::new();
593 tx.fee_exempt_keys(keys.iter().map(|key| key.public_key().into()).collect());
594
595 assert_eq!(
596 tx.get_fee_exempt_keys(),
597 &keys.iter().map(|key| key.public_key().into()).collect::<Vec<_>>()
598 );
599 }
600
601 #[test]
602 fn get_set_custom_fees() {
603 let mut tx = TopicCreateTransaction::new();
604 tx.custom_fees(vec![
605 CustomFixedFee::new(100, Some(TokenId::new(1, 2, 3)), Some(AccountId::new(4, 5, 6))),
606 CustomFixedFee::new(200, None, None),
607 ]);
608
609 assert_eq!(
610 tx.get_custom_fees(),
611 &vec![
612 CustomFixedFee::new(
613 100,
614 Some(TokenId::new(1, 2, 3)),
615 Some(AccountId::new(4, 5, 6))
616 ),
617 CustomFixedFee::new(200, None, None)
618 ]
619 );
620 }
621
622 #[test]
623 fn add_topic_custom_fee_to_list() {
624 let custom_fixed_fees = vec![
625 CustomFixedFee::new(1, Some(TokenId::new(0, 0, 0)), None),
626 CustomFixedFee::new(2, Some(TokenId::new(0, 0, 1)), None),
627 CustomFixedFee::new(3, Some(TokenId::new(0, 0, 2)), None),
628 ];
629
630 let custom_fee_to_add = CustomFixedFee::new(4, Some(TokenId::new(0, 0, 3)), None);
631
632 let mut expected_custom_fees = custom_fixed_fees.clone();
633 expected_custom_fees.push(custom_fee_to_add.clone());
634
635 let mut tx = TopicCreateTransaction::new();
636 tx.custom_fees(custom_fixed_fees);
637 tx.add_custom_fee(custom_fee_to_add);
638
639 assert_eq!(tx.get_custom_fees().len(), expected_custom_fees.len());
640 assert_eq!(tx.get_custom_fees(), &expected_custom_fees);
641 }
642
643 #[test]
644 fn add_topic_custom_fee_to_empty_list() {
645 let custom_fee_to_add = CustomFixedFee::new(4, Some(TokenId::new(0, 0, 3)), None);
646
647 let mut tx = TopicCreateTransaction::new();
648 tx.add_custom_fee(custom_fee_to_add.clone());
649
650 assert_eq!(tx.get_custom_fees().len(), 1);
651 assert_eq!(tx.get_custom_fees(), &vec![custom_fee_to_add]);
652 }
653
654 #[test]
655 fn add_fee_exempt_key_to_empty_list() {
656 let mut tx = TopicCreateTransaction::new();
657
658 let fee_exempt_key = PrivateKey::generate_ecdsa();
659 tx.add_fee_exempt_key(fee_exempt_key.public_key());
660
661 assert_eq!(tx.get_fee_exempt_keys().len(), 1);
662 assert_eq!(tx.get_fee_exempt_keys(), &vec![fee_exempt_key.public_key().into()]);
663 }
664
665 #[test]
666 fn add_fee_exempt_key_to_list() {
667 let fee_exempt_key = PrivateKey::generate_ecdsa();
668 let mut tx = TopicCreateTransaction::new();
669 tx.fee_exempt_keys(vec![fee_exempt_key.public_key().into()]);
670
671 let fee_exempt_key_to_add = PrivateKey::generate_ecdsa();
672 tx.add_fee_exempt_key(fee_exempt_key_to_add.public_key());
673
674 let expected_keys =
675 vec![fee_exempt_key.public_key().into(), fee_exempt_key_to_add.public_key().into()];
676
677 assert_eq!(tx.get_fee_exempt_keys().len(), 2);
678 assert_eq!(tx.get_fee_exempt_keys(), &expected_keys);
679 }
680}