Skip to content

Commit c6d2f45

Browse files
authored
Add account update support (#64) (#69)
* Add account update support (#64) Signed-off-by: Aman <amkr6207@gmail.com> * Apply spotless formatting Signed-off-by: Aman <amkr6207@gmail.com> * fix(account): add runtime null checks in AccountClientImpl update methods Signed-off-by: Aman <amkr6207@gmail.com> * test(account): add spring/microprofile update integration tests and allow blank memo updates Signed-off-by: Aman <amkr6207@gmail.com> --------- Signed-off-by: Aman <amkr6207@gmail.com>
1 parent aa0f78f commit c6d2f45

12 files changed

Lines changed: 392 additions & 0 deletions

File tree

hiero-enterprise-base/src/main/java/org/hiero/base/AccountClient.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.hedera.hashgraph.sdk.AccountId;
44
import com.hedera.hashgraph.sdk.Hbar;
5+
import com.hedera.hashgraph.sdk.PrivateKey;
56
import java.util.Objects;
67
import org.hiero.base.data.Account;
78
import org.jspecify.annotations.NonNull;
@@ -71,6 +72,39 @@ default Account createAccount(long initialBalanceInHbar) throws HieroException {
7172
*/
7273
void deleteAccount(@NonNull Account account, @NonNull Account toAccount) throws HieroException;
7374

75+
/**
76+
* Updates the account key of the given account.
77+
*
78+
* @param account the account to update
79+
* @param updatedPrivateKey the new private key to set for the account
80+
* @return the updated account with the same account ID and new key pair
81+
* @throws HieroException if the account could not be updated
82+
*/
83+
@NonNull Account updateAccountKey(@NonNull Account account, @NonNull PrivateKey updatedPrivateKey)
84+
throws HieroException;
85+
86+
/**
87+
* Updates the memo of the given account.
88+
*
89+
* @param account the account to update
90+
* @param memo the new memo
91+
* @throws HieroException if the account could not be updated
92+
*/
93+
void updateAccountMemo(@NonNull Account account, @NonNull String memo) throws HieroException;
94+
95+
/**
96+
* Updates both key and memo of the given account.
97+
*
98+
* @param account the account to update
99+
* @param updatedPrivateKey the new private key to set for the account
100+
* @param memo the new memo
101+
* @return the updated account with the same account ID and new key pair
102+
* @throws HieroException if the account could not be updated
103+
*/
104+
@NonNull Account updateAccount(
105+
@NonNull Account account, @NonNull PrivateKey updatedPrivateKey, @NonNull String memo)
106+
throws HieroException;
107+
74108
/**
75109
* Returns the balance of the given account.
76110
*

hiero-enterprise-base/src/main/java/org/hiero/base/implementation/AccountClientImpl.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.hedera.hashgraph.sdk.AccountId;
44
import com.hedera.hashgraph.sdk.Hbar;
5+
import com.hedera.hashgraph.sdk.PrivateKey;
56
import java.util.Objects;
67
import org.hiero.base.AccountClient;
78
import org.hiero.base.HieroException;
@@ -12,6 +13,7 @@
1213
import org.hiero.base.protocol.data.AccountCreateRequest;
1314
import org.hiero.base.protocol.data.AccountCreateResult;
1415
import org.hiero.base.protocol.data.AccountDeleteRequest;
16+
import org.hiero.base.protocol.data.AccountUpdateRequest;
1517
import org.jspecify.annotations.NonNull;
1618

1719
public class AccountClientImpl implements AccountClient {
@@ -55,6 +57,37 @@ public void deleteAccount(@NonNull Account account, @NonNull Account toAccount)
5557
client.executeAccountDeleteTransaction(request);
5658
}
5759

60+
@Override
61+
public @NonNull Account updateAccountKey(
62+
@NonNull Account account, @NonNull PrivateKey updatedPrivateKey) throws HieroException {
63+
Objects.requireNonNull(account, "account must not be null");
64+
Objects.requireNonNull(updatedPrivateKey, "updatedPrivateKey must not be null");
65+
final AccountUpdateRequest request = AccountUpdateRequest.updateKey(account, updatedPrivateKey);
66+
client.executeAccountUpdateTransaction(request);
67+
return Account.of(account.accountId(), updatedPrivateKey);
68+
}
69+
70+
@Override
71+
public void updateAccountMemo(@NonNull Account account, @NonNull String memo)
72+
throws HieroException {
73+
Objects.requireNonNull(account, "account must not be null");
74+
Objects.requireNonNull(memo, "memo must not be null");
75+
final AccountUpdateRequest request = AccountUpdateRequest.updateMemo(account, memo);
76+
client.executeAccountUpdateTransaction(request);
77+
}
78+
79+
@Override
80+
public @NonNull Account updateAccount(
81+
@NonNull Account account, @NonNull PrivateKey updatedPrivateKey, @NonNull String memo)
82+
throws HieroException {
83+
Objects.requireNonNull(account, "account must not be null");
84+
Objects.requireNonNull(updatedPrivateKey, "updatedPrivateKey must not be null");
85+
Objects.requireNonNull(memo, "memo must not be null");
86+
final AccountUpdateRequest request = AccountUpdateRequest.of(account, updatedPrivateKey, memo);
87+
client.executeAccountUpdateTransaction(request);
88+
return Account.of(account.accountId(), updatedPrivateKey);
89+
}
90+
5891
@NonNull
5992
@Override
6093
public Hbar getAccountBalance(@NonNull AccountId account) throws HieroException {

hiero-enterprise-base/src/main/java/org/hiero/base/implementation/ProtocolLayerClientImpl.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.hedera.hashgraph.sdk.AccountCreateTransaction;
77
import com.hedera.hashgraph.sdk.AccountDeleteTransaction;
88
import com.hedera.hashgraph.sdk.AccountId;
9+
import com.hedera.hashgraph.sdk.AccountUpdateTransaction;
910
import com.hedera.hashgraph.sdk.ContractCreateTransaction;
1011
import com.hedera.hashgraph.sdk.ContractDeleteTransaction;
1112
import com.hedera.hashgraph.sdk.ContractExecuteTransaction;
@@ -56,6 +57,8 @@
5657
import org.hiero.base.protocol.data.AccountCreateResult;
5758
import org.hiero.base.protocol.data.AccountDeleteRequest;
5859
import org.hiero.base.protocol.data.AccountDeleteResult;
60+
import org.hiero.base.protocol.data.AccountUpdateRequest;
61+
import org.hiero.base.protocol.data.AccountUpdateResult;
5962
import org.hiero.base.protocol.data.ContractCallRequest;
6063
import org.hiero.base.protocol.data.ContractCallResult;
6164
import org.hiero.base.protocol.data.ContractCreateRequest;
@@ -377,6 +380,30 @@ public AccountDeleteResult executeAccountDeleteTransaction(
377380
record.transactionFee);
378381
}
379382

383+
@Override
384+
@NonNull
385+
public AccountUpdateResult executeAccountUpdateTransaction(
386+
@NonNull final AccountUpdateRequest request) throws HieroException {
387+
Objects.requireNonNull(request, "request must not be null");
388+
final AccountUpdateTransaction transaction =
389+
new AccountUpdateTransaction()
390+
.setMaxTransactionFee(request.maxTransactionFee())
391+
.setTransactionValidDuration(request.transactionValidDuration())
392+
.setAccountId(request.toUpdate().accountId());
393+
if (request.memo() != null) {
394+
transaction.setAccountMemo(request.memo());
395+
}
396+
if (request.updatedPrivateKey() != null) {
397+
transaction.setKey(request.updatedPrivateKey().getPublicKey());
398+
sign(transaction, request.toUpdate().privateKey(), request.updatedPrivateKey());
399+
} else {
400+
sign(transaction, request.toUpdate().privateKey());
401+
}
402+
final TransactionReceipt receipt =
403+
executeTransactionAndWaitOnReceipt(transaction, TransactionType.ACCOUNT_UPDATE);
404+
return new AccountUpdateResult(receipt.transactionId, receipt.status);
405+
}
406+
380407
public TopicCreateResult executeTopicCreateTransaction(@NonNull final TopicCreateRequest request)
381408
throws HieroException {
382409
Objects.requireNonNull(request, "request must not be null");

hiero-enterprise-base/src/main/java/org/hiero/base/protocol/ProtocolLayerClient.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import org.hiero.base.protocol.data.AccountCreateResult;
99
import org.hiero.base.protocol.data.AccountDeleteRequest;
1010
import org.hiero.base.protocol.data.AccountDeleteResult;
11+
import org.hiero.base.protocol.data.AccountUpdateRequest;
12+
import org.hiero.base.protocol.data.AccountUpdateResult;
1113
import org.hiero.base.protocol.data.ContractCallRequest;
1214
import org.hiero.base.protocol.data.ContractCallResult;
1315
import org.hiero.base.protocol.data.ContractCreateRequest;
@@ -173,6 +175,16 @@ public interface ProtocolLayerClient {
173175
@NonNull AccountDeleteResult executeAccountDeleteTransaction(
174176
@NonNull AccountDeleteRequest request) throws HieroException;
175177

178+
/**
179+
* Executes an account update transaction.
180+
*
181+
* @param request the request containing the details of the account update transaction
182+
* @return the result of the account update transaction
183+
* @throws HieroException if the transaction could not be executed
184+
*/
185+
@NonNull AccountUpdateResult executeAccountUpdateTransaction(
186+
@NonNull AccountUpdateRequest request) throws HieroException;
187+
176188
/**
177189
* Executes a token create transaction.
178190
*
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package org.hiero.base.protocol.data;
2+
3+
import com.hedera.hashgraph.sdk.Hbar;
4+
import com.hedera.hashgraph.sdk.PrivateKey;
5+
import java.time.Duration;
6+
import java.util.Objects;
7+
import org.hiero.base.data.Account;
8+
import org.jspecify.annotations.NonNull;
9+
import org.jspecify.annotations.Nullable;
10+
11+
public record AccountUpdateRequest(
12+
@NonNull Hbar maxTransactionFee,
13+
@NonNull Duration transactionValidDuration,
14+
@NonNull Account toUpdate,
15+
@Nullable PrivateKey updatedPrivateKey,
16+
@Nullable String memo)
17+
implements TransactionRequest {
18+
19+
public AccountUpdateRequest {
20+
Objects.requireNonNull(maxTransactionFee, "maxTransactionFee is required");
21+
Objects.requireNonNull(transactionValidDuration, "transactionValidDuration is required");
22+
Objects.requireNonNull(toUpdate, "toUpdate is required");
23+
if (maxTransactionFee.toTinybars() < 0) {
24+
throw new IllegalArgumentException("maxTransactionFee must be non-negative");
25+
}
26+
if (transactionValidDuration.isNegative() || transactionValidDuration.isZero()) {
27+
throw new IllegalArgumentException("transactionValidDuration must be positive");
28+
}
29+
if (updatedPrivateKey == null && memo == null) {
30+
throw new IllegalArgumentException("at least one update field (key or memo) must be set");
31+
}
32+
}
33+
34+
@NonNull
35+
public static AccountUpdateRequest updateKey(
36+
@NonNull Account toUpdate, @NonNull PrivateKey updatedPrivateKey) {
37+
Objects.requireNonNull(toUpdate, "toUpdate is required");
38+
Objects.requireNonNull(updatedPrivateKey, "updatedPrivateKey is required");
39+
return new AccountUpdateRequest(
40+
DEFAULT_MAX_TRANSACTION_FEE,
41+
DEFAULT_TRANSACTION_VALID_DURATION,
42+
toUpdate,
43+
updatedPrivateKey,
44+
null);
45+
}
46+
47+
@NonNull
48+
public static AccountUpdateRequest updateMemo(@NonNull Account toUpdate, @NonNull String memo) {
49+
Objects.requireNonNull(toUpdate, "toUpdate is required");
50+
Objects.requireNonNull(memo, "memo is required");
51+
return new AccountUpdateRequest(
52+
DEFAULT_MAX_TRANSACTION_FEE, DEFAULT_TRANSACTION_VALID_DURATION, toUpdate, null, memo);
53+
}
54+
55+
@NonNull
56+
public static AccountUpdateRequest of(
57+
@NonNull Account toUpdate, @NonNull PrivateKey updatedPrivateKey, @NonNull String memo) {
58+
Objects.requireNonNull(toUpdate, "toUpdate is required");
59+
Objects.requireNonNull(updatedPrivateKey, "updatedPrivateKey is required");
60+
Objects.requireNonNull(memo, "memo is required");
61+
return new AccountUpdateRequest(
62+
DEFAULT_MAX_TRANSACTION_FEE,
63+
DEFAULT_TRANSACTION_VALID_DURATION,
64+
toUpdate,
65+
updatedPrivateKey,
66+
memo);
67+
}
68+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.hiero.base.protocol.data;
2+
3+
import com.hedera.hashgraph.sdk.Status;
4+
import com.hedera.hashgraph.sdk.TransactionId;
5+
import java.util.Objects;
6+
import org.jspecify.annotations.NonNull;
7+
8+
public record AccountUpdateResult(@NonNull TransactionId transactionId, @NonNull Status status)
9+
implements TransactionResult {
10+
public AccountUpdateResult {
11+
Objects.requireNonNull(transactionId, "transactionId must not be null");
12+
Objects.requireNonNull(status, "status must not be null");
13+
}
14+
}

hiero-enterprise-base/src/test/java/org/hiero/base/test/AccountClientImplTest.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
import com.hedera.hashgraph.sdk.AccountId;
88
import com.hedera.hashgraph.sdk.Hbar;
9+
import com.hedera.hashgraph.sdk.PrivateKey;
10+
import com.hedera.hashgraph.sdk.Status;
11+
import com.hedera.hashgraph.sdk.TransactionId;
912
import org.hiero.base.HieroException;
1013
import org.hiero.base.data.Account;
1114
import org.hiero.base.implementation.AccountClientImpl;
@@ -14,8 +17,11 @@
1417
import org.hiero.base.protocol.data.AccountBalanceResponse;
1518
import org.hiero.base.protocol.data.AccountCreateRequest;
1619
import org.hiero.base.protocol.data.AccountCreateResult;
20+
import org.hiero.base.protocol.data.AccountUpdateRequest;
21+
import org.hiero.base.protocol.data.AccountUpdateResult;
1722
import org.junit.jupiter.api.BeforeEach;
1823
import org.junit.jupiter.api.Test;
24+
import org.mockito.ArgumentCaptor;
1925
import org.mockito.ArgumentMatchers;
2026

2127
public class AccountClientImplTest {
@@ -151,4 +157,84 @@ void testCreateAccountHieroExceptionThrown() throws HieroException {
151157
assertThrows(HieroException.class, () -> accountClientImpl.createAccount(initialBalance));
152158
assertEquals("Transaction failed", exception.getMessage());
153159
}
160+
161+
@Test
162+
void testUpdateAccountKeySuccessful() throws HieroException {
163+
Account account = Account.of(AccountId.fromString("0.0.12345"), PrivateKey.generateECDSA());
164+
PrivateKey updatedPrivateKey = PrivateKey.generateECDSA();
165+
when(mockProtocolLayerClient.executeAccountUpdateTransaction(any(AccountUpdateRequest.class)))
166+
.thenReturn(
167+
new AccountUpdateResult(TransactionId.generate(account.accountId()), Status.SUCCESS));
168+
169+
Account updatedAccount = accountClientImpl.updateAccountKey(account, updatedPrivateKey);
170+
171+
assertEquals(account.accountId(), updatedAccount.accountId());
172+
assertEquals(updatedPrivateKey.getPublicKey(), updatedAccount.publicKey());
173+
assertEquals(updatedPrivateKey, updatedAccount.privateKey());
174+
verify(mockProtocolLayerClient, times(1))
175+
.executeAccountUpdateTransaction(any(AccountUpdateRequest.class));
176+
}
177+
178+
@Test
179+
void testUpdateAccountMemoSuccessful() throws HieroException {
180+
Account account = Account.of(AccountId.fromString("0.0.12345"), PrivateKey.generateECDSA());
181+
String memo = "updated-memo";
182+
ArgumentCaptor<AccountUpdateRequest> requestCaptor =
183+
ArgumentCaptor.forClass(AccountUpdateRequest.class);
184+
when(mockProtocolLayerClient.executeAccountUpdateTransaction(any(AccountUpdateRequest.class)))
185+
.thenReturn(
186+
new AccountUpdateResult(TransactionId.generate(account.accountId()), Status.SUCCESS));
187+
188+
accountClientImpl.updateAccountMemo(account, memo);
189+
190+
verify(mockProtocolLayerClient, times(1))
191+
.executeAccountUpdateTransaction(requestCaptor.capture());
192+
AccountUpdateRequest request = requestCaptor.getValue();
193+
assertEquals(account, request.toUpdate());
194+
assertEquals(memo, request.memo());
195+
assertNull(request.updatedPrivateKey());
196+
}
197+
198+
@Test
199+
void testUpdateAccountMemoBlankMemoSuccessful() throws HieroException {
200+
Account account = Account.of(AccountId.fromString("0.0.12345"), PrivateKey.generateECDSA());
201+
String memo = " ";
202+
ArgumentCaptor<AccountUpdateRequest> requestCaptor =
203+
ArgumentCaptor.forClass(AccountUpdateRequest.class);
204+
when(mockProtocolLayerClient.executeAccountUpdateTransaction(any(AccountUpdateRequest.class)))
205+
.thenReturn(
206+
new AccountUpdateResult(TransactionId.generate(account.accountId()), Status.SUCCESS));
207+
208+
accountClientImpl.updateAccountMemo(account, memo);
209+
210+
verify(mockProtocolLayerClient, times(1))
211+
.executeAccountUpdateTransaction(requestCaptor.capture());
212+
AccountUpdateRequest request = requestCaptor.getValue();
213+
assertEquals(account, request.toUpdate());
214+
assertEquals(memo, request.memo());
215+
assertNull(request.updatedPrivateKey());
216+
}
217+
218+
@Test
219+
void testUpdateAccountSuccessful() throws HieroException {
220+
Account account = Account.of(AccountId.fromString("0.0.12345"), PrivateKey.generateECDSA());
221+
PrivateKey updatedPrivateKey = PrivateKey.generateECDSA();
222+
String memo = "updated-memo";
223+
ArgumentCaptor<AccountUpdateRequest> requestCaptor =
224+
ArgumentCaptor.forClass(AccountUpdateRequest.class);
225+
when(mockProtocolLayerClient.executeAccountUpdateTransaction(any(AccountUpdateRequest.class)))
226+
.thenReturn(
227+
new AccountUpdateResult(TransactionId.generate(account.accountId()), Status.SUCCESS));
228+
229+
Account updatedAccount = accountClientImpl.updateAccount(account, updatedPrivateKey, memo);
230+
231+
verify(mockProtocolLayerClient, times(1))
232+
.executeAccountUpdateTransaction(requestCaptor.capture());
233+
AccountUpdateRequest request = requestCaptor.getValue();
234+
assertEquals(account, request.toUpdate());
235+
assertEquals(updatedPrivateKey, request.updatedPrivateKey());
236+
assertEquals(memo, request.memo());
237+
assertEquals(account.accountId(), updatedAccount.accountId());
238+
assertEquals(updatedPrivateKey, updatedAccount.privateKey());
239+
}
154240
}

0 commit comments

Comments
 (0)