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

Add implementation for AccountReadableKVState #9414

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public class ContractCallContext {
* The timestamp used to fetch the state from the stackedStateFrames.
*/
@Setter
private long timestamp = 0;
private Optional<Long> timestamp = Optional.empty();

private ContractCallContext() {}

Expand Down Expand Up @@ -121,8 +121,8 @@ public void addOpcodes(Opcode opcode) {
*/
public void initializeStackFrames(final StackedStateFrames stackedStateFrames) {
if (stackedStateFrames != null) {
final var stateTimestamp = this.timestamp > 0
? Optional.of(this.timestamp)
final var stateTimestamp = timestamp.isPresent()
? timestamp
: Optional.ofNullable(recordFile).map(RecordFile::getConsensusEnd);
stackBase = stack = stackedStateFrames.getInitializedStackBase(stateTimestamp);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.inject.Named;
import jakarta.validation.Valid;
import java.util.Optional;
import lombok.CustomLog;
import org.springframework.validation.annotation.Validated;

Expand All @@ -57,7 +58,7 @@ public ContractDebugService(
public OpcodesProcessingResult processOpcodeCall(
final @Valid ContractDebugParameters params, final OpcodeTracerOptions opcodeTracerOptions) {
return ContractCallContext.run(ctx -> {
ctx.setTimestamp(params.getConsensusTimestamp() - 1);
ctx.setTimestamp(Optional.of(params.getConsensusTimestamp() - 1));
ctx.setOpcodeTracerOptions(opcodeTracerOptions);
ctx.setContractActions(contractActionRepository.findFailedSystemActionsByConsensusTimestamp(
params.getConsensusTimestamp()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/*
* Copyright (C) 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.mirror.web3.state;

import static com.hedera.mirror.common.domain.entity.EntityType.CONTRACT;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.AccountID.AccountOneOfType;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.state.token.AccountApprovalForAllAllowance;
import com.hedera.hapi.node.state.token.AccountCryptoAllowance;
import com.hedera.hapi.node.state.token.AccountFungibleTokenAllowance;
import com.hedera.mirror.common.domain.entity.CryptoAllowance;
import com.hedera.mirror.common.domain.entity.Entity;
import com.hedera.mirror.common.domain.entity.NftAllowance;
import com.hedera.mirror.common.domain.entity.TokenAllowance;
import com.hedera.mirror.web3.common.ContractCallContext;
import com.hedera.mirror.web3.repository.AccountBalanceRepository;
import com.hedera.mirror.web3.repository.CryptoAllowanceRepository;
import com.hedera.mirror.web3.repository.NftAllowanceRepository;
import com.hedera.mirror.web3.repository.NftRepository;
import com.hedera.mirror.web3.repository.TokenAccountRepository;
import com.hedera.mirror.web3.repository.TokenAllowanceRepository;
import com.hedera.pbj.runtime.OneOf;
import com.hedera.pbj.runtime.ParseException;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import com.swirlds.state.spi.ReadableKVStateBase;
import jakarta.annotation.Nonnull;
import jakarta.inject.Named;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.extern.java.Log;

/**
* This class serves as a repository layer between hedera app services read only state and the Postgres database in mirror-node
*
* The object, which is read from DB is converted to the PBJ generated format, so that it can properly be utilized by the hedera app components
* */
@Named
@Log
public class AccountReadableKVState extends ReadableKVStateBase<AccountID, Account> {
private static final long DEFAULT_AUTO_RENEW_PERIOD = 7776000L;
private static final String KEY = "ACCOUNTS";
private static final Long ZERO_BALANCE = 0L;

private final CommonEntityAccessor commonEntityAccessor;
private final NftAllowanceRepository nftAllowanceRepository;
private final NftRepository nftRepository;
private final TokenAllowanceRepository tokenAllowanceRepository;
private final CryptoAllowanceRepository cryptoAllowanceRepository;
private final TokenAccountRepository tokenAccountRepository;
private final AccountBalanceRepository accountBalanceRepository;

public AccountReadableKVState(
CommonEntityAccessor commonEntityAccessor,
NftAllowanceRepository nftAllowanceRepository,
NftRepository nftRepository,
TokenAllowanceRepository tokenAllowanceRepository,
CryptoAllowanceRepository cryptoAllowanceRepository,
TokenAccountRepository tokenAccountRepository,
AccountBalanceRepository accountBalanceRepository) {
super(KEY);
this.commonEntityAccessor = commonEntityAccessor;
this.nftAllowanceRepository = nftAllowanceRepository;
this.nftRepository = nftRepository;
this.tokenAllowanceRepository = tokenAllowanceRepository;
this.cryptoAllowanceRepository = cryptoAllowanceRepository;
this.tokenAccountRepository = tokenAccountRepository;
this.accountBalanceRepository = accountBalanceRepository;
}

@Override
protected Account readFromDataSource(@Nonnull AccountID key) {
final var timestamp = ContractCallContext.get().getTimestamp();
return commonEntityAccessor
.get(key, timestamp)
.map(entity -> accountFromEntity(entity, timestamp))
.orElse(null);
}

@Override
protected @Nonnull Iterator<AccountID> iterateFromDataSource() {
return Collections.emptyIterator();

Check warning on line 102 in hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/AccountReadableKVState.java

View check run for this annotation

Codecov / codecov/patch

hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/AccountReadableKVState.java#L102

Added line #L102 was not covered by tests
}

@Override
public long size() {
return 0;

Check warning on line 107 in hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/AccountReadableKVState.java

View check run for this annotation

Codecov / codecov/patch

hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/AccountReadableKVState.java#L107

Added line #L107 was not covered by tests
}

private Account accountFromEntity(Entity entity, final Optional<Long> timestamp) {
var tokenAccountBalances = getNumberOfAllAndPositiveBalanceTokenAssociations(entity.getId(), timestamp);

return Account.newBuilder()
.accountId(new AccountID(
entity.getShard(),
entity.getRealm(),
new OneOf<>(AccountOneOfType.ACCOUNT_NUM, entity.getNum())))
.alias(
entity.getEvmAddress() != null && entity.getEvmAddress().length > 0
? Bytes.wrap(entity.getEvmAddress())

Check warning on line 120 in hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/AccountReadableKVState.java

View check run for this annotation

Codecov / codecov/patch

hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/AccountReadableKVState.java#L120

Added line #L120 was not covered by tests
: Bytes.EMPTY)
.key(parseKey(entity))
.expirationSecond(TimeUnit.SECONDS.convert(entity.getEffectiveExpiration(), TimeUnit.NANOSECONDS))
.tinybarBalance(getAccountBalance(entity, timestamp))
.memo(entity.getMemo())
.deleted(Optional.ofNullable(entity.getDeleted()).orElse(false))
.receiverSigRequired(entity.getReceiverSigRequired() != null && entity.getReceiverSigRequired())
.numberOwnedNfts(getOwnedNfts(entity.getId(), timestamp))
.maxAutoAssociations(Optional.ofNullable(entity.getMaxAutomaticTokenAssociations())
.orElse(0))
.numberAssociations(tokenAccountBalances.all())
.smartContract(CONTRACT.equals(entity.getType()))
.numberPositiveBalances(tokenAccountBalances.positive())
.ethereumNonce(entity.getEthereumNonce() != null ? entity.getEthereumNonce() : 0L)
.autoRenewAccountId(new AccountID(
entity.getShard(),
entity.getRealm(),
new OneOf<>(AccountOneOfType.ACCOUNT_NUM, entity.getAutoRenewAccountId())))
.autoRenewSeconds(
entity.getAutoRenewPeriod() != null ? entity.getAutoRenewPeriod() : DEFAULT_AUTO_RENEW_PERIOD)
.cryptoAllowances(getCryptoAllowances(entity.getId(), timestamp))
.approveForAllNftAllowances(getApproveForAllNfts(entity.getId(), timestamp))
.tokenAllowances(getFungibleTokenAllowances(entity.getId(), timestamp))
.expiredAndPendingRemoval(false)
.build();
}

private Long getOwnedNfts(Long accountId, final Optional<Long> timestamp) {
return timestamp
.map(aLong -> nftRepository.countByAccountIdAndTimestampNotDeleted(accountId, aLong))
.orElseGet(() -> nftRepository.countByAccountIdNotDeleted(accountId));
}

/**
* Determines account balance based on block context.
*
* Non-historical Call:
* Get the balance from entity.getBalance()
* Historical Call:
* If the entity creation is after the passed timestamp - return 0L (the entity was not created)
* Else get the balance from the historical query `findHistoricalAccountBalanceUpToTimestamp`
*/
private Long getAccountBalance(Entity entity, final Optional<Long> timestamp) {
if (timestamp.isPresent()) {
Long createdTimestamp = entity.getCreatedTimestamp();
if (createdTimestamp == null || timestamp.get() >= createdTimestamp) {
return accountBalanceRepository
.findHistoricalAccountBalanceUpToTimestamp(entity.getId(), timestamp.get())
.orElse(ZERO_BALANCE);
} else {
return ZERO_BALANCE;
}
}

return entity.getBalance() != null ? entity.getBalance() : ZERO_BALANCE;
}

private List<AccountCryptoAllowance> getCryptoAllowances(Long ownerId, final Optional<Long> timestamp) {
final var cryptoAllowances = timestamp.isPresent()
? cryptoAllowanceRepository.findByOwnerAndTimestamp(ownerId, timestamp.get())
: cryptoAllowanceRepository.findByOwner(ownerId);

return cryptoAllowances.stream().map(this::convertCryptoAllowance).toList();
}

private AccountCryptoAllowance convertCryptoAllowance(final CryptoAllowance cryptoAllowance) {
return new AccountCryptoAllowance(
new com.hedera.hapi.node.base.AccountID(
0L, 0L, new OneOf<>(AccountOneOfType.ACCOUNT_NUM, cryptoAllowance.getSpender())),
cryptoAllowance.getAmount());
}

private List<AccountFungibleTokenAllowance> getFungibleTokenAllowances(
Long ownerId, final Optional<Long> timestamp) {
final var fungibleAllowances = timestamp.isPresent()
? tokenAllowanceRepository.findByOwnerAndTimestamp(ownerId, timestamp.get())
: tokenAllowanceRepository.findByOwner(ownerId);

return fungibleAllowances.stream().map(this::convertFungibleAllowance).toList();
}

private AccountFungibleTokenAllowance convertFungibleAllowance(final TokenAllowance tokenAllowance) {
return new AccountFungibleTokenAllowance(
new TokenID(0L, 0L, tokenAllowance.getTokenId()),
new com.hedera.hapi.node.base.AccountID(
0L, 0L, new OneOf<>(AccountOneOfType.ACCOUNT_NUM, tokenAllowance.getSpender())),
tokenAllowance.getAmount());
}

private List<AccountApprovalForAllAllowance> getApproveForAllNfts(Long ownerId, final Optional<Long> timestamp) {
final var nftAllowances = timestamp.isPresent()
? nftAllowanceRepository.findByOwnerAndTimestampAndApprovedForAllIsTrue(ownerId, timestamp.get())
: nftAllowanceRepository.findByOwnerAndApprovedForAllIsTrue(ownerId);

return nftAllowances.stream().map(this::convertNftAllowance).toList();
}

private AccountApprovalForAllAllowance convertNftAllowance(final NftAllowance nftAllowance) {
return new AccountApprovalForAllAllowance(
new TokenID(0L, 0L, nftAllowance.getTokenId()),
new com.hedera.hapi.node.base.AccountID(
0L, 0L, new OneOf<>(AccountOneOfType.ACCOUNT_NUM, nftAllowance.getSpender())));
}

private TokenAccountBalances getNumberOfAllAndPositiveBalanceTokenAssociations(
long accountId, final Optional<Long> timestamp) {
var counts = timestamp
.map(t -> tokenAccountRepository.countByAccountIdAndTimestampAndAssociatedGroupedByBalanceIsPositive(
accountId, t))
.orElseGet(() ->
tokenAccountRepository.countByAccountIdAndAssociatedGroupedByBalanceIsPositive(accountId));
int all = 0;
int positive = 0;

for (final var count : counts) {
if (count.getIsPositiveBalance()) {
positive = count.getTokenCount();
}
all += count.getTokenCount();
}

final var allAggregated = all;
final var positiveAggregated = positive;

return new TokenAccountBalances(allAggregated, positiveAggregated);
}

private Key parseKey(Entity entity) {
final byte[] keyBytes = entity.getKey();

try {
if (keyBytes != null && keyBytes.length > 0) {
return Key.PROTOBUF.parse(Bytes.wrap(keyBytes));

Check warning on line 253 in hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/AccountReadableKVState.java

View check run for this annotation

Codecov / codecov/patch

hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/AccountReadableKVState.java#L253

Added line #L253 was not covered by tests
}
} catch (final ParseException e) {
log.warning("Failed to parse key for account " + entity.getId());

Check warning on line 256 in hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/AccountReadableKVState.java

View check run for this annotation

Codecov / codecov/patch

hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/AccountReadableKVState.java#L255-L256

Added lines #L255 - L256 were not covered by tests
}

return Key.DEFAULT;
}

private record TokenAccountBalances(int all, int positive) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (C) 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.mirror.web3.state;

import static com.hedera.services.utils.EntityIdUtils.entityIdFromId;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.mirror.common.domain.entity.Entity;
import com.hedera.mirror.common.domain.entity.EntityId;
import com.hedera.mirror.web3.repository.EntityRepository;
import com.hedera.services.store.models.Id;
import jakarta.annotation.Nonnull;
import jakarta.inject.Named;
import java.util.Optional;
import lombok.RequiredArgsConstructor;

@Named
@RequiredArgsConstructor
public class CommonEntityAccessor {
private final EntityRepository entityRepository;

public @Nonnull Optional<Entity> get(@Nonnull AccountID accountID, final Optional<Long> timestamp) {
if (accountID.hasAccountNum()) {
return getEntityByMirrorAddressAndTimestamp(
entityIdFromId(new Id(accountID.shardNum(), accountID.realmNum(), accountID.accountNum())),
timestamp);
} else {
return getEntityByEvmAddressAndTimestamp(accountID.alias().toByteArray(), timestamp);
}
}

private Optional<Entity> getEntityByMirrorAddressAndTimestamp(EntityId entityId, final Optional<Long> timestamp) {
return timestamp
.map(t -> entityRepository.findActiveByIdAndTimestamp(entityId.getId(), t))
.orElseGet(() -> entityRepository.findByIdAndDeletedIsFalse(entityId.getId()));
}

private Optional<Entity> getEntityByEvmAddressAndTimestamp(byte[] addressBytes, final Optional<Long> timestamp) {
return timestamp
.map(t -> entityRepository.findActiveByEvmAddressAndTimestamp(addressBytes, t))
.orElseGet(() -> entityRepository.findByEvmAddressAndDeletedIsFalse(addressBytes));
}
}
Loading