diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index bf6c92fa..45ce5960 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -115,7 +115,7 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Start the local node - run: npx @hashgraph/hedera-local start -d --network local --balance=100000 + run: npx @hashgraph/hedera-local start -d --network local --network-tag=0.57.0 - name: "Create env file" run: | diff --git a/examples/long_term_scheduled_transaction.rs b/examples/long_term_scheduled_transaction.rs new file mode 100644 index 00000000..8cbd47d0 --- /dev/null +++ b/examples/long_term_scheduled_transaction.rs @@ -0,0 +1,253 @@ +/* + * ‌ + * Hedera Rust SDK + * ​ + * Copyright (C) 2022 - 2023 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +use clap::Parser; +use hedera::{ + AccountCreateTransaction, AccountId, AccountInfoQuery, AccountUpdateTransaction, Client, Hbar, Key, KeyList, PrivateKey, ScheduleInfoQuery, ScheduleSignTransaction, TransferTransaction +}; +use time::{Duration, OffsetDateTime}; +use tokio::time::sleep; + +#[derive(Parser, Debug)] +struct Args { + #[clap(long, env)] + operator_account_id: AccountId, + + #[clap(long, env)] + operator_key: PrivateKey, + + #[clap(long, env, default_value = "testnet")] + hedera_network: String, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let _ = dotenvy::dotenv(); + + let args = Args::parse(); + + /* + * Step 0: Create and configure the client + */ + let client = Client::for_name(&args.hedera_network)?; + client.set_operator(args.operator_account_id, args.operator_key); + + /* + * Step 1: Create key pairs + */ + let key1 = PrivateKey::generate_ed25519(); + let key2 = PrivateKey::generate_ed25519(); + + println!("Creating Key List... (w/ threshold, 2 of 2 keys generated above is required to modify the account)"); + + let threshold_key = KeyList { + keys: vec![key1.public_key().into(), key2.public_key().into()], + threshold: Some(2), + }; + + println!("Created key list: {threshold_key:?}"); + + /* + * Step 2: Create the account + */ + println!("Creating account with threshold key..."); + let alice_id = AccountCreateTransaction::new() + .key(Key::KeyList(threshold_key)) + .initial_balance(Hbar::new(2)) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .account_id + .unwrap(); + + println!("Created account with id: {alice_id}"); + + /* + * Step 3: + * Schedule a transfer transaction of 1 hbar from the newly created account to the operator account. + * The transaction will be scheduled with expirationTime = 24 hours from now and waitForExpiry = false. + */ + println!("Creating new scheduled transaction with 1 day expiry..."); + let mut transfer = TransferTransaction::new(); + transfer + .hbar_transfer(alice_id, Hbar::new(-1)) + .hbar_transfer(args.operator_account_id, Hbar::new(1)); + + let schedule_id = transfer + .schedule() + .wait_for_expiry(false) + .expiration_time(OffsetDateTime::now_utc() + Duration::seconds(86400)) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .schedule_id + .unwrap(); + + /* + * Step 4: Sign the transaction with one key and verify the transaction is not executed + */ + println!("Signing transaction with key 1..."); + _ = ScheduleSignTransaction::new() + .schedule_id(schedule_id) + .freeze_with(&client)? + .sign(key1.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let info = ScheduleInfoQuery::new() + .schedule_id(schedule_id) + .execute(&client) + .await?; + + println!( + "Scheduled transaction is not executed yet. Executed at: {:?}", + info.executed_at + ); + + /* + * Step 5: Sign the transaction with the second key and verify the transaction is executed + */ + + let account_balance = AccountInfoQuery::new() + .account_id(alice_id) + .execute(&client) + .await? + .balance; + + println!("Alice's account balance before scheduled transaction: {account_balance}"); + + println!("Signing transaction with key 2..."); + _ = ScheduleSignTransaction::new() + .schedule_id(schedule_id) + .freeze_with(&client)? + .sign(key2.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let account_balance = AccountInfoQuery::new() + .account_id(alice_id) + .execute(&client) + .await? + .balance; + + println!("Alice's account balance after scheduled transaction: {account_balance}"); + + let info = ScheduleInfoQuery::new() + .schedule_id(schedule_id) + .execute(&client) + .await?; + + println!("Scheduled transaction executed at: {:?}", info.executed_at); + + /* + * Step 6: + * Schedule another transfer transaction of 1 Hbar from the account to the operator account + * with an expirationTime of 10 seconds in the future and waitForExpiry=true. + */ + println!("Creating new scheduled transaction with 10 second expiry..."); + let mut transfer = TransferTransaction::new(); + transfer + .hbar_transfer(alice_id, Hbar::new(-1)) + .hbar_transfer(args.operator_account_id, Hbar::new(1)); + + let schedule_id = transfer + .schedule() + .wait_for_expiry(true) + .expiration_time(OffsetDateTime::now_utc() + Duration::seconds(10)) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .schedule_id + .unwrap(); + + /* + * Step 7: + * Sign the transaction with one key and verify the transaction is not executed + */ + println!("Signing scheduled transaction with key 1..."); + _ = ScheduleSignTransaction::new() + .schedule_id(schedule_id) + .freeze_with(&client)? + .sign(key1.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let info = ScheduleInfoQuery::new() + .schedule_id(schedule_id) + .execute(&client) + .await?; + + println!( + "Scheduled transaction is not executed yet. Executed at: {:?}", + info.executed_at + ); + + /* + * Step 8: + * Update the account's key to be only the one key + * that has already signed the scheduled transfer. + */ + println!("Updating account key to only key 1..."); + _ = AccountUpdateTransaction::new() + .account_id(alice_id) + .key(key1.public_key()) + .freeze_with(&client)? + .sign(key1) + .sign(key2) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + /* + * Step 9: + * Verify that the transfer successfully executes roughly at the time of its expiration. + */ + let account_balance = AccountInfoQuery::new() + .account_id(alice_id) + .execute(&client) + .await? + .balance; + + println!("Alice's account balance before scheduled transfer: {account_balance}"); + + sleep(std::time::Duration::from_millis(10_000)).await; + + let account_balance = AccountInfoQuery::new() + .account_id(alice_id) + .execute(&client) + .await? + .balance; + + println!("Alice's account balance after scheduled transfer: {account_balance}"); + + println!("Successfully executed scheduled transfer"); + + Ok(()) +} diff --git a/protobufs/build.rs b/protobufs/build.rs index 8d0fa0e8..bdabcc7d 100644 --- a/protobufs/build.rs +++ b/protobufs/build.rs @@ -31,6 +31,7 @@ use regex::RegexBuilder; const DERIVE_EQ_HASH: &str = "#[derive(Eq, Hash)]"; const DERIVE_EQ_HASH_COPY: &str = "#[derive(Copy, Eq, Hash)]"; const SERVICES_FOLDER: &str = "./protobufs/services"; +const EVENT_FOLDER: &str = "./protobufs/platform/event"; fn main() -> anyhow::Result<()> { // services is the "base" module for the hedera protobufs @@ -64,9 +65,37 @@ fn main() -> anyhow::Result<()> { )?; fs::rename(out_path.join("services"), &services_tmp_path)?; + let event_path = Path::new(EVENT_FOLDER); + println!("cargo:rerun-if-changed={}", EVENT_FOLDER); + + if !event_path.is_dir() { + anyhow::bail!( + "Folder {EVENT_FOLDER} does not exist; do you need to `git submodule update --init`?" + ); + } + + let event_tmp_path = out_path.join("event"); + + // // Ensure we start fresh + let _ = fs::remove_dir_all(&event_tmp_path); + + create_dir_all(&event_tmp_path)?; + + // Copy the event folder + fs_extra::copy_items( + &[event_path], + &services_tmp_path, + &fs_extra::dir::CopyOptions::new().overwrite(true).copy_inside(false), + )?; + fs::rename(out_path.join("event"), &event_tmp_path)?; + let _ = fs::remove_dir_all(&event_tmp_path); + let services: Vec<_> = read_dir(&services_tmp_path)? + .chain(read_dir(&services_tmp_path.join("auxiliary").join("tss"))?) + .chain(read_dir(&services_tmp_path.join("event"))?) .filter_map(|entry| { let entry = entry.ok()?; + entry.file_type().ok()?.is_file().then(|| entry.path()) }) .collect(); @@ -82,6 +111,12 @@ fn main() -> anyhow::Result<()> { // remove com.hedera.hapi.node.addressbook. prefix let contents = contents.replace("com.hedera.hapi.node.addressbook.", ""); + // remove com.hedera.hapi.services.auxiliary.tss. prefix + let contents = contents.replace("com.hedera.hapi.services.auxiliary.tss.", ""); + + // remove com.hedera.hapi.platform.event. prefix + let contents = contents.replace("com.hedera.hapi.platform.event.", ""); + fs::write(service, &*contents)?; } @@ -141,8 +176,15 @@ fn main() -> anyhow::Result<()> { "]"#, ); + // Services fails with message: + // --- stderr + // Error: protoc failed: event/state_signature_transaction.proto: File not found. + // transaction_body.proto:111:1: Import "event/state_signature_transaction.proto" was not found or had errors. + // cfg.compile(&services, &[services_tmp_path])?; + // panic!("Services succeeded"); + // NOTE: prost generates rust doc comments and fails to remove the leading * line remove_useless_comments(&Path::new(&env::var("OUT_DIR")?).join("proto.rs"))?; @@ -256,6 +298,10 @@ fn main() -> anyhow::Result<()> { .services_same("TokenUpdateTransactionBody") .services_same("TokenUpdateNftsTransactionBody") .services_same("TokenWipeAccountTransactionBody") + .services_same("TssMessageTransactionBody") + .services_same("TssVoteTransactionBody") + .services_same("TssShareSignatureTransactionBody") + .services_same("TssEncryptionKeyTransactionBody") .services_same("Transaction") .services_same("TransactionBody") .services_same("UncheckedSubmitBody") diff --git a/protobufs/protobufs b/protobufs/protobufs index d5e69887..c4f40a22 160000 --- a/protobufs/protobufs +++ b/protobufs/protobufs @@ -1 +1 @@ -Subproject commit d5e69887fc65796c6afdcc140b5140d4133bb5f4 +Subproject commit c4f40a22aa3ffd66cbf1d2719d3c7d959b70b624 diff --git a/src/fee_schedules.rs b/src/fee_schedules.rs index f5822e80..2f3abbc3 100644 --- a/src/fee_schedules.rs +++ b/src/fee_schedules.rs @@ -430,6 +430,12 @@ pub enum RequestType { /// Cancel airdrop tokens. TokenCancelAirdrop, + + /// Submit a vote as part of the Threshold Signature Scheme (TSS) processing. + TssMessage, + + /// Submit a vote as part of the Threshold Signature Scheme (TSS) processing. + TssVote, } impl FromProtobuf for RequestType { @@ -518,6 +524,8 @@ impl FromProtobuf for RequestType { HederaFunctionality::TokenAirdrop => Self::TokenAirdrop, HederaFunctionality::TokenClaimAirdrop => Self::TokenClaimAirdrop, HederaFunctionality::TokenCancelAirdrop => Self::TokenCancelAirdrop, + HederaFunctionality::TssMessage => Self::TssMessage, + HederaFunctionality::TssVote => Self::TssVote, }; Ok(value) @@ -612,6 +620,8 @@ impl ToProtobuf for RequestType { Self::TokenAirdrop => HederaFunctionality::TokenAirdrop, Self::TokenClaimAirdrop => HederaFunctionality::TokenClaimAirdrop, Self::TokenCancelAirdrop => HederaFunctionality::TokenCancelAirdrop, + Self::TssMessage => HederaFunctionality::TssMessage, + Self::TssVote => HederaFunctionality::TssVote, } } } diff --git a/src/transaction/any.rs b/src/transaction/any.rs index 36882ac1..a75e4413 100644 --- a/src/transaction/any.rs +++ b/src/transaction/any.rs @@ -664,6 +664,12 @@ impl FromProtobuf for AnyTransactionData { Data::TokenAirdrop(pb) => data::TokenAirdrop::from_protobuf(pb)?.into(), Data::TokenClaimAirdrop(pb) => data::TokenClaimAirdrop::from_protobuf(pb)?.into(), Data::TokenCancelAirdrop(pb) => data::TokenCancelAirdrop::from_protobuf(pb)?.into(), + Data::TssMessage(_) => { + return Err(Error::from_protobuf("unsupported transaction `TssMessageTransaction`")) + } + Data::TssVote(_) => { + return Err(Error::from_protobuf("unsupported transaction `TssVoteTransaction`")) + } Data::CryptoAddLiveHash(_) => { return Err(Error::from_protobuf( "unsupported transaction `AddLiveHashTransaction`", @@ -1006,6 +1012,12 @@ impl FromProtobuf> for ServicesTransaction Data::TokenAirdrop(it) => Self::TokenAirdrop(make_vec(it, len)), Data::TokenClaimAirdrop(it) => Self::TokenClaimAirdrop(make_vec(it, len)), Data::TokenCancelAirdrop(it) => Self::TokenCancelAirdrop(make_vec(it, len)), + Data::TssMessage(_) => { + return Err(Error::from_protobuf("unsupported transaction `TssMessageTransaction`")) + } + Data::TssVote(_) => { + return Err(Error::from_protobuf("unsupported transaction `TssVoteTransaction`")) + } Data::CryptoAddLiveHash(_) => { return Err(Error::from_protobuf( "unsupported transaction `AddLiveHashTransaction`", diff --git a/tests/e2e/schedule/create.rs b/tests/e2e/schedule/create.rs index ec20622c..31e879c1 100644 --- a/tests/e2e/schedule/create.rs +++ b/tests/e2e/schedule/create.rs @@ -1,11 +1,22 @@ +use std::hash::{ + DefaultHasher, + Hash, + Hasher, +}; +use std::thread::sleep; + use assert_matches::assert_matches; use hedera::{ + AccountBalanceQuery, AccountCreateTransaction, AccountDeleteTransaction, + AccountUpdateTransaction, Hbar, + Key, KeyList, PrivateKey, ScheduleCreateTransaction, + ScheduleId, ScheduleInfoQuery, ScheduleSignTransaction, Status, @@ -13,7 +24,10 @@ use hedera::{ TopicMessageSubmitTransaction, TransferTransaction, }; -use time::OffsetDateTime; +use time::{ + Duration, + OffsetDateTime, +}; use crate::account::Account; use crate::common::{ @@ -21,6 +35,9 @@ use crate::common::{ TestEnvironment, }; +// Seconds in a day +const TEST_SECONDS: i64 = 86400; + #[tokio::test] #[ignore = "not implemented in Hedera yet"] async fn create_account() -> anyhow::Result<()> { @@ -107,7 +124,7 @@ async fn transfer() -> anyhow::Result<()> { let key2 = PrivateKey::generate_ed25519(); let key3 = PrivateKey::generate_ed25519(); - let key_list = KeyList::from([key1.public_key(), key2.public_key(), key3.public_key()]); + let key_list = KeyList::from([key1.public_key(), key2.public_key().into(), key3.public_key()]); // Create the account with the `KeyList` let mut transaction = AccountCreateTransaction::new(); @@ -281,3 +298,468 @@ async fn topic_message() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn can_sign_schedule() -> anyhow::Result<()> { + let Some(TestEnvironment { config, client }) = setup_nonfree() else { + return Ok(()); + }; + + let Some(op) = &config.operator else { + log::debug!("skipping test due to missing operator"); + return Ok(()); + }; + + let account = Account::create(Hbar::new(10), &client).await?; + + // Create transaction to schedule + let mut transfer = TransferTransaction::new(); + + transfer.hbar_transfer(op.account_id, Hbar::new(1)).hbar_transfer(account.id, Hbar::new(-1)); + + // Schedule transaction + let schedule_id = transfer + .schedule() + .schedule_memo("HIP-423 E2E test") + .expiration_time(OffsetDateTime::now_utc() + Duration::seconds(TEST_SECONDS)) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .schedule_id + .unwrap(); + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction hasn't executed yet + assert_eq!(info.executed_at, None); + + // Schedule sign + _ = ScheduleSignTransaction::new() + .schedule_id(schedule_id) + .freeze_with(&client)? + .sign(account.key) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction has executed + assert!(info.executed_at.is_some()); + + assert_eq!(schedule_id.checksum, None); + assert_eq!(schedule_id, ScheduleId::from_bytes(&schedule_id.to_bytes()[..])?); + + let mut hasher = DefaultHasher::new(); + + schedule_id.hash(&mut hasher); + assert_ne!(hasher.finish(), 0); + + Ok(()) +} + +#[tokio::test] +async fn schedule_ahead_one_year_fail() -> anyhow::Result<()> { + let Some(TestEnvironment { config, client }) = setup_nonfree() else { + return Ok(()); + }; + + let Some(op) = &config.operator else { + log::debug!("skipping test due to missing operator"); + return Ok(()); + }; + + let account = Account::create(Hbar::new(10), &client).await?; + + // Create transaction to schedule + let mut transfer = TransferTransaction::new(); + transfer.hbar_transfer(op.account_id, Hbar::new(1)).hbar_transfer(account.id, Hbar::new(-1)); + + let res = transfer + .schedule() + .schedule_memo("HIP-423 E2E test") + .expiration_time(OffsetDateTime::now_utc() + Duration::days(365)) + .execute(&client) + .await? + .get_receipt(&client) + .await; + + assert_matches!( + res, + Err(hedera::Error::ReceiptStatus { + status: Status::ScheduleExpirationTimeTooFarInFuture, + .. + }) + ); + + Ok(()) +} + +#[tokio::test] +async fn schedule_in_the_past_fail() -> anyhow::Result<()> { + let Some(TestEnvironment { config, client }) = setup_nonfree() else { + return Ok(()); + }; + + let Some(op) = &config.operator else { + log::debug!("skipping test due to missing operator"); + return Ok(()); + }; + + let account = Account::create(Hbar::new(10), &client).await?; + + // Create transaction to schedule + let mut transfer = TransferTransaction::new(); + transfer.hbar_transfer(op.account_id, Hbar::new(1)).hbar_transfer(account.id, Hbar::new(-1)); + + let res = transfer + .schedule() + .schedule_memo("HIP-423 E2E test") + .expiration_time(OffsetDateTime::now_utc() - Duration::seconds(10)) + .execute(&client) + .await? + .get_receipt(&client) + .await; + + assert_matches!( + res, + Err(hedera::Error::ReceiptStatus { + status: Status::ScheduleExpirationTimeMustBeHigherThanConsensusTime, + .. + }) + ); + + Ok(()) +} + +#[tokio::test] +async fn sign_schedule_and_wait_for_expiry() -> anyhow::Result<()> { + let Some(TestEnvironment { config, client }) = setup_nonfree() else { + return Ok(()); + }; + + let Some(op) = &config.operator else { + log::debug!("skipping test due to missing operator"); + return Ok(()); + }; + + let account = Account::create(Hbar::new(10), &client).await?; + + // Create transaction to schedule + let mut transfer = TransferTransaction::new(); + + transfer.hbar_transfer(op.account_id, Hbar::new(1)).hbar_transfer(account.id, Hbar::new(-1)); + + // Schedule transaction + let schedule_id = transfer + .schedule() + .schedule_memo("HIP-423 E2E test") + .wait_for_expiry(true) + .expiration_time(OffsetDateTime::now_utc() + Duration::seconds(TEST_SECONDS)) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .schedule_id + .unwrap(); + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction hasn't executed yet + assert_eq!(info.executed_at, None); + + // Schedule sign + _ = ScheduleSignTransaction::new() + .schedule_id(schedule_id) + .freeze_with(&client)? + .sign(account.key) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction hasn't executed yet + assert!(info.executed_at.is_none()); + + assert_eq!(schedule_id.checksum, None); + assert_eq!(schedule_id, ScheduleId::from_bytes(&schedule_id.to_bytes()[..])?); + + let mut hasher = DefaultHasher::new(); + schedule_id.hash(&mut hasher); + assert_ne!(hasher.finish(), 0); + + Ok(()) +} + +#[tokio::test] +async fn sign_with_multi_sig_and_update_signing_requirements() -> anyhow::Result<()> { + let Some(TestEnvironment { config, client }) = setup_nonfree() else { + return Ok(()); + }; + + let Some(op) = &config.operator else { + log::debug!("skipping test due to missing operator"); + return Ok(()); + }; + + let key1 = PrivateKey::generate_ed25519(); + let key2 = PrivateKey::generate_ed25519(); + let key3 = PrivateKey::generate_ed25519(); + let key4 = PrivateKey::generate_ed25519(); + + let key_list = KeyList { + keys: vec![key1.public_key().into(), key2.public_key().into(), key3.public_key().into()], + threshold: Some(2), + }; + + let account_id = AccountCreateTransaction::new() + .key(key_list) + .initial_balance(Hbar::new(10)) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .account_id + .unwrap(); + + // Create transaction to schedule + let mut transfer = TransferTransaction::new(); + + transfer.hbar_transfer(op.account_id, Hbar::new(1)).hbar_transfer(account_id, Hbar::new(-1)); + + // Schedule transaction + let schedule_id = transfer + .schedule() + .schedule_memo("HIP-423 E2E test") + .expiration_time(OffsetDateTime::now_utc() + Duration::seconds(86400)) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .schedule_id + .unwrap(); + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction hasn't executed yet + assert_eq!(info.executed_at, None); + + // Schedule sign + _ = ScheduleSignTransaction::new() + .schedule_id(schedule_id) + .freeze_with(&client)? + .sign(key1.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction hasn't executed yet + assert_eq!(info.executed_at, None); + + // Update the signing requirements + _ = AccountUpdateTransaction::new() + .account_id(account_id) + .key(Key::Single(key4.public_key())) + .freeze_with(&client)? + .sign(key1) + .sign(key2) + .sign(key4.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction hasn't executed yet + assert_eq!(info.executed_at, None); + + // Schedule sign + _ = ScheduleSignTransaction::new() + .schedule_id(schedule_id) + .freeze_with(&client)? + .sign(key4) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the schedule is executed + assert!(info.executed_at.is_some()); + + Ok(()) +} + +#[tokio::test] +async fn sign_with_multi_sig() -> anyhow::Result<()> { + let Some(TestEnvironment { config, client }) = setup_nonfree() else { + return Ok(()); + }; + + let Some(op) = &config.operator else { + log::debug!("skipping test due to missing operator"); + return Ok(()); + }; + + let key1 = PrivateKey::generate_ed25519(); + let key2 = PrivateKey::generate_ed25519(); + let key3 = PrivateKey::generate_ed25519(); + + let key_list = KeyList { + keys: vec![key1.public_key().into(), key2.public_key().into(), key3.public_key().into()], + threshold: Some(2), + }; + + let account_id = AccountCreateTransaction::new() + .key(key_list) + .initial_balance(Hbar::new(10)) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .account_id + .unwrap(); + + // Create transaction to schedule + let mut transfer = TransferTransaction::new(); + + transfer.hbar_transfer(op.account_id, Hbar::new(1)).hbar_transfer(account_id, Hbar::new(-1)); + + // Schedule transaction + let schedule_id = transfer + .schedule() + .schedule_memo("HIP-423 E2E test") + .expiration_time(OffsetDateTime::now_utc() + Duration::seconds(86400)) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .schedule_id + .unwrap(); + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction hasn't executed yet + assert_eq!(info.executed_at, None); + + // Schedule sign + _ = ScheduleSignTransaction::new() + .schedule_id(schedule_id) + .freeze_with(&client)? + .sign(key1.clone()) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction has still not executed + assert_eq!(info.executed_at, None); + + // Update the signing requirements + _ = AccountUpdateTransaction::new() + .account_id(account_id) + .key(key1.public_key()) + .freeze_with(&client)? + .sign(key1) + .sign(key2.clone()) + .execute(&client) + .await? + .get_receipt(&client); + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction has still not executed + assert_eq!(info.executed_at, None); + + // Schedule sign with one key + _ = ScheduleSignTransaction::new() + .schedule_id(schedule_id) + .freeze_with(&client)? + .sign(key2) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the schedule is executed + assert!(info.executed_at.is_some()); + + Ok(()) +} + +#[tokio::test] +async fn execute_with_short_exp_time() -> anyhow::Result<()> { + let Some(TestEnvironment { config, client }) = setup_nonfree() else { + return Ok(()); + }; + + let Some(op) = &config.operator else { + log::debug!("skipping test due to missing operator"); + return Ok(()); + }; + + let account = Account::create(Hbar::new(10), &client).await?; + + // Create transaction to schedule + let mut transfer = TransferTransaction::new(); + + transfer.hbar_transfer(op.account_id, Hbar::new(1)).hbar_transfer(account.id, Hbar::new(-1)); + + // Schedule transaction + let schedule_id = transfer + .schedule() + .schedule_memo("HIP-423 E2E test") + .wait_for_expiry(true) + .expiration_time(OffsetDateTime::now_utc() + Duration::seconds(10)) + .execute(&client) + .await? + .get_receipt(&client) + .await? + .schedule_id + .unwrap(); + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction hasn't executed yet + assert_eq!(info.executed_at, None); + + // Sign + _ = ScheduleSignTransaction::new() + .schedule_id(schedule_id) + .freeze_with(&client)? + .sign(account.key) + .execute(&client) + .await? + .get_receipt(&client) + .await?; + + let info = ScheduleInfoQuery::new().schedule_id(schedule_id).execute(&client).await?; + + // Verify the transaction has still not executed + assert_eq!(info.executed_at, None); + + let initial_balance = + AccountBalanceQuery::new().account_id(account.id).execute(&client).await?; + + sleep(std::time::Duration::from_millis(10_000)); + + let new_balance = AccountBalanceQuery::new().account_id(account.id).execute(&client).await?; + + // Verify the schedule is executed after 10 seconds + assert_eq!(initial_balance.hbars, new_balance.hbars + Hbar::new(1)); + + Ok(()) +} diff --git a/tests/e2e/schedule/info.rs b/tests/e2e/schedule/info.rs index f8552098..a1c0291f 100644 --- a/tests/e2e/schedule/info.rs +++ b/tests/e2e/schedule/info.rs @@ -84,7 +84,10 @@ async fn query() -> anyhow::Result<()> { assert_eq!(info.payer_account_id, Some(op.account_id)); let _ = info.scheduled_transaction()?; - assert_eq!(info.signatories, KeyList::new()); + assert_eq!( + info.signatories, + KeyList { keys: vec![op.private_key.public_key().into()].into(), threshold: None } + ); assert!(!info.wait_for_expiry); account.delete(&client).await?;