diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallNestedCallsHistoricalTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallNestedCallsHistoricalTest.java index 8108c5f4a07..f91e25a73cd 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallNestedCallsHistoricalTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/service/ContractCallNestedCallsHistoricalTest.java @@ -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. @@ -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 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; } } diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/web3j/TestWeb3jService.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/web3j/TestWeb3jService.java index a8640eebf58..99c3c50e117 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/web3j/TestWeb3jService.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/web3j/TestWeb3jService.java @@ -230,10 +230,13 @@ private EthCall ethCall(List 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()); @@ -405,6 +408,10 @@ private EthGetTransactionReceipt getTransactionReceipt(final Request request) { return ethTransactionReceipt; } + private boolean isHistoricalContext() { + return historicalRange != null; + } + public interface Deployer { RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider); } diff --git a/hedera-mirror-web3/src/test/solidity_historical/NestedCallsHistorical.sol b/hedera-mirror-web3/src/test/solidity_historical/NestedCallsHistorical.sol new file mode 100644 index 00000000000..aa5c879f8ce --- /dev/null +++ b/hedera-mirror-web3/src/test/solidity_historical/NestedCallsHistorical.sol @@ -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; + } +}