Skip to content

Commit 6e322c7

Browse files
committed
feat: implement HIP-1313 with unit and integration tests
Signed-off-by: emiliyank <e.kadiyski@gmail.com>
1 parent 614ace6 commit 6e322c7

File tree

5 files changed

+271
-1
lines changed

5 files changed

+271
-1
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
package com.hedera.hashgraph.sdk.examples;
3+
4+
import com.hedera.hashgraph.sdk.*;
5+
import com.hedera.hashgraph.sdk.logger.LogLevel;
6+
import com.hedera.hashgraph.sdk.logger.Logger;
7+
import io.github.cdimascio.dotenv.Dotenv;
8+
import java.util.Objects;
9+
10+
/**
11+
* Create a Hedera account using high-volume throttles.
12+
*/
13+
class HighVolumeAccountCreateExample {
14+
15+
/*
16+
* See .env.sample in the examples folder root for how to specify values below
17+
* or set environment variables with the same names.
18+
*/
19+
20+
/**
21+
* Operator's account ID.
22+
* Used to sign and pay for operations on Hedera.
23+
*/
24+
private static final AccountId OPERATOR_ID =
25+
AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID")));
26+
27+
/**
28+
* Operator's private key.
29+
*/
30+
private static final PrivateKey OPERATOR_KEY =
31+
PrivateKey.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_KEY")));
32+
33+
/**
34+
* HEDERA_NETWORK defaults to testnet if not specified in dotenv file.
35+
* Network can be: localhost, testnet, previewnet or mainnet.
36+
*/
37+
private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet");
38+
39+
/**
40+
* SDK_LOG_LEVEL defaults to SILENT if not specified in dotenv file.
41+
* Log levels can be: TRACE, DEBUG, INFO, WARN, ERROR, SILENT.
42+
* <p>
43+
* Important pre-requisite: set simple logger log level to same level as the SDK_LOG_LEVEL,
44+
* for example via VM options: -Dorg.slf4j.simpleLogger.log.org.hiero=trace
45+
*/
46+
private static final String SDK_LOG_LEVEL = Dotenv.load().get("SDK_LOG_LEVEL", "SILENT");
47+
48+
public static void main(String[] args) throws Exception {
49+
System.out.println("High-Volume Account Create Example Start!");
50+
51+
/*
52+
* Step 0:
53+
* Create and configure the SDK Client.
54+
*/
55+
Client client = ClientHelper.forName(HEDERA_NETWORK);
56+
// All generated transactions will be paid by this account and signed by this key.
57+
client.setOperator(OPERATOR_ID, OPERATOR_KEY);
58+
// Attach logger to the SDK Client.
59+
client.setLogger(new Logger(LogLevel.valueOf(SDK_LOG_LEVEL)));
60+
61+
/*
62+
* Step 1:
63+
* Generate ED25519 private and public key pair for the account.
64+
*/
65+
PrivateKey privateKey = PrivateKey.generateED25519();
66+
PublicKey publicKey = privateKey.getPublicKey();
67+
System.out.println("Future account private key: " + privateKey);
68+
System.out.println("Future account public key: " + publicKey);
69+
70+
/*
71+
* Step 2:
72+
* Create a new account using high-volume throttles and set a fee limit.
73+
*/
74+
System.out.println("Creating new account with high-volume throttles...");
75+
TransactionResponse accountCreateTxResponse = new AccountCreateTransaction()
76+
.setKeyWithoutAlias(publicKey)
77+
.setInitialBalance(Hbar.from(1))
78+
.setHighVolume(true)
79+
.setMaxTransactionFee(Hbar.from(5))
80+
.execute(client);
81+
82+
// This will wait for the receipt to become available.
83+
TransactionReceipt accountCreateTxReceipt = accountCreateTxResponse.getReceipt(client);
84+
AccountId newAccountId = accountCreateTxReceipt.accountId;
85+
Objects.requireNonNull(newAccountId);
86+
System.out.println("Created account with ID: " + newAccountId);
87+
88+
/*
89+
* Clean up:
90+
* Delete created account.
91+
*/
92+
new AccountDeleteTransaction()
93+
.setTransferAccountId(OPERATOR_ID)
94+
.setAccountId(newAccountId)
95+
.freezeWith(client)
96+
.sign(privateKey)
97+
.execute(client)
98+
.getReceipt(client);
99+
100+
client.close();
101+
102+
System.out.println("High-Volume Account Create Example Complete!");
103+
}
104+
}

sdk/src/main/java/com/hedera/hashgraph/sdk/Transaction.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ public abstract class Transaction<T extends Transaction<T>>
118118

119119
private String memo = "";
120120

121+
private boolean highVolume = false;
122+
121123
List<CustomFeeLimit> customFeeLimits = new ArrayList<>();
122124

123125
private Key batchKey = null;
@@ -142,6 +144,7 @@ public abstract class Transaction<T extends Transaction<T>>
142144
setTransactionValidDuration(DEFAULT_TRANSACTION_VALID_DURATION);
143145
setMaxTransactionFee(Hbar.fromTinybars(txBody.getTransactionFee()));
144146
setTransactionMemo(txBody.getMemo());
147+
setHighVolume(txBody.getHighVolume());
145148

146149
sourceTransactionBody = txBody;
147150
}
@@ -225,6 +228,7 @@ public abstract class Transaction<T extends Transaction<T>>
225228
DurationConverter.fromProtobuf(sourceTransactionBody.getTransactionValidDuration()));
226229
setMaxTransactionFee(Hbar.fromTinybars(sourceTransactionBody.getTransactionFee()));
227230
setTransactionMemo(sourceTransactionBody.getMemo());
231+
setHighVolume(sourceTransactionBody.getHighVolume());
228232

229233
this.customFeeLimits = sourceTransactionBody.getMaxCustomFeesList().stream()
230234
.map(CustomFeeLimit::fromProtobuf)
@@ -839,6 +843,30 @@ public final T setTransactionMemo(String memo) {
839843
return (T) this;
840844
}
841845

846+
/**
847+
* Extract the high-volume flag.
848+
*
849+
* @return true if high-volume throttles are enabled, false otherwise
850+
*/
851+
public final boolean getHighVolume() {
852+
return highVolume;
853+
}
854+
855+
/**
856+
* If set to true, this transaction uses high-volume throttles and pricing
857+
* for entity creation. It only affects supported transaction types; otherwise,
858+
* it is ignored.
859+
*
860+
* @param highVolume true to enable high-volume throttles, false otherwise
861+
* @return {@code this}
862+
*/
863+
public final T setHighVolume(boolean highVolume) {
864+
requireNotFrozen();
865+
this.highVolume = highVolume;
866+
// noinspection unchecked
867+
return (T) this;
868+
}
869+
842870
/**
843871
* batchify method is used to mark a transaction as part of a batch transaction or make it so-called inner transaction.
844872
* The Transaction will be frozen and signed by the operator of the client.
@@ -1215,7 +1243,8 @@ protected TransactionBody.Builder spawnBodyBuilder(@Nullable Client client) {
12151243
.setTransactionValidDuration(DurationConverter.toProtobuf(transactionValidDuration).toBuilder())
12161244
.addAllMaxCustomFees(
12171245
customFeeLimits.stream().map(CustomFeeLimit::toProtobuf).collect(Collectors.toList()))
1218-
.setMemo(memo);
1246+
.setMemo(memo)
1247+
.setHighVolume(highVolume);
12191248
if (batchKey != null) {
12201249
builder.setBatchKey(batchKey.toProtobufKey());
12211250
}

sdk/src/main/proto/transaction.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ message TransactionBody {
240240
*/
241241
string memo = 6;
242242

243+
/**
244+
* If set to true, this transaction uses high-volume throttles and pricing
245+
* for entity creation. It only affects supported transaction types; otherwise,
246+
* it is ignored.
247+
*/
248+
bool high_volume = 77;
249+
243250
/**
244251
* The <b>public key</b> of the trusted batch assembler.
245252
*/

sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,4 +618,66 @@ void testGetSignableNodeBodyBytesListFileAppendMultipleChunks() throws InvalidPr
618618
}
619619
}
620620
}
621+
622+
@Test
623+
void highVolumeDefaultsToFalse() {
624+
var transaction = new AccountCreateTransaction();
625+
626+
assertThat(transaction.getHighVolume()).isFalse();
627+
}
628+
629+
@Test
630+
void highVolumeCanBeSerialized() throws InvalidProtocolBufferException {
631+
var transaction = new AccountCreateTransaction()
632+
.setKey(PrivateKey.generateED25519())
633+
.setHighVolume(true);
634+
635+
var transactionFromBytes = Transaction.fromBytes(transaction.toBytes());
636+
637+
assertThat(transactionFromBytes).isInstanceOf(AccountCreateTransaction.class);
638+
assertThat(((AccountCreateTransaction) transactionFromBytes).getHighVolume())
639+
.isTrue();
640+
}
641+
642+
@Test
643+
void highVolumeCannotChangeAfterFreeze() {
644+
var transaction = new AccountCreateTransaction()
645+
.setKey(PrivateKey.generateED25519())
646+
.setTransactionId(testTransactionID)
647+
.setNodeAccountIds(testNodeAccountIds)
648+
.freeze();
649+
650+
assertThrows(IllegalStateException.class, () -> transaction.setHighVolume(true));
651+
}
652+
653+
@Test
654+
void highVolumeIsIncludedInProtobufOutput() throws InvalidProtocolBufferException {
655+
var transaction = new AccountCreateTransaction()
656+
.setKey(PrivateKey.generateED25519())
657+
.setTransactionId(testTransactionID)
658+
.setNodeAccountIds(testNodeAccountIds)
659+
.setHighVolume(true)
660+
.freeze();
661+
662+
List<Transaction.SignableNodeTransactionBodyBytes> signableBodies = transaction.getSignableNodeBodyBytesList();
663+
assertThat(signableBodies).isNotEmpty();
664+
665+
// Parse the first body and verify high_volume is set
666+
TransactionBody body = TransactionBody.parseFrom(signableBodies.get(0).getBody());
667+
assertThat(body.getHighVolume()).isTrue();
668+
669+
// Test with highVolume set to false
670+
var transactionFalse = new AccountCreateTransaction()
671+
.setKey(PrivateKey.generateED25519())
672+
.setTransactionId(testTransactionID)
673+
.setNodeAccountIds(testNodeAccountIds)
674+
.setHighVolume(false)
675+
.freeze();
676+
677+
List<Transaction.SignableNodeTransactionBodyBytes> signableBodiesFalse =
678+
transactionFalse.getSignableNodeBodyBytesList();
679+
TransactionBody bodyFalse =
680+
TransactionBody.parseFrom(signableBodiesFalse.get(0).getBody());
681+
assertThat(bodyFalse.getHighVolume()).isFalse();
682+
}
621683
}

sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/AccountCreateIntegrationTest.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,74 @@ void createAccountUsingSetKeyWithAliasWithED25519KeyAndPublicECDSAKeyShouldHaveE
548548
}
549549
}
550550

551+
@Test
552+
@DisplayName("Can create account with high-volume throttles enabled")
553+
void canCreateAccountWithHighVolume() throws Exception {
554+
try (var testEnv = new IntegrationTestEnv(1)) {
555+
var key = PrivateKey.generateED25519();
556+
557+
var response = new AccountCreateTransaction()
558+
.setKeyWithoutAlias(key)
559+
.setInitialBalance(new Hbar(1))
560+
.setHighVolume(true)
561+
.setMaxTransactionFee(Hbar.from(10))
562+
.execute(testEnv.client);
563+
564+
var accountId = Objects.requireNonNull(response.getReceipt(testEnv.client).accountId);
565+
566+
var info = new AccountInfoQuery().setAccountId(accountId).execute(testEnv.client);
567+
568+
assertThat(info.accountId).isEqualTo(accountId);
569+
assertThat(info.isDeleted).isFalse();
570+
assertThat(info.key.toString()).isEqualTo(key.getPublicKey().toString());
571+
assertThat(info.balance).isEqualTo(new Hbar(1));
572+
}
573+
}
574+
575+
@Test
576+
@DisplayName("Can create account with high-volume throttles and valid max transaction fee")
577+
void canCreateAccountWithHighVolumeAndValidMaxFee() throws Exception {
578+
try (var testEnv = new IntegrationTestEnv(1)) {
579+
var key = PrivateKey.generateED25519();
580+
581+
var response = new AccountCreateTransaction()
582+
.setKeyWithoutAlias(key)
583+
.setInitialBalance(new Hbar(1))
584+
.setHighVolume(true)
585+
.setMaxTransactionFee(Hbar.from(5))
586+
.execute(testEnv.client);
587+
588+
var receipt = response.getReceipt(testEnv.client);
589+
var accountId = Objects.requireNonNull(receipt.accountId);
590+
591+
assertThat(receipt.status).isEqualTo(Status.SUCCESS);
592+
assertThat(accountId).isNotNull();
593+
594+
var info = new AccountInfoQuery().setAccountId(accountId).execute(testEnv.client);
595+
assertThat(info.accountId).isEqualTo(accountId);
596+
}
597+
}
598+
599+
@Test
600+
@DisplayName(
601+
"Account creation with high-volume throttles fails with INSUFFICIENT_TX_FEE when maxTransactionFee is too low")
602+
void accountCreateWithHighVolumeFailsWithInsufficientTxFeeWhenMaxFeeTooLow() throws Exception {
603+
try (var testEnv = new IntegrationTestEnv(1)) {
604+
var key = PrivateKey.generateED25519();
605+
606+
// Set a very low max transaction fee that will be insufficient for high-volume throttles
607+
assertThatExceptionOfType(ReceiptStatusException.class)
608+
.isThrownBy(() -> new AccountCreateTransaction()
609+
.setKeyWithoutAlias(key)
610+
.setInitialBalance(new Hbar(1))
611+
.setHighVolume(true)
612+
.setMaxTransactionFee(Hbar.fromTinybars(1)) // Very low fee - should fail
613+
.execute(testEnv.client)
614+
.getReceipt(testEnv.client))
615+
.satisfies(error -> assertThat(error.receipt.status).isEqualTo(Status.INSUFFICIENT_TX_FEE));
616+
}
617+
}
618+
551619
private boolean isLongZeroAddress(byte[] address) {
552620
for (int i = 0; i < 12; i++) {
553621
if (address[i] != 0) {

0 commit comments

Comments
 (0)