Skip to content

Commit

Permalink
feat: Refactor ContractCallNestedCallsHistoricalTest (#9164)
Browse files Browse the repository at this point in the history
This pr refactors ContractCallNestedCallsHistoricalTest to use the web3j plugin and use native java type.

The class test 3 method getTokenInfo, getApproved and mintNft in historical context.
The historical context is for blocks pre evm 34 version.

This PR modifies:
TestWeb3jService - Adds a new check to execute estimate gas calls only when we are not in historical context since estimate gas does not support historical blocks.

Added NestedCallHistorical.sol file. Modified the 3 tests to not return "hardcoded result" but instead take the response from the Hts call.

Modified ContractCallNestedCallsHistoricalTest to use the new approach with web3j.

---------

Signed-off-by: Kristiyan Selveliev <[email protected]>
  • Loading branch information
kselveliev authored Aug 27, 2024
1 parent 4ae8918 commit 01c881a
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Hedera Hashgraph, LLC
* 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.
Expand All @@ -16,59 +16,262 @@

package com.hedera.mirror.web3.service;

import static com.hedera.mirror.web3.service.model.CallServiceParameters.CallType.ETH_CALL;
import static com.hedera.mirror.common.domain.entity.EntityType.TOKEN;
import static com.hedera.mirror.common.util.DomainUtils.toEvmAddress;
import static com.hedera.mirror.web3.evm.utils.EvmTokenUtils.entityIdFromEvmAddress;
import static com.hedera.mirror.web3.evm.utils.EvmTokenUtils.toAddress;
import static com.hedera.mirror.web3.utils.ContractCallTestUtil.EVM_V_34_BLOCK;
import static com.hedera.mirror.web3.utils.ContractCallTestUtil.KEY_PROTO;
import static com.hedera.node.app.service.evm.utils.EthSigsUtils.recoverAddressFromPubKey;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

import com.google.common.collect.Range;
import com.google.protobuf.ByteString;
import com.hedera.mirror.web3.utils.ContractFunctionProviderEnum;
import com.hedera.mirror.common.domain.entity.EntityId;
import com.hedera.mirror.common.domain.token.TokenFreezeStatusEnum;
import com.hedera.mirror.common.domain.token.TokenPauseStatusEnum;
import com.hedera.mirror.common.domain.token.TokenSupplyTypeEnum;
import com.hedera.mirror.common.domain.token.TokenTypeEnum;
import com.hedera.mirror.common.domain.transaction.RecordFile;
import com.hedera.mirror.web3.viewmodel.BlockType;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import com.hedera.mirror.web3.web3j.generated.NestedCallsHistorical;
import java.math.BigInteger;
import java.util.Collections;
import org.apache.tuweni.bytes.Bytes;
import org.hyperledger.besu.datatypes.Address;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class ContractCallNestedCallsHistoricalTest extends ContractCallTestSetup {
class ContractCallNestedCallsHistoricalTest extends AbstractContractCallServiceTest {

@ParameterizedTest
@EnumSource(NestedEthCallContractFunctionsNegativeCases.class)
void failedNestedCallWithHardcodedResult(final NestedEthCallContractFunctionsNegativeCases func) {
final var functionHash =
functionEncodeDecoder.functionHashFor(func.name, NESTED_CALLS_ABI_PATH, func.functionParameters);
final var serviceParameters = serviceParametersForExecution(
functionHash, NESTED_ETH_CALLS_CONTRACT_ADDRESS, ETH_CALL, 0L, func.block);
private RecordFile recordFileBeforeEvm34;

final var successfulResponse =
functionEncodeDecoder.encodedResultFor(func.name, NESTED_CALLS_ABI_PATH, func.expectedResultFields);
@BeforeEach
void beforeEach() {
recordFileBeforeEvm34 = domainBuilder
.recordFile()
.customize(f -> f.index(EVM_V_34_BLOCK - 1))
.persist();
testWeb3jService.setBlockType(BlockType.of(String.valueOf(EVM_V_34_BLOCK - 1)));
testWeb3jService.setHistoricalRange(
Range.closedOpen(recordFileBeforeEvm34.getConsensusStart(), recordFileBeforeEvm34.getConsensusEnd()));
}

@Test
void testGetHistoricalInfo() throws Exception {
// Given
final var ownerAddress = toAddress(1065);
final var ownerEntityId = ownerEntityPersistHistorical(ownerAddress);
final var spenderAddress = toAddress(1016);
final var spenderPublicKeyHistorical = "3a210398e17bcbd2926c4d8a31e32616b4754ac0a2fc71d7fb768e657db46202625f34";
final var spenderEntityPersist = spenderEntityPersistHistorical(spenderAddress, spenderPublicKeyHistorical);
final var tokenAddress = toAddress(1063);
final var tokenMemo = "TestMemo";
final var nftAmountToMint = 2;
nftPersistHistorical(
tokenAddress,
tokenMemo,
nftAmountToMint,
ownerEntityId,
spenderEntityPersist,
ownerEntityId,
Range.closedOpen(recordFileBeforeEvm34.getConsensusStart(), recordFileBeforeEvm34.getConsensusEnd()));

final var contract = testWeb3jService.deploy(NestedCallsHistorical::deploy);

// When
final var result =
contract.call_nestedGetTokenInfo(tokenAddress.toHexString()).send();
// Then
assertThat(result).isNotNull();
assertThat(result.token).isNotNull();
assertThat(result.deleted).isFalse();
assertThat(result.token.memo).isEqualTo(tokenMemo);
}

@Test
void testGetApprovedHistorical() throws Exception {
// When
final var ownerAddress = toAddress(1065);
final var ownerEntityId = ownerEntityPersistHistorical(ownerAddress);
final var spenderAddress = toAddress(1016);
final var spenderPublicKey = "3a210398e17bcbd2926c4d8a31e32616b4754ac0a2fc71d7fb768e657db46202625f34";
final var spenderEntityPersist = spenderEntityPersistHistorical(spenderAddress, spenderPublicKey);
final var tokenAddress = toAddress(1063);
final var tokenMemo = "TestMemo1";
final var nftAmountToMint = 2;
nftPersistHistorical(
tokenAddress,
tokenMemo,
nftAmountToMint,
ownerEntityId,
spenderEntityPersist,
ownerEntityId,
Range.closedOpen(recordFileBeforeEvm34.getConsensusStart(), recordFileBeforeEvm34.getConsensusEnd()));
final var contract = testWeb3jService.deploy(NestedCallsHistorical::deploy);

// When
final var result = contract.call_nestedHtsGetApproved(tokenAddress.toHexString(), BigInteger.ONE)
.send();

// Then
final var key = ByteString.fromHex(spenderPublicKey);
final var expectedOutput = Address.wrap(
Bytes.wrap(recoverAddressFromPubKey(key.substring(2).toByteArray())))
.toString();
assertThat(result).isEqualTo(expectedOutput);
}

@Test
void testMintTokenHistorical() throws Exception {
// Given
final var ownerAddress = toAddress(1065);
final var ownerEntityId = ownerEntityPersistHistorical(ownerAddress);
final var spenderAddress = toAddress(1016);
final var spenderPublicKeyHistorical = "3a210398e17bcbd2926c4d8a31e32616b4754ac0a2fc71d7fb768e657db46202625f34";
final var spenderEntityPersist = spenderEntityPersistHistorical(spenderAddress, spenderPublicKeyHistorical);
final var tokenAddress = toAddress(1063);
final var tokenMemo = "TestMemo2";
final var nftAmountToMint = 3;
final var nftEntity = nftPersistHistorical(
tokenAddress,
tokenMemo,
nftAmountToMint,
ownerEntityId,
spenderEntityPersist,
ownerEntityId,
Range.closedOpen(recordFileBeforeEvm34.getConsensusStart(), recordFileBeforeEvm34.getConsensusEnd()));

domainBuilder
.tokenAccountHistory()
.customize(e -> e.freezeStatus(TokenFreezeStatusEnum.UNFROZEN)
.accountId(ownerEntityId.getId())
.tokenId(nftEntity.getId())
.timestampRange(Range.closedOpen(
recordFileBeforeEvm34.getConsensusStart(), recordFileBeforeEvm34.getConsensusEnd())))
.persist();

final var contract = testWeb3jService.deploy(NestedCallsHistorical::deploy);

// When
final var result = contract.call_nestedMintToken(
tokenAddress.toHexString(),
BigInteger.ZERO,
Collections.singletonList(
ByteString.copyFromUtf8("firstMeta").toByteArray()))
.send();

// Then
int expectedTotalSupply = nftAmountToMint + 1;
assertThat(result).isEqualTo(BigInteger.valueOf(expectedTotalSupply));
}

private EntityId nftPersistHistorical(
final Address tokenAddress,
final String memo,
final int nftAmountToMint,
final EntityId ownerEntityId,
final EntityId spenderEntityId,
final EntityId treasuryId,
final Range<Long> historicalBlock) {

final var nftEntityId = entityIdFromEvmAddress(tokenAddress);
final var nftEvmAddress = toEvmAddress(nftEntityId);
final var ownerEntity = EntityId.of(ownerEntityId.getId());

domainBuilder
.entity()
.customize(e -> e.id(nftEntityId.getId())
.num(nftEntityId.getNum())
.evmAddress(nftEvmAddress)
.type(TOKEN)
.key(KEY_PROTO)
.expirationTimestamp(9999999999999L)
.memo(memo)
.deleted(false)
.timestampRange(historicalBlock))
.persist();

domainBuilder
.tokenHistory()
.customize(t -> t.tokenId(nftEntityId.getId())
.treasuryAccountId(treasuryId)
.type(TokenTypeEnum.NON_FUNGIBLE_UNIQUE)
.kycKey(KEY_PROTO)
.freezeDefault(true)
.feeScheduleKey(KEY_PROTO)
.maxSupply(2_000_000_000L)
.supplyType(TokenSupplyTypeEnum.FINITE)
.freezeKey(KEY_PROTO)
.pauseKey(KEY_PROTO)
.pauseStatus(TokenPauseStatusEnum.PAUSED)
.wipeKey(KEY_PROTO)
.supplyKey(KEY_PROTO)
.wipeKey(KEY_PROTO)
.decimals(0)
.timestampRange(historicalBlock))
.persist();

for (int i = 0; i < nftAmountToMint; i++) {
int finalI = i;
domainBuilder
.nftHistory()
.customize(n -> n.accountId(spenderEntityId)
.createdTimestamp(1475067194949034022L)
.serialNumber(finalI + 1)
.spender(spenderEntityId)
.metadata("NFT_METADATA_URI".getBytes())
.accountId(ownerEntity)
.tokenId(nftEntityId.getId())
.deleted(false)
.timestampRange(Range.openClosed(
historicalBlock.lowerEndpoint() - 1, historicalBlock.upperEndpoint() + 1)))
.persist();

domainBuilder
.nft()
.customize(n -> n.accountId(spenderEntityId)
.createdTimestamp(1475067194949034022L)
.serialNumber(finalI + 1)
.metadata("NFT_METADATA_URI".getBytes())
.accountId(ownerEntity)
.tokenId(nftEntityId.getId())
.deleted(false)
.timestampRange(Range.atLeast(historicalBlock.upperEndpoint() + 1)))
.persist();
}
return nftEntityId;
}

assertThat(contractCallService.processCall(serviceParameters)).isEqualTo(successfulResponse);
private EntityId ownerEntityPersistHistorical(Address address) {
final var ownerEntityId = entityIdFromEvmAddress(address);
domainBuilder
.entity()
.customize(e -> e.id(ownerEntityId.getId())
.num(ownerEntityId.getNum())
.alias(toEvmAddress(ownerEntityId))
.timestampRange(Range.closedOpen(
recordFileBeforeEvm34.getConsensusStart(), recordFileBeforeEvm34.getConsensusEnd())))
.persist();
return ownerEntityId;
}

@Getter
@RequiredArgsConstructor
enum NestedEthCallContractFunctionsNegativeCases implements ContractFunctionProviderEnum {
GET_TOKEN_INFO_HISTORICAL(
"nestedGetTokenInfoAndHardcodedResult",
new Object[] {NFT_ADDRESS_HISTORICAL},
new Object[] {"hardcodedResult"},
BlockType.of(String.valueOf(EVM_V_34_BLOCK - 1))),
HTS_GET_APPROVED_HISTORICAL(
"nestedHtsGetApprovedAndHardcodedResult",
new Object[] {NFT_ADDRESS_HISTORICAL, 1L},
new Object[] {"hardcodedResult"},
BlockType.of(String.valueOf(EVM_V_34_BLOCK - 1))),
MINT_TOKEN_HISTORICAL(
"nestedMintTokenAndHardcodedResult",
new Object[] {
NFT_ADDRESS_HISTORICAL,
0L,
new byte[][] {ByteString.copyFromUtf8("firstMeta").toByteArray()}
},
new Object[] {"hardcodedResult"},
BlockType.of(String.valueOf(EVM_V_34_BLOCK - 1)));

private final String name;
private final Object[] functionParameters;
private final Object[] expectedResultFields;
private final BlockType block;
private EntityId spenderEntityPersistHistorical(Address spenderAddress, String spenderAlias) {
final var spenderEntityId = entityIdFromEvmAddress(spenderAddress);
final var spenderPublicKeyHistorical = ByteString.fromHex(spenderAlias);
final var spenderAliasHistorical = Address.wrap(Bytes.wrap(
recoverAddressFromPubKey(spenderPublicKeyHistorical.substring(2).toByteArray())));
domainBuilder
.entity()
.customize(e -> e.id(spenderEntityId.getId())
.num(spenderEntityId.getNum())
.evmAddress(spenderAliasHistorical.toArray())
.alias(spenderPublicKeyHistorical.toByteArray())
.deleted(false)
.createdTimestamp(recordFileBeforeEvm34.getConsensusStart())
.timestampRange(Range.closedOpen(
recordFileBeforeEvm34.getConsensusStart(), recordFileBeforeEvm34.getConsensusEnd())))
.persist();
return spenderEntityId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,13 @@ private EthCall ethCall(List<Transaction> reqParams, Request request) {
final var result = contractExecutionService.processCall(serviceParametersForCall);
transactionResult = result;

// Then get the estimated gas
final var serviceParametersForEstimate =
serviceParametersForExecutionSingle(transaction, ETH_ESTIMATE_GAS, blockType);
estimatedGas = contractExecutionService.processCall(serviceParametersForEstimate);
// estimate gas is not supported for historical blocks so we ignore the estimate gas logic in historical context
if (!isHistoricalContext()) {
// Then get the estimated gas
final var serviceParametersForEstimate =
serviceParametersForExecutionSingle(transaction, ETH_ESTIMATE_GAS, blockType);
estimatedGas = contractExecutionService.processCall(serviceParametersForEstimate);
}

final var ethCall = new EthCall();
ethCall.setId(request.getId());
Expand Down Expand Up @@ -383,6 +386,10 @@ private EthGetTransactionReceipt getTransactionReceipt(final Request request) {
return ethTransactionReceipt;
}

private boolean isHistoricalContext() {
return historicalRange != null;
}

public interface Deployer<T extends Contract> {
RemoteCall<T> deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
pragma solidity ^0.8.0;

import "./HederaTokenService.sol";
import "./HederaResponseCodes.sol";

contract NestedCallsHistorical is HederaTokenService {

function nestedGetTokenInfo(address token) external returns (IHederaTokenService.TokenInfo memory) {
(int responseCode, IHederaTokenService.TokenInfo memory retrievedTokenInfo) = HederaTokenService.getTokenInfo(token);
return retrievedTokenInfo;
}

function nestedHtsGetApproved(address token, uint256 serialNumber) public returns (address) {
(int _responseCode, address approved) = HederaTokenService.getApproved(token, serialNumber);
return approved;
}

function nestedMintToken(address token, int64 amount, bytes[] memory metadata) public returns (int64) {
(int responseCode, int64 newTotalSupply, int64[] memory serialNumbers) = HederaTokenService.mintToken(token, amount, metadata);
return newTotalSupply;
}
}

0 comments on commit 01c881a

Please sign in to comment.