Skip to content

Commit 606bf77

Browse files
authored
HIP-423: Long Term Scheduled Transaction
1 parent 435f9fc commit 606bf77

File tree

8 files changed

+811
-5
lines changed

8 files changed

+811
-5
lines changed

.github/workflows/rust-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ jobs:
115115
repo-token: ${{ secrets.GITHUB_TOKEN }}
116116

117117
- name: Start the local node
118-
run: npx @hashgraph/hedera-local start -d --network local --balance=100000
118+
run: npx @hashgraph/hedera-local start -d --network local --network-tag=0.57.0
119119

120120
- name: "Create env file"
121121
run: |
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
/*
2+
* ‌
3+
* Hedera Rust SDK
4+
* ​
5+
* Copyright (C) 2022 - 2023 Hedera Hashgraph, LLC
6+
* ​
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* ‍
19+
*/
20+
21+
use clap::Parser;
22+
use hedera::{
23+
AccountCreateTransaction, AccountId, AccountInfoQuery, AccountUpdateTransaction, Client, Hbar, Key, KeyList, PrivateKey, ScheduleInfoQuery, ScheduleSignTransaction, TransferTransaction
24+
};
25+
use time::{Duration, OffsetDateTime};
26+
use tokio::time::sleep;
27+
28+
#[derive(Parser, Debug)]
29+
struct Args {
30+
#[clap(long, env)]
31+
operator_account_id: AccountId,
32+
33+
#[clap(long, env)]
34+
operator_key: PrivateKey,
35+
36+
#[clap(long, env, default_value = "testnet")]
37+
hedera_network: String,
38+
}
39+
40+
#[tokio::main]
41+
async fn main() -> anyhow::Result<()> {
42+
let _ = dotenvy::dotenv();
43+
44+
let args = Args::parse();
45+
46+
/*
47+
* Step 0: Create and configure the client
48+
*/
49+
let client = Client::for_name(&args.hedera_network)?;
50+
client.set_operator(args.operator_account_id, args.operator_key);
51+
52+
/*
53+
* Step 1: Create key pairs
54+
*/
55+
let key1 = PrivateKey::generate_ed25519();
56+
let key2 = PrivateKey::generate_ed25519();
57+
58+
println!("Creating Key List... (w/ threshold, 2 of 2 keys generated above is required to modify the account)");
59+
60+
let threshold_key = KeyList {
61+
keys: vec![key1.public_key().into(), key2.public_key().into()],
62+
threshold: Some(2),
63+
};
64+
65+
println!("Created key list: {threshold_key:?}");
66+
67+
/*
68+
* Step 2: Create the account
69+
*/
70+
println!("Creating account with threshold key...");
71+
let alice_id = AccountCreateTransaction::new()
72+
.key(Key::KeyList(threshold_key))
73+
.initial_balance(Hbar::new(2))
74+
.execute(&client)
75+
.await?
76+
.get_receipt(&client)
77+
.await?
78+
.account_id
79+
.unwrap();
80+
81+
println!("Created account with id: {alice_id}");
82+
83+
/*
84+
* Step 3:
85+
* Schedule a transfer transaction of 1 hbar from the newly created account to the operator account.
86+
* The transaction will be scheduled with expirationTime = 24 hours from now and waitForExpiry = false.
87+
*/
88+
println!("Creating new scheduled transaction with 1 day expiry...");
89+
let mut transfer = TransferTransaction::new();
90+
transfer
91+
.hbar_transfer(alice_id, Hbar::new(-1))
92+
.hbar_transfer(args.operator_account_id, Hbar::new(1));
93+
94+
let schedule_id = transfer
95+
.schedule()
96+
.wait_for_expiry(false)
97+
.expiration_time(OffsetDateTime::now_utc() + Duration::seconds(86400))
98+
.execute(&client)
99+
.await?
100+
.get_receipt(&client)
101+
.await?
102+
.schedule_id
103+
.unwrap();
104+
105+
/*
106+
* Step 4: Sign the transaction with one key and verify the transaction is not executed
107+
*/
108+
println!("Signing transaction with key 1...");
109+
_ = ScheduleSignTransaction::new()
110+
.schedule_id(schedule_id)
111+
.freeze_with(&client)?
112+
.sign(key1.clone())
113+
.execute(&client)
114+
.await?
115+
.get_receipt(&client)
116+
.await?;
117+
118+
let info = ScheduleInfoQuery::new()
119+
.schedule_id(schedule_id)
120+
.execute(&client)
121+
.await?;
122+
123+
println!(
124+
"Scheduled transaction is not executed yet. Executed at: {:?}",
125+
info.executed_at
126+
);
127+
128+
/*
129+
* Step 5: Sign the transaction with the second key and verify the transaction is executed
130+
*/
131+
132+
let account_balance = AccountInfoQuery::new()
133+
.account_id(alice_id)
134+
.execute(&client)
135+
.await?
136+
.balance;
137+
138+
println!("Alice's account balance before scheduled transaction: {account_balance}");
139+
140+
println!("Signing transaction with key 2...");
141+
_ = ScheduleSignTransaction::new()
142+
.schedule_id(schedule_id)
143+
.freeze_with(&client)?
144+
.sign(key2.clone())
145+
.execute(&client)
146+
.await?
147+
.get_receipt(&client)
148+
.await?;
149+
150+
let account_balance = AccountInfoQuery::new()
151+
.account_id(alice_id)
152+
.execute(&client)
153+
.await?
154+
.balance;
155+
156+
println!("Alice's account balance after scheduled transaction: {account_balance}");
157+
158+
let info = ScheduleInfoQuery::new()
159+
.schedule_id(schedule_id)
160+
.execute(&client)
161+
.await?;
162+
163+
println!("Scheduled transaction executed at: {:?}", info.executed_at);
164+
165+
/*
166+
* Step 6:
167+
* Schedule another transfer transaction of 1 Hbar from the account to the operator account
168+
* with an expirationTime of 10 seconds in the future and waitForExpiry=true.
169+
*/
170+
println!("Creating new scheduled transaction with 10 second expiry...");
171+
let mut transfer = TransferTransaction::new();
172+
transfer
173+
.hbar_transfer(alice_id, Hbar::new(-1))
174+
.hbar_transfer(args.operator_account_id, Hbar::new(1));
175+
176+
let schedule_id = transfer
177+
.schedule()
178+
.wait_for_expiry(true)
179+
.expiration_time(OffsetDateTime::now_utc() + Duration::seconds(10))
180+
.execute(&client)
181+
.await?
182+
.get_receipt(&client)
183+
.await?
184+
.schedule_id
185+
.unwrap();
186+
187+
/*
188+
* Step 7:
189+
* Sign the transaction with one key and verify the transaction is not executed
190+
*/
191+
println!("Signing scheduled transaction with key 1...");
192+
_ = ScheduleSignTransaction::new()
193+
.schedule_id(schedule_id)
194+
.freeze_with(&client)?
195+
.sign(key1.clone())
196+
.execute(&client)
197+
.await?
198+
.get_receipt(&client)
199+
.await?;
200+
201+
let info = ScheduleInfoQuery::new()
202+
.schedule_id(schedule_id)
203+
.execute(&client)
204+
.await?;
205+
206+
println!(
207+
"Scheduled transaction is not executed yet. Executed at: {:?}",
208+
info.executed_at
209+
);
210+
211+
/*
212+
* Step 8:
213+
* Update the account's key to be only the one key
214+
* that has already signed the scheduled transfer.
215+
*/
216+
println!("Updating account key to only key 1...");
217+
_ = AccountUpdateTransaction::new()
218+
.account_id(alice_id)
219+
.key(key1.public_key())
220+
.freeze_with(&client)?
221+
.sign(key1)
222+
.sign(key2)
223+
.execute(&client)
224+
.await?
225+
.get_receipt(&client)
226+
.await?;
227+
228+
/*
229+
* Step 9:
230+
* Verify that the transfer successfully executes roughly at the time of its expiration.
231+
*/
232+
let account_balance = AccountInfoQuery::new()
233+
.account_id(alice_id)
234+
.execute(&client)
235+
.await?
236+
.balance;
237+
238+
println!("Alice's account balance before scheduled transfer: {account_balance}");
239+
240+
sleep(std::time::Duration::from_millis(10_000)).await;
241+
242+
let account_balance = AccountInfoQuery::new()
243+
.account_id(alice_id)
244+
.execute(&client)
245+
.await?
246+
.balance;
247+
248+
println!("Alice's account balance after scheduled transfer: {account_balance}");
249+
250+
println!("Successfully executed scheduled transfer");
251+
252+
Ok(())
253+
}

protobufs/build.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use regex::RegexBuilder;
3131
const DERIVE_EQ_HASH: &str = "#[derive(Eq, Hash)]";
3232
const DERIVE_EQ_HASH_COPY: &str = "#[derive(Copy, Eq, Hash)]";
3333
const SERVICES_FOLDER: &str = "./protobufs/services";
34+
const EVENT_FOLDER: &str = "./protobufs/platform/event";
3435

3536
fn main() -> anyhow::Result<()> {
3637
// services is the "base" module for the hedera protobufs
@@ -64,9 +65,37 @@ fn main() -> anyhow::Result<()> {
6465
)?;
6566
fs::rename(out_path.join("services"), &services_tmp_path)?;
6667

68+
let event_path = Path::new(EVENT_FOLDER);
69+
println!("cargo:rerun-if-changed={}", EVENT_FOLDER);
70+
71+
if !event_path.is_dir() {
72+
anyhow::bail!(
73+
"Folder {EVENT_FOLDER} does not exist; do you need to `git submodule update --init`?"
74+
);
75+
}
76+
77+
let event_tmp_path = out_path.join("event");
78+
79+
// // Ensure we start fresh
80+
let _ = fs::remove_dir_all(&event_tmp_path);
81+
82+
create_dir_all(&event_tmp_path)?;
83+
84+
// Copy the event folder
85+
fs_extra::copy_items(
86+
&[event_path],
87+
&services_tmp_path,
88+
&fs_extra::dir::CopyOptions::new().overwrite(true).copy_inside(false),
89+
)?;
90+
fs::rename(out_path.join("event"), &event_tmp_path)?;
91+
let _ = fs::remove_dir_all(&event_tmp_path);
92+
6793
let services: Vec<_> = read_dir(&services_tmp_path)?
94+
.chain(read_dir(&services_tmp_path.join("auxiliary").join("tss"))?)
95+
.chain(read_dir(&services_tmp_path.join("event"))?)
6896
.filter_map(|entry| {
6997
let entry = entry.ok()?;
98+
7099
entry.file_type().ok()?.is_file().then(|| entry.path())
71100
})
72101
.collect();
@@ -82,6 +111,12 @@ fn main() -> anyhow::Result<()> {
82111
// remove com.hedera.hapi.node.addressbook. prefix
83112
let contents = contents.replace("com.hedera.hapi.node.addressbook.", "");
84113

114+
// remove com.hedera.hapi.services.auxiliary.tss. prefix
115+
let contents = contents.replace("com.hedera.hapi.services.auxiliary.tss.", "");
116+
117+
// remove com.hedera.hapi.platform.event. prefix
118+
let contents = contents.replace("com.hedera.hapi.platform.event.", "");
119+
85120
fs::write(service, &*contents)?;
86121
}
87122

@@ -141,8 +176,15 @@ fn main() -> anyhow::Result<()> {
141176
"]"#,
142177
);
143178

179+
// Services fails with message:
180+
// --- stderr
181+
// Error: protoc failed: event/state_signature_transaction.proto: File not found.
182+
// transaction_body.proto:111:1: Import "event/state_signature_transaction.proto" was not found or had errors.
183+
//
144184
cfg.compile(&services, &[services_tmp_path])?;
145185

186+
// panic!("Services succeeded");
187+
146188
// NOTE: prost generates rust doc comments and fails to remove the leading * line
147189
remove_useless_comments(&Path::new(&env::var("OUT_DIR")?).join("proto.rs"))?;
148190

@@ -256,6 +298,10 @@ fn main() -> anyhow::Result<()> {
256298
.services_same("TokenUpdateTransactionBody")
257299
.services_same("TokenUpdateNftsTransactionBody")
258300
.services_same("TokenWipeAccountTransactionBody")
301+
.services_same("TssMessageTransactionBody")
302+
.services_same("TssVoteTransactionBody")
303+
.services_same("TssShareSignatureTransactionBody")
304+
.services_same("TssEncryptionKeyTransactionBody")
259305
.services_same("Transaction")
260306
.services_same("TransactionBody")
261307
.services_same("UncheckedSubmitBody")

src/fee_schedules.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,12 @@ pub enum RequestType {
430430

431431
/// Cancel airdrop tokens.
432432
TokenCancelAirdrop,
433+
434+
/// Submit a vote as part of the Threshold Signature Scheme (TSS) processing.
435+
TssMessage,
436+
437+
/// Submit a vote as part of the Threshold Signature Scheme (TSS) processing.
438+
TssVote,
433439
}
434440

435441
impl FromProtobuf<services::HederaFunctionality> for RequestType {
@@ -518,6 +524,8 @@ impl FromProtobuf<services::HederaFunctionality> for RequestType {
518524
HederaFunctionality::TokenAirdrop => Self::TokenAirdrop,
519525
HederaFunctionality::TokenClaimAirdrop => Self::TokenClaimAirdrop,
520526
HederaFunctionality::TokenCancelAirdrop => Self::TokenCancelAirdrop,
527+
HederaFunctionality::TssMessage => Self::TssMessage,
528+
HederaFunctionality::TssVote => Self::TssVote,
521529
};
522530

523531
Ok(value)
@@ -612,6 +620,8 @@ impl ToProtobuf for RequestType {
612620
Self::TokenAirdrop => HederaFunctionality::TokenAirdrop,
613621
Self::TokenClaimAirdrop => HederaFunctionality::TokenClaimAirdrop,
614622
Self::TokenCancelAirdrop => HederaFunctionality::TokenCancelAirdrop,
623+
Self::TssMessage => HederaFunctionality::TssMessage,
624+
Self::TssVote => HederaFunctionality::TssVote,
615625
}
616626
}
617627
}

0 commit comments

Comments
 (0)