Skip to main content

hiero_sdk/topic/
topic_create_transaction.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use 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
32/// Create a topic to be used for consensus.
33///
34/// If an `auto_renew_account_id` is specified, that account must also sign this transaction.
35///
36/// If an `admin_key` is specified, the adminKey must sign the transaction.
37///
38/// On success, the resulting `TransactionReceipt` contains the newly created `TopicId`.
39///
40pub type TopicCreateTransaction = Transaction<TopicCreateTransactionData>;
41
42#[derive(Debug, Clone)]
43pub struct TopicCreateTransactionData {
44    /// Short publicly visible memo about the topic. No guarantee of uniqueness.
45    topic_memo: String,
46
47    /// Access control for `TopicUpdateTransaction` and `TopicDeleteTransaction`.
48    admin_key: Option<Key>,
49
50    /// Access control for `TopicMessageSubmitTransaction`.
51    submit_key: Option<Key>,
52
53    /// The initial lifetime of the topic and the amount of time to attempt to
54    /// extend the topic's lifetime by automatically at the topic's expiration time, if
55    /// the `auto_renew_account_id` is configured.
56    auto_renew_period: Option<Duration>,
57
58    /// Account to be used at the topic's expiration time to extend the life of the topic.
59    auto_renew_account_id: Option<AccountId>,
60
61    /// The key that can be used to update the custom fees for this topic.
62    fee_schedule_key: Option<Key>,
63
64    /// If the transaction contains a signer from this list, no custom fees are applied.
65    fee_exempt_keys: Vec<Key>,
66
67    /// The custom fee to be assessed during a message submission to this topic. Empty if no custom fees are applied.
68    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    /// Returns the short, publicly visible, memo about the topic.
88    #[must_use]
89    pub fn get_topic_memo(&self) -> &str {
90        &self.data().topic_memo
91    }
92
93    /// Sets the short publicly visible memo about the topic.
94    ///
95    /// No guarantee of uniqueness.
96    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    /// Returns the access control for [`TopicUpdateTransaction`](crate::TopicUpdateTransaction)
102    /// and [`TopicDeleteTransaction`](crate::TopicDeleteTransaction).
103    #[must_use]
104    pub fn get_admin_key(&self) -> Option<&Key> {
105        self.data().admin_key.as_ref()
106    }
107
108    /// Sets the access control for [`TopicUpdateTransaction`](crate::TopicUpdateTransaction)
109    /// and [`TopicDeleteTransaction`](crate::TopicDeleteTransaction).
110    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    /// Returns the access control for [`TopicMessageSubmitTransaction`](crate::TopicMessageSubmitTransaction)
116    #[must_use]
117    pub fn get_submit_key(&self) -> Option<&Key> {
118        self.data().submit_key.as_ref()
119    }
120
121    /// Sets the access control for [`TopicMessageSubmitTransaction`](crate::TopicMessageSubmitTransaction).
122    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    /// Returns the initial lifetime of the topic and the amount of time to attempt to
128    /// extend the topic's lifetime by automatically at the topic's expiration time.
129    #[must_use]
130    pub fn get_auto_renew_period(&self) -> Option<Duration> {
131        self.data().auto_renew_period
132    }
133
134    /// Sets the initial lifetime of the topic and the amount of time to attempt to
135    /// extend the topic's lifetime by automatically at the topic's expiration time.
136    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    /// Returns the account to be used at the topic's expiration time to extend the life of the topic.
142    #[must_use]
143    pub fn get_auto_renew_account_id(&self) -> Option<AccountId> {
144        self.data().auto_renew_account_id
145    }
146
147    /// Sets the account to be used at the topic's expiration time to extend the life of the topic.
148    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    /// Sets the key that can be used to update the fee schedule for the topic.
154    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    /// The keys that can be used to update the fee schedule for the topic.
160    #[must_use]
161    pub fn get_fee_schedule_key(&self) -> Option<&Key> {
162        self.data().fee_schedule_key.as_ref()
163    }
164
165    /// Sets the keys that can be used to update the fee schedule for the topic.
166    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    /// The keys exempt from custom fees for this topic.
172    #[must_use]
173    pub fn get_fee_exempt_keys(&self) -> &Vec<Key> {
174        &self.data().fee_exempt_keys
175    }
176
177    /// Clears the keys exempt from custom fees for this topic.
178    pub fn clear_fee_exempt_keys(&mut self) -> &mut Self {
179        self.data_mut().fee_exempt_keys.clear();
180        self
181    }
182
183    /// Adds a key to the list of keys exempt from custom fees for this topic.
184    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    /// The custom fees to be assessed during a message submission to this topic.
190    #[must_use]
191    pub fn get_custom_fees(&self) -> &Vec<CustomFixedFee> {
192        &self.data().custom_fees
193    }
194
195    /// Sets the custom fees to be assessed during a message submission to this topic.
196    pub fn custom_fees(&mut self, fees: Vec<CustomFixedFee>) -> &mut Self {
197        self.data_mut().custom_fees = fees;
198        self
199    }
200
201    /// Clears the custom fees for this topic.
202    pub fn clear_custom_fees(&mut self) -> &mut Self {
203        self.data_mut().custom_fees.clear();
204        self
205    }
206
207    /// Adds a custom fee to the list of custom fees for this topic.
208    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        // Generate the protobuf data
244        let mut protobuf_data = self.to_protobuf();
245
246        // Manually assign the auto_renew_account with operator_id if none is set
247        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}