Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HIP-423: Long Term Scheduled Transactions #2123

Merged
merged 7 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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!");
}
}
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