Skip to main content

hiero_sdk/schedule/
schedule_create_transaction.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use hiero_sdk_proto::services;
4use hiero_sdk_proto::services::schedule_service_client::ScheduleServiceClient;
5use time::OffsetDateTime;
6use tonic::transport::Channel;
7
8use super::schedulable_transaction_body::SchedulableTransactionBody;
9use crate::protobuf::{
10    FromProtobuf,
11    ToProtobuf,
12};
13use crate::transaction::{
14    AnyTransactionData,
15    ChunkInfo,
16    ToSchedulableTransactionDataProtobuf,
17    ToTransactionDataProtobuf,
18    TransactionData,
19    TransactionExecute,
20};
21use crate::{
22    AccountId,
23    BoxGrpcFuture,
24    Error,
25    Key,
26    Transaction,
27    ValidateChecksums,
28};
29
30/// Create a new schedule entity (or simply, schedule) in the network's action queue.
31///
32/// Upon `SUCCESS`, the receipt contains the `ScheduleId` of the created schedule. A schedule
33/// entity includes a `scheduled_transaction_body` to be executed.
34///
35/// When the schedule has collected enough signing keys to satisfy the schedule's signing
36/// requirements, the schedule can be executed.
37///
38pub type ScheduleCreateTransaction = Transaction<ScheduleCreateTransactionData>;
39
40#[derive(Default, Debug, Clone)]
41pub struct ScheduleCreateTransactionData {
42    scheduled_transaction: Option<SchedulableTransactionBody>,
43
44    schedule_memo: Option<String>,
45
46    admin_key: Option<Key>,
47
48    payer_account_id: Option<AccountId>,
49
50    expiration_time: Option<OffsetDateTime>,
51
52    wait_for_expiry: bool,
53}
54
55impl ScheduleCreateTransaction {
56    // note(sr): not sure what the right way to go about this is?
57    // pub fn get_scheduled_transaction(&self) -> Option<&SchedulableTransactionBody> {
58    //     self.data().scheduled_transaction.as_ref()
59    // }
60
61    /// Sets the scheduled transaction.
62    ///
63    /// # Panics
64    /// panics if the transaction is not schedulable, a transaction can be non-schedulable due to:
65    /// - being a transaction kind that's non-schedulable, IE, `EthereumTransaction`, or
66    /// - being a chunked transaction with multiple chunks.
67    pub fn scheduled_transaction<D>(&mut self, transaction: Transaction<D>) -> &mut Self
68    where
69        D: TransactionExecute,
70    {
71        let body = transaction.into_body();
72
73        // this gets infered right but `foo.into().try_into()` looks really really weird.
74        let data: AnyTransactionData = body.data.into();
75
76        self.data_mut().scheduled_transaction = Some(SchedulableTransactionBody {
77            max_transaction_fee: body.max_transaction_fee,
78            transaction_memo: body.transaction_memo,
79            data: Box::new(data.try_into().unwrap()),
80        });
81
82        self
83    }
84
85    /// Returns the timestamp for when the transaction should be evaluated for execution and then expire.
86    #[must_use]
87    pub fn get_expiration_time(&self) -> Option<OffsetDateTime> {
88        self.data().expiration_time
89    }
90
91    /// Sets the timestamp for when the transaction should be evaluated for execution and then expire.
92    pub fn expiration_time(&mut self, time: OffsetDateTime) -> &mut Self {
93        self.data_mut().expiration_time = Some(time);
94        self
95    }
96
97    /// Returns `true` if the transaction will be evaluated at `expiration_time` instead
98    /// of when all the required signatures are received, `false` otherwise.
99    #[must_use]
100    pub fn get_wait_for_expiry(&self) -> bool {
101        self.data().wait_for_expiry
102    }
103
104    /// Sets if the transaction will be evaluated for execution at `expiration_time` instead
105    /// of when all required signatures are received.
106    pub fn wait_for_expiry(&mut self, wait: bool) -> &mut Self {
107        self.data_mut().wait_for_expiry = wait;
108        self
109    }
110
111    /// Returns the id of the account to be charged the service fee for the scheduled transaction at
112    /// the consensus time it executes (if ever).
113    #[must_use]
114    pub fn get_payer_account_id(&self) -> Option<AccountId> {
115        self.data().payer_account_id
116    }
117
118    /// Sets the id of the account to be charged the service fee for the scheduled transaction at
119    /// the consensus time that it executes (if ever).
120    pub fn payer_account_id(&mut self, id: AccountId) -> &mut Self {
121        self.data_mut().payer_account_id = Some(id);
122        self
123    }
124
125    /// Returns the memo for the schedule entity.
126    #[must_use]
127    pub fn get_schedule_memo(&self) -> Option<&str> {
128        self.data().schedule_memo.as_deref()
129    }
130
131    /// Sets the memo for the schedule entity.
132    pub fn schedule_memo(&mut self, memo: impl Into<String>) -> &mut Self {
133        self.data_mut().schedule_memo = Some(memo.into());
134        self
135    }
136
137    /// Returns the Hiero key which can be used to sign a `ScheduleDelete` and remove the schedule.
138    #[must_use]
139    pub fn get_admin_key(&self) -> Option<&Key> {
140        self.data().admin_key.as_ref()
141    }
142
143    /// Sets the Hiero key which can be used to sign a `ScheduleDelete` and remove the schedule.
144    pub fn admin_key(&mut self, key: impl Into<Key>) -> &mut Self {
145        self.data_mut().admin_key = Some(key.into());
146        self
147    }
148}
149
150impl TransactionData for ScheduleCreateTransactionData {}
151
152impl TransactionExecute for ScheduleCreateTransactionData {
153    fn execute(
154        &self,
155        channel: Channel,
156        request: services::Transaction,
157    ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
158        Box::pin(async { ScheduleServiceClient::new(channel).create_schedule(request).await })
159    }
160}
161
162impl ValidateChecksums for ScheduleCreateTransactionData {
163    fn validate_checksums(&self, ledger_id: &crate::ledger_id::RefLedgerId) -> Result<(), Error> {
164        self.payer_account_id.validate_checksums(ledger_id)
165    }
166}
167
168impl ToTransactionDataProtobuf for ScheduleCreateTransactionData {
169    // not really anything I can do about this
170    #[allow(clippy::too_many_lines)]
171    fn to_transaction_data_protobuf(
172        &self,
173        chunk_info: &ChunkInfo,
174    ) -> services::transaction_body::Data {
175        let _ = chunk_info.assert_single_transaction();
176
177        let body = self.scheduled_transaction.as_ref().map(|scheduled| {
178            let data = scheduled.data.to_schedulable_transaction_data_protobuf();
179
180            services::SchedulableTransactionBody {
181                data: Some(data),
182                memo: scheduled.transaction_memo.clone(),
183                // FIXME: does not use the client to default the max transaction fee
184                transaction_fee: scheduled
185                    .max_transaction_fee
186                    .unwrap_or_else(|| scheduled.data.default_max_transaction_fee())
187                    .to_tinybars() as u64,
188                max_custom_fees: vec![],
189            }
190        });
191
192        let payer_account_id = self.payer_account_id.to_protobuf();
193        let admin_key = self.admin_key.to_protobuf();
194        let expiration_time = self.expiration_time.map(Into::into);
195
196        services::transaction_body::Data::ScheduleCreate(services::ScheduleCreateTransactionBody {
197            scheduled_transaction_body: body,
198            memo: self.schedule_memo.clone().unwrap_or_default(),
199            admin_key,
200            payer_account_id,
201            expiration_time,
202            wait_for_expiry: self.wait_for_expiry,
203        })
204    }
205}
206
207impl From<ScheduleCreateTransactionData> for AnyTransactionData {
208    fn from(transaction: ScheduleCreateTransactionData) -> Self {
209        Self::ScheduleCreate(transaction)
210    }
211}
212
213impl FromProtobuf<services::ScheduleCreateTransactionBody> for ScheduleCreateTransactionData {
214    fn from_protobuf(pb: services::ScheduleCreateTransactionBody) -> crate::Result<Self> {
215        Ok(Self {
216            scheduled_transaction: Option::from_protobuf(pb.scheduled_transaction_body)?,
217            schedule_memo: Some(pb.memo),
218            admin_key: Option::from_protobuf(pb.admin_key)?,
219            payer_account_id: Option::from_protobuf(pb.payer_account_id)?,
220            expiration_time: pb.expiration_time.map(Into::into),
221            wait_for_expiry: pb.wait_for_expiry,
222        })
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use expect_test::expect;
229    use hiero_sdk_proto::services;
230    use time::OffsetDateTime;
231
232    use super::ScheduleCreateTransactionData;
233    use crate::protobuf::{
234        FromProtobuf,
235        ToProtobuf,
236    };
237    use crate::transaction::test_helpers::{
238        check_body,
239        transaction_body,
240        unused_private_key,
241        VALID_START,
242    };
243    use crate::transaction::ToSchedulableTransactionDataProtobuf;
244    use crate::{
245        AccountId,
246        AnyTransaction,
247        Hbar,
248        PublicKey,
249        ScheduleCreateTransaction,
250        TransferTransaction,
251    };
252
253    fn scheduled_transaction() -> TransferTransaction {
254        let mut tx = TransferTransaction::new();
255        tx.hbar_transfer("0.0.555".parse().unwrap(), -Hbar::new(10))
256            .hbar_transfer("0.0.666".parse().unwrap(), Hbar::new(10));
257        tx
258    }
259
260    fn admin_key() -> PublicKey {
261        unused_private_key().public_key()
262    }
263
264    const PAYER_ACCOUNT_ID: AccountId = AccountId::new(0, 0, 222);
265    const SCHEDULE_MEMO: &str = "hi";
266    const EXPIRATION_TIME: OffsetDateTime = VALID_START;
267
268    fn make_transaction() -> ScheduleCreateTransaction {
269        let mut tx = ScheduleCreateTransaction::new_for_tests();
270
271        tx.scheduled_transaction(scheduled_transaction())
272            .admin_key(admin_key())
273            .payer_account_id(PAYER_ACCOUNT_ID)
274            .schedule_memo(SCHEDULE_MEMO)
275            .expiration_time(EXPIRATION_TIME)
276            .freeze()
277            .unwrap();
278
279        tx
280    }
281
282    #[test]
283    fn serialize() {
284        let tx = make_transaction();
285
286        let tx = transaction_body(tx);
287
288        let tx = check_body(tx);
289
290        expect![[r#"
291            ScheduleCreate(
292                ScheduleCreateTransactionBody {
293                    scheduled_transaction_body: Some(
294                        SchedulableTransactionBody {
295                            transaction_fee: 200000000,
296                            memo: "",
297                            max_custom_fees: [],
298                            data: Some(
299                                CryptoTransfer(
300                                    CryptoTransferTransactionBody {
301                                        transfers: Some(
302                                            TransferList {
303                                                account_amounts: [
304                                                    AccountAmount {
305                                                        account_id: Some(
306                                                            AccountId {
307                                                                shard_num: 0,
308                                                                realm_num: 0,
309                                                                account: Some(
310                                                                    AccountNum(
311                                                                        555,
312                                                                    ),
313                                                                ),
314                                                            },
315                                                        ),
316                                                        amount: -1000000000,
317                                                        is_approval: false,
318                                                        hook_call: None,
319                                                    },
320                                                    AccountAmount {
321                                                        account_id: Some(
322                                                            AccountId {
323                                                                shard_num: 0,
324                                                                realm_num: 0,
325                                                                account: Some(
326                                                                    AccountNum(
327                                                                        666,
328                                                                    ),
329                                                                ),
330                                                            },
331                                                        ),
332                                                        amount: 1000000000,
333                                                        is_approval: false,
334                                                        hook_call: None,
335                                                    },
336                                                ],
337                                            },
338                                        ),
339                                        token_transfers: [],
340                                    },
341                                ),
342                            ),
343                        },
344                    ),
345                    memo: "hi",
346                    admin_key: Some(
347                        Key {
348                            key: Some(
349                                Ed25519(
350                                    [
351                                        224,
352                                        200,
353                                        236,
354                                        39,
355                                        88,
356                                        165,
357                                        135,
358                                        159,
359                                        250,
360                                        194,
361                                        38,
362                                        161,
363                                        60,
364                                        12,
365                                        81,
366                                        107,
367                                        121,
368                                        158,
369                                        114,
370                                        227,
371                                        81,
372                                        65,
373                                        160,
374                                        221,
375                                        130,
376                                        143,
377                                        148,
378                                        211,
379                                        121,
380                                        136,
381                                        164,
382                                        183,
383                                    ],
384                                ),
385                            ),
386                        },
387                    ),
388                    payer_account_id: Some(
389                        AccountId {
390                            shard_num: 0,
391                            realm_num: 0,
392                            account: Some(
393                                AccountNum(
394                                    222,
395                                ),
396                            ),
397                        },
398                    ),
399                    expiration_time: Some(
400                        Timestamp {
401                            seconds: 1554158542,
402                            nanos: 0,
403                        },
404                    ),
405                    wait_for_expiry: false,
406                },
407            )
408        "#]]
409        .assert_debug_eq(&tx)
410    }
411
412    #[test]
413    fn to_from_bytes() {
414        let tx = make_transaction();
415
416        let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
417
418        let tx = transaction_body(tx);
419
420        let tx2 = transaction_body(tx2);
421
422        assert_eq!(tx, tx2);
423    }
424
425    #[test]
426    fn from_proto_body() {
427        let tx = services::ScheduleCreateTransactionBody {
428            scheduled_transaction_body: Some(services::SchedulableTransactionBody {
429                transaction_fee: Hbar::new(2).to_tinybars() as _,
430                memo: String::new(),
431                data: Some(
432                    scheduled_transaction().data().to_schedulable_transaction_data_protobuf(),
433                ),
434                max_custom_fees: vec![],
435            }),
436            memo: SCHEDULE_MEMO.to_owned(),
437            admin_key: Some(admin_key().to_protobuf()),
438            payer_account_id: Some(PAYER_ACCOUNT_ID.to_protobuf()),
439            expiration_time: Some(EXPIRATION_TIME.to_protobuf()),
440            wait_for_expiry: false,
441        };
442
443        let tx = ScheduleCreateTransactionData::from_protobuf(tx).unwrap();
444
445        expect![[r#"
446            SchedulableTransactionBody {
447                data: Transfer(
448                    TransferTransactionData {
449                        transfers: [
450                            Transfer {
451                                account_id: "0.0.555",
452                                amount: -1000000000,
453                                is_approval: false,
454                                hook_call: None,
455                            },
456                            Transfer {
457                                account_id: "0.0.666",
458                                amount: 1000000000,
459                                is_approval: false,
460                                hook_call: None,
461                            },
462                        ],
463                        token_transfers: [],
464                    },
465                ),
466                max_transaction_fee: Some(
467                    "2 ℏ",
468                ),
469                transaction_memo: "",
470            }
471        "#]]
472        .assert_debug_eq(&tx.scheduled_transaction.unwrap());
473
474        assert_eq!(tx.schedule_memo.as_deref(), Some(SCHEDULE_MEMO));
475        assert_eq!(tx.admin_key, Some(admin_key().into()));
476        assert_eq!(tx.payer_account_id, Some(PAYER_ACCOUNT_ID));
477        assert_eq!(tx.expiration_time, Some(EXPIRATION_TIME));
478        assert_eq!(tx.wait_for_expiry, false);
479    }
480
481    mod get_set {
482        use super::*;
483        #[test]
484        fn admin_key() {
485            let mut tx = ScheduleCreateTransaction::new();
486            tx.admin_key(super::admin_key());
487
488            assert_eq!(tx.get_admin_key(), Some(&super::admin_key().into()));
489        }
490
491        #[test]
492        #[should_panic]
493        fn admin_key_frozen_panics() {
494            make_transaction().admin_key(super::admin_key());
495        }
496
497        #[test]
498        fn payer_account_id() {
499            let mut tx = ScheduleCreateTransaction::new();
500            tx.payer_account_id(PAYER_ACCOUNT_ID);
501
502            assert_eq!(tx.get_payer_account_id(), Some(PAYER_ACCOUNT_ID));
503        }
504
505        #[test]
506        #[should_panic]
507        fn payer_account_id_frozen_panics() {
508            make_transaction().payer_account_id(PAYER_ACCOUNT_ID);
509        }
510
511        #[test]
512        fn expiration_time() {
513            let mut tx = ScheduleCreateTransaction::new();
514            tx.expiration_time(EXPIRATION_TIME);
515
516            assert_eq!(tx.get_expiration_time(), Some(EXPIRATION_TIME));
517        }
518
519        #[test]
520        #[should_panic]
521        fn expiration_time_frozen_panics() {
522            make_transaction().expiration_time(EXPIRATION_TIME);
523        }
524
525        #[test]
526        fn wait_for_expiry() {
527            let mut tx = ScheduleCreateTransaction::new();
528            tx.wait_for_expiry(true);
529
530            assert_eq!(tx.get_wait_for_expiry(), true);
531        }
532
533        #[test]
534        #[should_panic]
535        fn wait_for_expiry_frozen_panics() {
536            make_transaction().wait_for_expiry(true);
537        }
538    }
539}