Skip to content

Commit 38a6048

Browse files
authored
Add batch transaction acceptance test (hiero-ledger#12922)
this pr adds a new acceptance test to cover batch transactions --------- Signed-off-by: Jesse Nelson <jesse@hashgraph.com>
1 parent d2de795 commit 38a6048

5 files changed

Lines changed: 214 additions & 2 deletions

File tree

rest/api/v1/openapi.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4224,7 +4224,9 @@ components:
42244224
valid_start_timestamp:
42254225
$ref: "#/components/schemas/Timestamp"
42264226
example:
4227-
batch_key: "0xae8bebf1c9fa0f309356e48057f6047af7cde63037d0509d16ddc3b20e085158bfdf14d15345c1b18b199b72fed4dead"
4227+
batch_key:
4228+
_type: "ED25519"
4229+
key: "7934a257a6144fabc8fbdeeaa5810662adb89e7b6978ace46a74fdb2d12bd4b2"
42284230
bytes: null
42294231
charged_tx_fee: 7
42304232
consensus_timestamp: "1234567890.000000007"

test/src/test/java/org/hiero/mirror/test/e2e/acceptance/client/AccountClient.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.hedera.hashgraph.sdk.AccountDeleteTransaction;
1010
import com.hedera.hashgraph.sdk.AccountId;
1111
import com.hedera.hashgraph.sdk.AccountUpdateTransaction;
12+
import com.hedera.hashgraph.sdk.BatchTransaction;
1213
import com.hedera.hashgraph.sdk.ContractId;
1314
import com.hedera.hashgraph.sdk.EvmAddress;
1415
import com.hedera.hashgraph.sdk.Hbar;
@@ -32,6 +33,7 @@
3233
import java.util.function.Consumer;
3334
import lombok.CustomLog;
3435
import lombok.RequiredArgsConstructor;
36+
import lombok.SneakyThrows;
3537
import org.hiero.mirror.test.e2e.acceptance.config.AcceptanceTestProperties;
3638
import org.hiero.mirror.test.e2e.acceptance.props.ExpandedAccountId;
3739
import org.hiero.mirror.test.e2e.acceptance.response.NetworkTransactionResponse;
@@ -376,6 +378,65 @@ public NetworkTransactionResponse updateAccount(
376378
return response;
377379
}
378380

381+
/**
382+
* Submits an atomic batch with two inner CryptoTransfer transactions:
383+
* 1) funding transfer to an EVM alias -> hollow auto-create
384+
* 2) normal transfer to an existing account
385+
*
386+
* Returns NetworkTransactionResponse for the batch transaction.
387+
*/
388+
@SneakyThrows
389+
public NetworkTransactionResponse submitBatchWithHollowAutoCreateAndNormalTransfer(
390+
ExpandedAccountId batchSigner,
391+
AccountId hollowAliasAccountId,
392+
AccountClient.AccountNameEnum transferRecipientName,
393+
long hollowFundingTinybars,
394+
long normalTransferTinybars) {
395+
396+
final var recipient = getAccount(transferRecipientName);
397+
398+
// Inner #1: hollow auto-create (operator funds alias)
399+
final var hollowCreateInner = new TransferTransaction()
400+
.addHbarTransfer(client.getOperatorAccountId(), Hbar.fromTinybars(-hollowFundingTinybars))
401+
.addHbarTransfer(hollowAliasAccountId, Hbar.fromTinybars(hollowFundingTinybars))
402+
.setTransactionMemo(getMemo("Hollow auto-create"))
403+
.batchify(client, batchSigner.getPublicKey());
404+
405+
// Inner #2: normal transfer (operator -> recipient)
406+
final var normalTransferInner = new TransferTransaction()
407+
.addHbarTransfer(client.getOperatorAccountId(), Hbar.fromTinybars(-normalTransferTinybars))
408+
.addHbarTransfer(recipient.getAccountId(), Hbar.fromTinybars(normalTransferTinybars))
409+
.setTransactionMemo(getMemo("Normal transfer"))
410+
.batchify(client, batchSigner.getPublicKey());
411+
412+
final var batch =
413+
new BatchTransaction().addInnerTransaction(hollowCreateInner).addInnerTransaction(normalTransferInner);
414+
415+
batch.setMaxTransactionFee(Hbar.from(5));
416+
417+
return executeTransactionAndRetrieveReceipt(batch, (KeyList) null);
418+
}
419+
420+
/**
421+
* Completion transaction (hollow -> full):
422+
* payer is the hollow account and it must sign with its ECDSA private key.
423+
*
424+
* Returns NetworkTransactionResponse for the completion transaction.
425+
*/
426+
@SneakyThrows
427+
public NetworkTransactionResponse submitCompletionTransaction(
428+
AccountId hollowResolvedAccountId, PrivateKey hollowAccountPrivateKey) {
429+
final var completionTx = new TransferTransaction().setTransactionMemo(getMemo("Complete hollow account"));
430+
431+
final var hollowPayer = new ExpandedAccountId(hollowResolvedAccountId, hollowAccountPrivateKey);
432+
final var keys = KeyList.of(hollowAccountPrivateKey);
433+
final var completionResponse = executeTransactionAndRetrieveReceipt(completionTx, keys, hollowPayer);
434+
435+
accountIds.add(hollowPayer);
436+
437+
return completionResponse;
438+
}
439+
379440
@RequiredArgsConstructor
380441
public enum AccountNameEnum {
381442
ALICE(false, Key.KeyCase.ED25519),

test/src/test/java/org/hiero/mirror/test/e2e/acceptance/config/AcceptanceTestProperties.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public final class AcceptanceTestProperties {
8181
@NotNull
8282
@DecimalMax("1000000")
8383
@DecimalMin("1.0")
84-
private BigDecimal operatorBalance = BigDecimal.valueOf(72); // Amount in USD
84+
private BigDecimal operatorBalance = BigDecimal.valueOf(73); // Amount in USD
8585

8686
@NotBlank
8787
private String operatorId;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package org.hiero.mirror.test.e2e.acceptance.steps;
4+
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
7+
import com.hedera.hashgraph.sdk.AccountId;
8+
import com.hedera.hashgraph.sdk.PrivateKey;
9+
import io.cucumber.java.en.Then;
10+
import io.cucumber.java.en.When;
11+
import lombok.CustomLog;
12+
import lombok.RequiredArgsConstructor;
13+
import org.hiero.mirror.common.CommonProperties;
14+
import org.hiero.mirror.rest.model.TransactionTypes;
15+
import org.hiero.mirror.test.e2e.acceptance.client.AccountClient;
16+
import org.hiero.mirror.test.e2e.acceptance.client.MirrorNodeClient;
17+
import org.hiero.mirror.test.e2e.acceptance.props.ExpandedAccountId;
18+
19+
@CustomLog
20+
@RequiredArgsConstructor
21+
public class BatchTransactionFeature {
22+
23+
private final MirrorNodeClient mirrorClient;
24+
private final AccountClient accountClient;
25+
private final CommonProperties commonProperties;
26+
private final PrivateKey hollowAccountPrivateKey = PrivateKey.generateECDSA();
27+
28+
private String batchTransactionId;
29+
private AccountId hollowResolvedAccountId;
30+
private String completionTransactionId;
31+
private ExpandedAccountId batchSigner;
32+
private AccountId hollowAliasAccountId;
33+
34+
@When(
35+
"I submit a batch transaction containing transfer {long} tℏ to {string} and a hollow account create with {long} tℏ with batch signed by {string}")
36+
public void submitBatchWithHollowAndNormalTransfer(
37+
long normalTransferAmount,
38+
String recipientAccountName,
39+
long hollowFundingAmount,
40+
String batchSignerAccountName) {
41+
log.debug("Submitting batch transaction (hollow auto-create + normal crypto transfer)");
42+
batchSigner = accountClient.getAccount(AccountClient.AccountNameEnum.valueOf(batchSignerAccountName));
43+
44+
hollowAliasAccountId = AccountId.fromEvmAddress(
45+
hollowAccountPrivateKey.getPublicKey().toEvmAddress().toString(),
46+
commonProperties.getShard(),
47+
commonProperties.getRealm());
48+
49+
final var batchResult = accountClient.submitBatchWithHollowAutoCreateAndNormalTransfer(
50+
batchSigner,
51+
hollowAliasAccountId,
52+
AccountClient.AccountNameEnum.valueOf(recipientAccountName),
53+
hollowFundingAmount,
54+
normalTransferAmount);
55+
56+
assertThat(batchResult).isNotNull();
57+
assertThat(batchResult.getTransactionIdStringNoCheckSum()).isNotNull();
58+
59+
batchTransactionId = batchResult.getTransactionIdStringNoCheckSum();
60+
}
61+
62+
@When("I submit a transaction that completes the hollow account")
63+
public void completeHollow() {
64+
assertThat(hollowResolvedAccountId).isNotNull();
65+
log.debug("Submitting completion transaction for hollow account {}", hollowResolvedAccountId);
66+
67+
var completionResponse =
68+
accountClient.submitCompletionTransaction(hollowResolvedAccountId, hollowAccountPrivateKey);
69+
70+
assertThat(completionResponse).isNotNull();
71+
assertThat(completionResponse.getTransactionId()).isNotNull();
72+
73+
completionTransactionId = completionResponse.getTransactionIdStringNoCheckSum();
74+
}
75+
76+
@Then("I should be able to resolve the hollow account id")
77+
public void resolveHollowAccountId() {
78+
final var accountDetails = mirrorClient.getAccountDetailsUsingEvmAddress(hollowAliasAccountId);
79+
80+
hollowResolvedAccountId = AccountId.fromString(accountDetails.getAccount());
81+
82+
assertThat(hollowResolvedAccountId).isNotNull();
83+
assertThat(accountDetails.getKey()).isNull();
84+
}
85+
86+
@Then("I should see the batch transaction in mirror node")
87+
public void verifyBatchTransactionInRecordStream() {
88+
assertThat(batchTransactionId).isNotNull();
89+
90+
final var transactions =
91+
mirrorClient.getTransactions(batchTransactionId).getTransactions();
92+
assertThat(transactions).isNotNull();
93+
assertThat(transactions).hasSize(4); // batch + 2 inner + hollow create
94+
95+
final var atomicBatch = transactions.stream()
96+
.filter(t -> TransactionTypes.ATOMICBATCH.equals(t.getName()))
97+
.findFirst()
98+
.orElse(null);
99+
assertThat(atomicBatch);
100+
101+
// Matches inner transactions and hollow create. All should have parent timestamp of ATOMIC_BATCH consensus
102+
// timestamp
103+
final var innerTransactions = transactions.stream()
104+
.filter(t -> atomicBatch.getConsensusTimestamp().equals(t.getParentConsensusTimestamp()))
105+
.toList();
106+
assertThat(innerTransactions).hasSize(3);
107+
assertThat(innerTransactions)
108+
.filteredOn(t -> TransactionTypes.CRYPTOTRANSFER.equals(t.getName()))
109+
.hasSize(2)
110+
.allMatch(t -> batchSigner
111+
.getPublicKey()
112+
.toStringRaw()
113+
.equals(t.getBatchKey().getKey()));
114+
assertThat(innerTransactions)
115+
.filteredOn(t -> TransactionTypes.CRYPTOCREATEACCOUNT.equals(t.getName()))
116+
.hasSize(1)
117+
.allMatch(t -> t.getBatchKey() == null);
118+
}
119+
120+
@Then("I should see the completion transaction in mirror node")
121+
public void verifyCompletionTransaction() {
122+
assertThat(completionTransactionId).isNotNull();
123+
124+
var completionTransactions =
125+
mirrorClient.getTransactions(completionTransactionId).getTransactions();
126+
assertThat(completionTransactions).hasSize(2);
127+
128+
assertThat(completionTransactions)
129+
.filteredOn(t -> TransactionTypes.CRYPTOTRANSFER.equals(t.getName()))
130+
.hasSize(1);
131+
132+
assertThat(completionTransactions)
133+
.filteredOn(t -> TransactionTypes.CRYPTOUPDATEACCOUNT.equals(t.getName()))
134+
.hasSize(1);
135+
}
136+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@batch @fullsuite
2+
Feature: Batch Transaction Coverage Feature
3+
4+
@acceptance
5+
Scenario Outline: Submit a batch transaction containing a hollow account create, a normal crypto transfer, then complete the hollow account
6+
When I submit a batch transaction containing transfer <normalTransferAmount> tℏ to <recipientAccountName> and a hollow account create with <hollowFundingAmount> tℏ with batch signed by <batchSignerAccountName>
7+
Then I should be able to resolve the hollow account id
8+
And I should see the batch transaction in mirror node
9+
When I submit a transaction that completes the hollow account
10+
Then I should see the completion transaction in mirror node
11+
Examples:
12+
| normalTransferAmount | recipientAccountName | hollowFundingAmount | batchSignerAccountName |
13+
| 10000 | "ALICE" | 5000000 | "OPERATOR" |

0 commit comments

Comments
 (0)