Skip to content

Commit

Permalink
HIP-423: Long Term Scheduled Transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
RickyLB authored Dec 27, 2024
1 parent 435f9fc commit 606bf77
Show file tree
Hide file tree
Showing 8 changed files with 811 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
253 changes: 253 additions & 0 deletions examples/long_term_scheduled_transaction.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
46 changes: 46 additions & 0 deletions protobufs/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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)?;
}

Expand Down Expand Up @@ -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"))?;

Expand Down Expand Up @@ -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")
Expand Down
10 changes: 10 additions & 0 deletions src/fee_schedules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<services::HederaFunctionality> for RequestType {
Expand Down Expand Up @@ -518,6 +524,8 @@ impl FromProtobuf<services::HederaFunctionality> for RequestType {
HederaFunctionality::TokenAirdrop => Self::TokenAirdrop,
HederaFunctionality::TokenClaimAirdrop => Self::TokenClaimAirdrop,
HederaFunctionality::TokenCancelAirdrop => Self::TokenCancelAirdrop,
HederaFunctionality::TssMessage => Self::TssMessage,
HederaFunctionality::TssVote => Self::TssVote,
};

Ok(value)
Expand Down Expand Up @@ -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,
}
}
}
Expand Down
Loading

0 comments on commit 606bf77

Please sign in to comment.