Skip to content

Commit

Permalink
HIP-423: Long Term Scheduled Transactions (#2123)
Browse files Browse the repository at this point in the history
Signed-off-by: Ivan Ivanov <ivanivanov.ii726@gmail.com>
  • Loading branch information
0xivanov authored Dec 11, 2024
1 parent 5ac181b commit 472693d
Show file tree
Hide file tree
Showing 20 changed files with 1,084 additions and 151 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
run: ./gradlew checkAllModuleInfo --continue --scan

- name: Start Local Node
run: npx @hashgraph/hedera-local start -d --network local
run: npx @hashgraph/hedera-local start -d --network local --network-tag=0.57.0

- name: Run Unit and Integration Tests
env:
Expand Down Expand Up @@ -150,7 +150,7 @@ jobs:
run: ./gradlew -p example-android assemble --scan

- name: Start the local node
run: npx @hashgraph/hedera-local start -d --network local
run: npx @hashgraph/hedera-local start -d --network local --network-tag=0.57.0

- name: Prepare .env for Examples
run: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/*-
*
* Hedera Java SDK
*
* Copyright (C) 2020 - 2024 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.
*
*/
package com.hedera.hashgraph.sdk.examples;

import com.hedera.hashgraph.sdk.AccountBalanceQuery;
import com.hedera.hashgraph.sdk.AccountCreateTransaction;
import com.hedera.hashgraph.sdk.AccountId;
import com.hedera.hashgraph.sdk.AccountUpdateTransaction;
import com.hedera.hashgraph.sdk.Client;
import com.hedera.hashgraph.sdk.Hbar;
import com.hedera.hashgraph.sdk.KeyList;
import com.hedera.hashgraph.sdk.PrivateKey;
import com.hedera.hashgraph.sdk.ScheduleInfo;
import com.hedera.hashgraph.sdk.ScheduleInfoQuery;
import com.hedera.hashgraph.sdk.ScheduleSignTransaction;
import com.hedera.hashgraph.sdk.TransferTransaction;
import com.hedera.hashgraph.sdk.logger.LogLevel;
import com.hedera.hashgraph.sdk.logger.Logger;
import io.github.cdimascio.dotenv.Dotenv;
import java.time.Instant;
import java.util.Objects;

/**
* How to long term schedule transactions (HIP-423).
*/
class LongTermScheduledTransactionExample {

/*
* See .env.sample in the examples folder root for how to specify values below
* or set environment variables with the same names.
*/

/**
* Operator's account ID. Used to sign and pay for operations on Hedera.
*/
private static final AccountId OPERATOR_ID = AccountId.fromString(
Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID")));

/**
* Operator's private key.
*/
private static final PrivateKey OPERATOR_KEY = PrivateKey.fromString(
Objects.requireNonNull(Dotenv.load().get("OPERATOR_KEY")));

/**
* HEDERA_NETWORK defaults to testnet if not specified in dotenv file. Network can be: localhost, testnet,
* previewnet or mainnet.
*/
private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet");

/**
* SDK_LOG_LEVEL defaults to SILENT if not specified in dotenv file. Log levels can be: TRACE, DEBUG, INFO, WARN,
* ERROR, SILENT.
* <p>
* Important pre-requisite: set simple logger log level to same level as the SDK_LOG_LEVEL, for example via VM
* options: -Dorg.slf4j.simpleLogger.log.com.hedera.hashgraph=trace
*/
private static final String SDK_LOG_LEVEL = Dotenv.load().get("SDK_LOG_LEVEL", "SILENT");

public static void main(String[] args) throws Exception {
System.out.println("Long Term Scheduled Transaction Example Start!");

/*
* Step 0:
* Create and configure the SDK Client.
*/
Client client = ClientHelper.forName(HEDERA_NETWORK);
// All generated transactions will be paid by this account and signed by this key.
client.setOperator(OPERATOR_ID, OPERATOR_KEY);
// Attach logger to the SDK Client.
client.setLogger(new Logger(LogLevel.valueOf(SDK_LOG_LEVEL)));

/*
* Step 1:
* Create key pairs
*/
var privateKey1 = PrivateKey.generateED25519();
var publicKey1 = privateKey1.getPublicKey();
var privateKey2 = PrivateKey.generateED25519();

System.out.println("Creating a Key List..." +
"(with threshold, it will require 2 of 2 keys we generated to sign on anything modifying this account).");
KeyList thresholdKey = KeyList.withThreshold(2);
thresholdKey.add(privateKey1);
thresholdKey.add(privateKey2);
System.out.println("Created a Key List: " + thresholdKey);

/*
* Step 2:
* Create the account
*/
System.out.println("Creating new account...(with the above Key List as an account key).");
var alice = new AccountCreateTransaction()
.setKey(thresholdKey)
.setInitialBalance(new Hbar(2))
.execute(client)
.getReceipt(client).accountId;
System.out.println("Created new account with ID: " + alice);

/*
* Step 3:
* Schedule a transfer transaction of 1 Hbar from the created account to the
* operator account with an expirationTime of
* 24 hours in the future and waitForExpiry=false
*/
System.out.println("Creating new scheduled transaction with 1 day expiry");
TransferTransaction transfer = new TransferTransaction()
.addHbarTransfer(alice, new Hbar(1).negated())
.addHbarTransfer(client.getOperatorAccountId(), new Hbar(1));

int oneDayInSecs = 86400;
var scheduleId = transfer
.schedule()
.setWaitForExpiry(false)
.setExpirationTime(Instant.now().plusSeconds(oneDayInSecs))
.execute(client)
.getReceipt(client)
.scheduleId;

/*
* Step 4:
* Sign the transaction with one key and verify the transaction is not executed
*/
System.out.println("Signing the new scheduled transaction with 1 key");
new ScheduleSignTransaction()
.setScheduleId(scheduleId)
.freezeWith(client)
.sign(privateKey1)
.execute(client)
.getReceipt(client);

ScheduleInfo info = new ScheduleInfoQuery()
.setScheduleId(scheduleId)
.execute(client);
System.out.println("Scheduled transaction is not yet executed. Executed at: " + info.executedAt);

/*
* Step 5:
* Sign the transaction with the other key and verify the transaction executes successfully
*/
var accountBalance = new AccountBalanceQuery()
.setAccountId(alice)
.execute(client);
System.out.println("Alice's account balance before schedule transfer: " + accountBalance.hbars);

System.out.println("Signing the new scheduled transaction with the 2nd key");
new ScheduleSignTransaction()
.setScheduleId(scheduleId)
.freezeWith(client)
.sign(privateKey2)
.execute(client)
.getReceipt(client);

accountBalance = new AccountBalanceQuery()
.setAccountId(alice)
.execute(client);
System.out.println("Alice's account balance after schedule transfer: " + accountBalance.hbars);

info = new ScheduleInfoQuery()
.setScheduleId(scheduleId)
.execute(client);
System.out.println("Scheduled transaction is executed. Executed at: " + info.executedAt);

/*
* 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 .
*/
System.out.println("Creating new scheduled transaction with 10 seconds expiry");
transfer = new TransferTransaction()
.addHbarTransfer(alice, new Hbar(1).negated())
.addHbarTransfer(client.getOperatorAccountId(), new Hbar(1));

var scheduleId2 = transfer
.schedule()
.setWaitForExpiry(true)
.setExpirationTime(Instant.now().plusSeconds(10))
.execute(client)
.getReceipt(client)
.scheduleId;
long startTime = System.currentTimeMillis();
long elapsedTime = 0;

/*
* Step 7:
* Sign the transaction with one key and verify the transaction is not executed
*/
System.out.println("Signing the new scheduled transaction with 1 key");
new ScheduleSignTransaction()
.setScheduleId(scheduleId2)
.freezeWith(client)
.sign(privateKey1)
.execute(client)
.getReceipt(client);

info = new ScheduleInfoQuery()
.setScheduleId(scheduleId2)
.execute(client);
System.out.println("Scheduled transaction is not yet executed. Executed at: " + info.executedAt);

/*
* Step 8:
* Update the account’s key to be only the one key
* that has already signed the scheduled transfer.
*/
System.out.println("Updating Alice's key to be the 1st key");
new AccountUpdateTransaction()
.setAccountId(alice)
.setKey(publicKey1)
.freezeWith(client)
.sign(privateKey1)
.sign(privateKey2)
.execute(client)
.getReceipt(client);

/*
* Step 9:
* Verify that the transfer successfully executes roughly at the time of its expiration.
*/
accountBalance = new AccountBalanceQuery()
.setAccountId(alice)
.execute(client);

System.out.println("Alice's account balance before schedule transfer: " + accountBalance.hbars);
while (elapsedTime < 10 * 1000) {
elapsedTime = System.currentTimeMillis() - startTime;
System.out.printf("Elapsed time: %.1f seconds\r", elapsedTime / 1000.0);
Thread.sleep(100); // Pause briefly to reduce CPU usage
}
accountBalance = new AccountBalanceQuery()
.setAccountId(alice)
.execute(client);
System.out.println("Alice's account balance after schedule transfer: " + accountBalance.hbars);

System.out.println("Long Term Scheduled Transaction Example Complete!");
}
}
9 changes: 8 additions & 1 deletion sdk/src/main/java/com/hedera/hashgraph/sdk/RequestType.java
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,12 @@ public enum RequestType {
/**
* Submit a vote as part of the Threshold Signature Scheme (TSS) processing.
*/
TSS_VOTE(HederaFunctionality.TssVote);
TSS_VOTE(HederaFunctionality.TssVote),

/**
* Submit a node signature as part of the Threshold Signature Scheme (TSS) processing.
*/
TSS_SHARE_SIGNATURE(HederaFunctionality.TssShareSignature);

final HederaFunctionality code;

Expand Down Expand Up @@ -536,6 +541,7 @@ static RequestType valueOf(HederaFunctionality code) {
case TokenClaimAirdrop -> TOKEN_CLAIM_AIRDROP;
case TssMessage -> TSS_MESSAGE;
case TssVote -> TSS_VOTE;
case TssShareSignature -> TSS_SHARE_SIGNATURE;
default -> throw new IllegalStateException("(BUG) unhandled HederaFunctionality");
};
}
Expand Down Expand Up @@ -627,6 +633,7 @@ public String toString() {
case TOKEN_CLAIM_AIRDROP -> "TOKEN_CLAIM_AIRDROP";
case TSS_MESSAGE -> "TSS_MESSAGE";
case TSS_VOTE -> "TSS_VOTE";
case TSS_SHARE_SIGNATURE -> "TSS_SHARE_SIGNATURE";
};
}
}
23 changes: 22 additions & 1 deletion sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java
Original file line number Diff line number Diff line change
Expand Up @@ -1749,7 +1749,25 @@ public enum Status {
* The client SHOULD query mirror node to determine the status of the pending
* airdrop and whether the sender can fulfill the offer.
*/
INVALID_TOKEN_IN_PENDING_AIRDROP(ResponseCodeEnum.INVALID_TOKEN_IN_PENDING_AIRDROP);
INVALID_TOKEN_IN_PENDING_AIRDROP(ResponseCodeEnum.INVALID_TOKEN_IN_PENDING_AIRDROP),

/**
* A scheduled transaction configured to wait for expiry to execute was given
* an expiry time at which there is already too many transactions scheduled to
* expire; its creation must be retried with a different expiry.
*/
SCHEDULE_EXPIRY_IS_BUSY(ResponseCodeEnum.SCHEDULE_EXPIRY_IS_BUSY),

/**
* The provided gRPC certificate hash is invalid.
*/
INVALID_GRPC_CERTIFICATE_HASH(ResponseCodeEnum.INVALID_GRPC_CERTIFICATE_HASH),

/**
* A scheduled transaction configured to wait for expiry to execute was not
* given an explicit expiration time.
*/
MISSING_EXPIRY_TIME(ResponseCodeEnum.MISSING_EXPIRY_TIME);

final ResponseCodeEnum code;

Expand Down Expand Up @@ -2087,6 +2105,9 @@ static Status valueOf(ResponseCodeEnum code) {
case INVALID_PENDING_AIRDROP_ID -> INVALID_PENDING_AIRDROP_ID;
case TOKEN_AIRDROP_WITH_FALLBACK_ROYALTY -> TOKEN_AIRDROP_WITH_FALLBACK_ROYALTY;
case INVALID_TOKEN_IN_PENDING_AIRDROP -> INVALID_TOKEN_IN_PENDING_AIRDROP;
case SCHEDULE_EXPIRY_IS_BUSY -> SCHEDULE_EXPIRY_IS_BUSY;
case INVALID_GRPC_CERTIFICATE_HASH -> INVALID_GRPC_CERTIFICATE_HASH;
case MISSING_EXPIRY_TIME -> MISSING_EXPIRY_TIME;
case UNRECOGNIZED ->
// NOTE: Protobuf deserialization will not give us the code on the wire
throw new IllegalArgumentException(
Expand Down
7 changes: 6 additions & 1 deletion sdk/src/main/proto/basic_types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1246,7 +1246,7 @@ enum HederaFunctionality {
TokenCancelAirdrop = 94;

/**
* Claim one or more pending airdrops
* Claim one or more pending airdrops
*/
TokenClaimAirdrop = 95;

Expand All @@ -1259,6 +1259,11 @@ enum HederaFunctionality {
* Submit a vote as part of the Threshold Signature Scheme (TSS) processing.
*/
TssVote = 97;

/**
* Submit a node signature as part of the Threshold Signature Scheme (TSS) processing.
*/
TssShareSignature = 98;
}

/**
Expand Down
16 changes: 12 additions & 4 deletions sdk/src/main/proto/block_stream_info.proto
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ message BlockStreamInfo {
* The latest available hash SHALL be for block N-1.<br/>
* This is REQUIRED to implement the EVM `BLOCKHASH` opcode.
* <p>
* <div style="display: block;font-size: .83em;margin-top: 1.67em;margin-bottom: 1.67em;margin-left: 0;margin-right: 0;font-weight: bold;">Field Length</div>
* ### Field Length
* Each hash value SHALL be the trailing 265 bits of a SHA2-384 hash.<br/>
* The length of this field SHALL be an integer multiple of 32 bytes.<br/>
* This field SHALL be at least 32 bytes.<br/>
Expand Down Expand Up @@ -143,9 +143,9 @@ message BlockStreamInfo {

/**
* A version describing the version of application software.
* <p>
* This SHALL be the software version that created this block.
*/
* <p>
* This SHALL be the software version that created this block.
*/
proto.SemanticVersion creation_software_version = 11;

/**
Expand All @@ -155,4 +155,12 @@ message BlockStreamInfo {
* at which an interval of time-dependent events were processed.
*/
proto.Timestamp last_interval_process_time = 12;

/**
* The time stamp at which the last user transaction was handled.
* <p>
* This field SHALL hold the consensus time for the last time
* at which a user transaction was handled.
*/
proto.Timestamp last_handle_time = 13;
}
Loading

0 comments on commit 472693d

Please sign in to comment.