From 627531943097f9c37ed087518790afbed92d1e65 Mon Sep 17 00:00:00 2001 From: Xin Li <59580070+xin-hedera@users.noreply.github.com> Date: Mon, 7 Feb 2022 17:32:59 -0600 Subject: [PATCH] Support HIP-329 CREATE2 opcode (#3217) * Add evm_address to contract and contract_history tables * Add an EntityIdService service to centralize alias/evm_address lookups * Make RecordItem HAPI version aware and skip persisting contracts from createdContractIDs if HAPI >= 0.23.0 * Populate evm_address for newly created contracts * Rename solidity_address to evm_adress in REST API response Signed-off-by: Xin Li Signed-off-by: Steven Sheehy Co-authored-by: Steven Sheehy --- docs/design/smart-contracts.md | 5 +- .../mirror/common/domain/Aliasable.java | 35 ++ .../common/domain/contract/Contract.java | 21 +- .../mirror/common/domain/entity/Entity.java | 3 +- .../mirror/common/domain/entity/EntityId.java | 4 + .../common/domain/transaction/RecordFile.java | 20 +- .../common/domain/transaction/RecordItem.java | 23 +- .../mirror/common/util/DomainUtils.java | 40 ++ .../mirror/common/domain/DomainBuilder.java | 5 + .../domain/transaction/RecordFileTest.java | 57 +++ .../domain/transaction/RecordItemTest.java | 19 +- .../mirror/common/util/DomainUtilsTest.java | 52 +- .../addressbook/AddressBookServiceImpl.java | 6 +- .../importer/domain/EntityIdService.java | 75 +++ .../importer/domain/EntityIdServiceImpl.java | 182 +++++++ .../migration/ContractResultMigration.java | 2 +- .../NonFeeTransferExtractionStrategyImpl.java | 25 +- .../record/entity/sql/SqlEntityListener.java | 5 + ...bstractContractCallTransactionHandler.java | 47 +- .../ContractCallTransactionHandler.java | 20 +- .../ContractCreateTransactionHandler.java | 19 +- .../ContractDeleteTransactionHandler.java | 24 +- .../ContractUpdateTransactionHandler.java | 20 +- .../SystemDeleteTransactionHandler.java | 12 +- .../SystemUndeleteTransactionHandler.java | 12 +- .../record/AbstractPreV5RecordFileReader.java | 4 +- .../reader/record/RecordFileReaderImplV5.java | 11 +- .../repository/ContractRepository.java | 5 + .../db/migration/v1/V1.54.2__evm_address.sql | 6 + .../db/migration/v2/V2.0.0__create_tables.sql | 1 + .../db/migration/v2/V2.0.3__index_init.sql | 1 + .../db/scripts/v2/csvRestoreTables.sql | 4 +- .../mirror/importer/IntegrationTest.java | 2 +- .../domain/EntityIdServiceImplTest.java | 286 +++++++++++ .../ContractResultMigrationTest.java | 1 + .../parser/domain/RecordItemBuilder.java | 62 ++- ...FeeTransferExtractionStrategyImplTest.java | 81 ++- .../parser/record/RecordFileParserTest.java | 13 +- .../EntityRecordItemListenerContractTest.java | 464 +++++++++++++++--- .../EntityRecordItemListenerCryptoTest.java | 3 +- ...yRecordItemListenerNonFeeTransferTest.java | 34 +- .../entity/sql/SqlEntityListenerTest.java | 4 +- .../pubsub/PubSubRecordItemListenerTest.java | 28 +- .../AbstractTransactionHandlerTest.java | 23 +- .../ContractCallTransactionHandlerTest.java | 31 +- .../ContractCreateTransactionHandlerTest.java | 61 ++- .../ContractDeleteTransactionHandlerTest.java | 37 +- .../ContractUpdateTransactionHandlerTest.java | 30 +- .../SystemDeleteTransactionHandlerTest.java | 11 +- .../SystemUndeleteTransactionHandlerTest.java | 15 +- .../repository/ContractRepositoryTest.java | 18 + .../GenericUpsertQueryGeneratorTest.java | 17 +- .../UpsertQueryGeneratorFactoryTest.java | 6 +- .../controllers/contractController.test.js | 4 +- hedera-mirror-rest/__tests__/entityId.test.js | 8 +- .../__tests__/integrationDomainOps.js | 3 + .../specs/contracts-01-specific-id.spec.json | 2 +- ...ntracts-02-specific-id-timestamp.spec.json | 2 +- ...imestamp-historical-missing-file.spec.json | 2 +- .../specs/contracts-09-no-args.spec.json | 6 +- .../specs/contracts-10-order-asc.spec.json | 6 +- .../specs/contracts-11-limit.spec.json | 5 +- .../specs/contracts-12-contract-id.spec.json | 2 +- ...ntracts-13-multiple-contract-ids.spec.json | 4 +- .../specs/contracts-14-all-params.spec.json | 4 +- .../viewmodel/contractViewModel.test.js | 18 +- hedera-mirror-rest/api/v1/openapi.yml | 55 ++- .../controllers/contractController.js | 1 + hedera-mirror-rest/entityId.js | 22 +- hedera-mirror-rest/model/contract.js | 1 + hedera-mirror-rest/utils.js | 2 +- .../viewmodel/contractResultLogViewModel.js | 2 +- .../contractResultStateChangeViewModel.js | 2 +- .../viewmodel/contractResultViewModel.js | 4 +- .../viewmodel/contractViewModel.js | 33 +- 75 files changed, 1864 insertions(+), 316 deletions(-) create mode 100644 hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/Aliasable.java create mode 100644 hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/transaction/RecordFileTest.java create mode 100644 hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdService.java create mode 100644 hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdServiceImpl.java create mode 100644 hedera-mirror-importer/src/main/resources/db/migration/v1/V1.54.2__evm_address.sql create mode 100644 hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/EntityIdServiceImplTest.java diff --git a/docs/design/smart-contracts.md b/docs/design/smart-contracts.md index f7f12aca392..04dc2a3ea78 100644 --- a/docs/design/smart-contracts.md +++ b/docs/design/smart-contracts.md @@ -37,6 +37,7 @@ create table if not exists contract auto_renew_period bigint null, created_timestamp bigint null, deleted boolean null, + evm_address bytea null, expiration_timestamp bigint null, file_id bigint null, id bigint not null, @@ -192,11 +193,11 @@ create table if not exists contract_state_change "_type": "ProtobufEncoded", "key": "7b2233222c2233222c2233227d" }, - "address": "0x0000000000000000000000000000000000001001", "auto_renew_period": 7776000, "contract_id": "0.0.10001", "created_timestamp": "1633466568.31556926", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001001", "expiration_timestamp": null, "file_id": 1000, "memo": "First contract", @@ -230,12 +231,12 @@ Optional filters "_type": "ProtobufEncoded", "key": "7b2233222c2233222c2233227d" }, - "address": "0x0000000000000000000000000000000000001001", "auto_renew_period": 7776000, "bytecode": "0xc896c66db6d98784cc03807640f3dfd41ac3a48c", "contract_id": "0.0.10001", "created_timestamp": "1633466229.96874612", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001001", "expiration_timestamp": null, "file_id": "0.0.1000", "memo": "First contract", diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/Aliasable.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/Aliasable.java new file mode 100644 index 00000000000..30d89bc5af8 --- /dev/null +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/Aliasable.java @@ -0,0 +1,35 @@ +package com.hedera.mirror.common.domain; + +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 - 2022 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. + * ‍ + */ + +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.entity.EntityType; + +public interface Aliasable { + + byte[] getAlias(); + + Boolean getDeleted(); + + EntityType getType(); + + EntityId toEntityId(); +} diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/contract/Contract.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/contract/Contract.java index 71f1e286fc9..4e0437ab721 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/contract/Contract.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/contract/Contract.java @@ -20,24 +20,29 @@ * ‍ */ +import com.fasterxml.jackson.annotation.JsonIgnore; import javax.persistence.Column; import javax.persistence.Convert; - -import com.hedera.mirror.common.domain.entity.AbstractEntity; -import com.hedera.mirror.common.domain.entity.EntityId; - import lombok.Data; import lombok.NoArgsConstructor; +import lombok.ToString; import lombok.experimental.SuperBuilder; import com.hedera.mirror.common.converter.FileIdConverter; import com.hedera.mirror.common.converter.UnknownIdConverter; +import com.hedera.mirror.common.domain.Aliasable; +import com.hedera.mirror.common.domain.entity.AbstractEntity; +import com.hedera.mirror.common.domain.entity.EntityId; @Data @javax.persistence.Entity @NoArgsConstructor @SuperBuilder -public class Contract extends AbstractEntity { +public class Contract extends AbstractEntity implements Aliasable { + + @Column(updatable = false) + @ToString.Exclude + private byte[] evmAddress; @Column(updatable = false) @Convert(converter = FileIdConverter.class) @@ -45,4 +50,10 @@ public class Contract extends AbstractEntity { @Convert(converter = UnknownIdConverter.class) private EntityId obtainerId; + + @JsonIgnore + @Override + public byte[] getAlias() { + return evmAddress; + } } diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/Entity.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/Entity.java index 5337b9424e0..20849444aa4 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/Entity.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/Entity.java @@ -28,12 +28,13 @@ import lombok.experimental.SuperBuilder; import com.hedera.mirror.common.converter.AccountIdConverter; +import com.hedera.mirror.common.domain.Aliasable; @Data @javax.persistence.Entity @NoArgsConstructor @SuperBuilder -public class Entity extends AbstractEntity { +public class Entity extends AbstractEntity implements Aliasable { @Column(updatable = false) @ToString.Exclude diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/EntityId.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/EntityId.java index 20dc0856f70..dc90d0dec02 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/EntityId.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/EntityId.java @@ -81,6 +81,10 @@ public static EntityId of(AccountID accountID) { return of(accountID.getShardNum(), accountID.getRealmNum(), accountID.getAccountNum(), EntityType.ACCOUNT); } + /** + * @deprecated in favor of using EntityIdService.lookup where applicable + */ + @Deprecated(since = "v0.50.0") public static EntityId of(ContractID contractID) { return of(contractID.getShardNum(), contractID.getRealmNum(), contractID.getContractNum(), EntityType.CONTRACT); diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/RecordFile.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/RecordFile.java index 8704fafb508..adcaec69c1e 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/RecordFile.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/RecordFile.java @@ -26,15 +26,14 @@ import javax.persistence.Enumerated; import javax.persistence.Id; import javax.persistence.Transient; - -import com.hedera.mirror.common.domain.StreamItem; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; +import org.springframework.data.util.Version; import reactor.core.publisher.Flux; import com.hedera.mirror.common.converter.AccountIdConverter; @@ -50,6 +49,13 @@ @NoArgsConstructor public class RecordFile implements StreamFile { + public static final Version HAPI_VERSION_NOT_SET = new Version(0, 0, 0); + public static final Version HAPI_VERSION_0_23_0 = new Version(0, 23, 0); + + @Getter(lazy = true) + @Transient + private final Version hapiVersion = hapiVersion(); + @ToString.Exclude private byte[] bytes; @@ -105,4 +111,12 @@ public class RecordFile implements StreamFile { public StreamType getType() { return StreamType.RECORD; } + + private Version hapiVersion() { + if (hapiVersionMajor == null || hapiVersionMinor == null || hapiVersionPatch == null) { + return HAPI_VERSION_NOT_SET; + } + + return new Version(hapiVersionMajor, hapiVersionMinor, hapiVersionPatch); + } } diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/RecordItem.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/RecordItem.java index 978d72979b0..c4e89dc5337 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/RecordItem.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/RecordItem.java @@ -22,9 +22,6 @@ import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; - -import com.hedera.mirror.common.exception.ProtobufException; - import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.SignatureMap; import com.hederahashgraph.api.proto.java.SignedTransaction; @@ -36,9 +33,11 @@ import lombok.Getter; import lombok.Value; import lombok.extern.log4j.Log4j2; +import org.springframework.data.util.Version; import com.hedera.mirror.common.domain.StreamItem; import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.exception.ProtobufException; import com.hedera.mirror.common.util.DomainUtils; @Log4j2 @@ -48,6 +47,7 @@ public class RecordItem implements StreamItem { static final String BAD_RECORD_BYTES_MESSAGE = "Failed to parse record bytes"; static final String BAD_TRANSACTION_BODY_BYTES_MESSAGE = "Error parsing transactionBody from transaction"; + private final Version hapiVersion; private final Transaction transaction; private final TransactionBodyAndSignatureMap transactionBodyAndSignatureMap; private final TransactionRecord record; @@ -67,7 +67,7 @@ public class RecordItem implements StreamItem { /** * Constructs RecordItem from serialized transactionBytes and recordBytes. */ - public RecordItem(byte[] transactionBytes, byte[] recordBytes) { + public RecordItem(Version hapiVersion, byte[] transactionBytes, byte[] recordBytes) { try { transaction = Transaction.parseFrom(transactionBytes); } catch (InvalidProtocolBufferException e) { @@ -80,6 +80,8 @@ record = TransactionRecord.parseFrom(recordBytes); } transactionBodyAndSignatureMap = parseTransactionBodyAndSignatureMap(transaction); transactionType = getTransactionType(transactionBodyAndSignatureMap.getTransactionBody()); + + this.hapiVersion = hapiVersion; this.transactionBytes = transactionBytes; this.recordBytes = recordBytes; } @@ -87,15 +89,22 @@ record = TransactionRecord.parseFrom(recordBytes); // Used only in tests // There are many brittle RecordItemParser*Tests which rely on bytes being null. Those tests need to be fixed, // then this function can be removed. - public RecordItem(Transaction transaction, TransactionRecord record) { + public RecordItem(Version hapiVersion, Transaction transaction, TransactionRecord record) { Objects.requireNonNull(transaction, "transaction is required"); Objects.requireNonNull(record, "record is required"); + + this.hapiVersion = hapiVersion; this.transaction = transaction; transactionBodyAndSignatureMap = parseTransactionBodyAndSignatureMap(transaction); transactionType = getTransactionType(transactionBodyAndSignatureMap.getTransactionBody()); this.record = record; - transactionBytes = null; - recordBytes = null; + transactionBytes = transaction.toByteArray(); + recordBytes = record.toByteArray(); + } + + // Used only in tests, default hapiVersion to RecordFile.HAPI_VERSION_NOT_SET + public RecordItem(Transaction transaction, TransactionRecord record) { + this(RecordFile.HAPI_VERSION_NOT_SET, transaction, record); } private static TransactionBodyAndSignatureMap parseTransactionBodyAndSignatureMap(Transaction transaction) { diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/util/DomainUtils.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/util/DomainUtils.java index 8320950867a..4a53f00748c 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/util/DomainUtils.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/util/DomainUtils.java @@ -20,6 +20,8 @@ * ‍ */ +import static com.hedera.mirror.common.domain.entity.EntityType.CONTRACT; + import com.google.protobuf.ByteOutput; import com.google.protobuf.ByteString; import com.google.protobuf.UnsafeByteOperations; @@ -37,10 +39,14 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.exception.InvalidEntityException; + @Log4j2 @UtilityClass public class DomainUtils { + private static final int EVM_ADDRESS_LENGTH = 20; private static final long NANOS_PER_SECOND = 1_000_000_000L; private static final char NULL_CHARACTER = (char) 0; private static final char NULL_REPLACEMENT = '�'; // Standard replacement character 0xFFFD @@ -216,6 +222,40 @@ public static byte[] toBytes(ByteString byteString) { return byteString.toByteArray(); } + public static ByteString fromBytes(byte[] bytes) { + if (bytes == null) { + return null; + } + + return UnsafeByteOperations.unsafeWrap(bytes); + } + + // The 'shard.realm.num' form evm address has 4 bytes for shard, and 8 bytes each for realm and num. + public static EntityId fromEvmAddress(byte[] evmAddress) { + try { + if (evmAddress != null && evmAddress.length == EVM_ADDRESS_LENGTH) { + ByteBuffer buffer = ByteBuffer.wrap(evmAddress); + return EntityId.of(buffer.getInt(), buffer.getLong(), buffer.getLong(), CONTRACT); + } + } catch (InvalidEntityException ex) { + log.debug("Failed to parse shard.realm.num form evm address into EntityId", ex); + } + return null; + } + + public static byte[] toEvmAddress(EntityId contractId) { + if (EntityId.isEmpty(contractId)) { + throw new InvalidEntityException("Empty contractId"); + } + + byte[] evmAddress = new byte[EVM_ADDRESS_LENGTH]; + ByteBuffer buffer = ByteBuffer.wrap(evmAddress); + buffer.putInt(contractId.getShardNum().intValue()); + buffer.putLong(contractId.getRealmNum()); + buffer.putLong(contractId.getEntityNum()); + return evmAddress; + } + static class UnsafeByteOutput extends ByteOutput { static final short SIZE = 12 + 4; // Size of the object header plus a compressed object reference to bytes field diff --git a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/DomainBuilder.java b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/DomainBuilder.java index abc3c38e745..ed5fca4594d 100644 --- a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/DomainBuilder.java +++ b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/DomainBuilder.java @@ -156,6 +156,7 @@ public DomainWrapper contract() { .autoRenewPeriod(1800L) .createdTimestamp(timestamp) .deleted(false) + .evmAddress(create2EvmAddress()) .expirationTimestamp(timestamp + 30_000_000L) .fileId(entityId(FILE)) .id(id) @@ -338,6 +339,10 @@ public byte[] bytes(int length) { return bytes; } + public byte[] create2EvmAddress() { + return bytes(20); + } + public EntityId entityId(EntityType type) { return EntityId.of(0L, 0L, id(), type); } diff --git a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/transaction/RecordFileTest.java b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/transaction/RecordFileTest.java new file mode 100644 index 00000000000..50fcfef1371 --- /dev/null +++ b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/transaction/RecordFileTest.java @@ -0,0 +1,57 @@ +package com.hedera.mirror.common.domain.transaction; + +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 - 2022 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. + * ‍ + */ + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.data.util.Version; + +class RecordFileTest { + + @Test + void testHapiVersion() { + RecordFile recordFile = RecordFile.builder() + .hapiVersionMajor(1) + .hapiVersionMinor(23) + .hapiVersionPatch(1) + .build(); + assertThat(recordFile.getHapiVersion()).isEqualTo(new Version(1, 23, 1)); + } + + @ParameterizedTest + @CsvSource(value = { + ",,", + ",1,1", + "1,,1", + "1,1,", + }) + void testHapiVersionNotSet(Integer major, Integer minor, Integer patch) { + RecordFile recordFile = RecordFile.builder() + .hapiVersionMajor(major) + .hapiVersionMinor(minor) + .hapiVersionPatch(patch) + .build(); + assertThat(recordFile.getHapiVersion()).isEqualTo(RecordFile.HAPI_VERSION_NOT_SET); + } +} diff --git a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/transaction/RecordItemTest.java b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/transaction/RecordItemTest.java index d05904fce3c..bb2fe7a36aa 100644 --- a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/transaction/RecordItemTest.java +++ b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/transaction/RecordItemTest.java @@ -24,9 +24,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.google.protobuf.ByteString; - -import com.hedera.mirror.common.exception.ProtobufException; - import com.hederahashgraph.api.proto.java.CryptoTransferTransactionBody; import com.hederahashgraph.api.proto.java.SignatureMap; import com.hederahashgraph.api.proto.java.SignaturePair; @@ -38,9 +35,13 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.junit.jupiter.api.Test; +import org.springframework.data.util.Version; + +import com.hedera.mirror.common.exception.ProtobufException; class RecordItemTest { + private static final Version DEFAULT_HAPI_VERSION = new Version(0, 22, 0); private static final Transaction DEFAULT_TRANSACTION = Transaction.newBuilder() .setSignedTransactionBytes(SignedTransaction.getDefaultInstance().getBodyBytes()) .build(); @@ -94,7 +95,7 @@ void testWithBody() { .setBody(TRANSACTION_BODY) .setSigMap(SIGNATURE_MAP) .build(); - RecordItem recordItem = new RecordItem(transaction.toByteArray(), TRANSACTION_RECORD.toByteArray()); + RecordItem recordItem = new RecordItem(DEFAULT_HAPI_VERSION, transaction, TRANSACTION_RECORD); assertRecordItem(transaction, recordItem); } @@ -109,7 +110,8 @@ void testWithBodyProto() { .setSigMap(SIGNATURE_MAP) .build(); - RecordItem recordItem = new RecordItem(transactionFromProto, TRANSACTION_RECORD.toByteArray()); + RecordItem recordItem = new RecordItem(DEFAULT_HAPI_VERSION, transactionFromProto, + TRANSACTION_RECORD.toByteArray()); assertRecordItem(expectedTransaction, recordItem); } @@ -119,7 +121,7 @@ void testWithBodyBytes() { .setBodyBytes(TRANSACTION_BODY.toByteString()) .setSigMap(SIGNATURE_MAP) .build(); - RecordItem recordItem = new RecordItem(transaction.toByteArray(), TRANSACTION_RECORD.toByteArray()); + RecordItem recordItem = new RecordItem(DEFAULT_HAPI_VERSION, transaction, TRANSACTION_RECORD); assertRecordItem(transaction, recordItem); } @@ -128,7 +130,7 @@ void testWithSignedTransaction() { Transaction transaction = Transaction.newBuilder() .setSignedTransactionBytes(SIGNED_TRANSACTION.toByteString()) .build(); - RecordItem recordItem = new RecordItem(transaction.toByteArray(), TRANSACTION_RECORD.toByteArray()); + RecordItem recordItem = new RecordItem(DEFAULT_HAPI_VERSION, transaction, TRANSACTION_RECORD); assertRecordItem(transaction, recordItem); } @@ -150,12 +152,13 @@ void unknownTransactionType() throws Exception { } private void testException(byte[] transactionBytes, byte[] recordBytes, String expectedMessage) { - assertThatThrownBy(() -> new RecordItem(transactionBytes, recordBytes)) + assertThatThrownBy(() -> new RecordItem(DEFAULT_HAPI_VERSION, transactionBytes, recordBytes)) .isInstanceOf(ProtobufException.class) .hasMessage(expectedMessage); } private void assertRecordItem(Transaction transaction, RecordItem recordItem) { + assertThat(recordItem.getHapiVersion()).isEqualTo(DEFAULT_HAPI_VERSION); assertThat(recordItem.getTransaction()).isEqualTo(transaction); assertThat(recordItem.getRecord()).isEqualTo(TRANSACTION_RECORD); assertThat(recordItem.getTransactionBody()).isEqualTo(TRANSACTION_BODY); diff --git a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/util/DomainUtilsTest.java b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/util/DomainUtilsTest.java index f175d580a8a..bd164eeb6d3 100644 --- a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/util/DomainUtilsTest.java +++ b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/util/DomainUtilsTest.java @@ -39,6 +39,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.entity.EntityType; +import com.hedera.mirror.common.exception.InvalidEntityException; + class DomainUtilsTest { private static final String KEY = "c83755a935e442f18f12fbb9ecb5bc416417059ddb3c15aac32c1702e7da6734"; @@ -246,6 +250,53 @@ void toBytes() { .isNotSameAs(largeArray); } + @Test + void fromBytes() { + byte[] bytes = RandomUtils.nextBytes(16); + + assertThat(DomainUtils.fromBytes(null)).isNull(); + assertThat(DomainUtils.fromBytes(new byte[0])).isEqualTo(ByteString.EMPTY); + assertThat(DomainUtils.fromBytes(bytes).toByteArray()).isEqualTo(bytes); + } + + @Test + void fromEvmAddress() { + long shard = 1; + long realm = 2; + long num = 255; + byte[] evmAddress = new byte[20]; + evmAddress[3] = (byte) shard; + evmAddress[11] = (byte) realm; + evmAddress[19] = (byte) num; + EntityId expected = EntityId.of(shard, realm, num, EntityType.CONTRACT); + assertThat(DomainUtils.fromEvmAddress(evmAddress)).isEqualTo(expected); + + evmAddress[0] = (byte) 255; + evmAddress[4] = (byte) 255; + evmAddress[12] = (byte) 255; + // can't be encoded to long + assertThat(DomainUtils.fromEvmAddress(evmAddress)).isNull(); + } + + @Test + void fromEvmAddressIncorrectSize() { + assertNull(DomainUtils.fromEvmAddress(null)); + assertNull(DomainUtils.fromEvmAddress(new byte[10])); + } + + @Test + void toEvmAddress() { + EntityId contractId = EntityId.of(1, 2, 255, EntityType.CONTRACT); + String expected = "00000001000000000000000200000000000000FF"; + assertThat(DomainUtils.toEvmAddress(contractId)).asHexString().isEqualTo(expected); + } + + @Test + void toEvmAddressThrows() { + assertThrows(InvalidEntityException.class, () -> DomainUtils.toEvmAddress(null)); + assertThrows(InvalidEntityException.class, () -> DomainUtils.toEvmAddress(EntityId.EMPTY)); + } + @Test void bytesToHex() { assertThat(DomainUtils.bytesToHex(new byte[] {1})).isEqualTo("01"); @@ -257,4 +308,3 @@ void bytesToHex() { assertThat(DomainUtils.bytesToHex(null)).isNull(); } } - diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/addressbook/AddressBookServiceImpl.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/addressbook/AddressBookServiceImpl.java index 1e0d7bf3939..3a37f50f5fe 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/addressbook/AddressBookServiceImpl.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/addressbook/AddressBookServiceImpl.java @@ -23,10 +23,6 @@ import static com.hedera.mirror.importer.addressbook.AddressBookServiceImpl.ADDRESS_BOOK_102_CACHE_NAME; import static com.hedera.mirror.importer.config.CacheConfiguration.EXPIRE_AFTER_5M; -import com.hedera.mirror.common.domain.file.FileData; - -import com.hedera.mirror.common.util.DomainUtils; - import com.hederahashgraph.api.proto.java.NodeAddress; import com.hederahashgraph.api.proto.java.NodeAddressBook; import com.hederahashgraph.api.proto.java.ServiceEndpoint; @@ -64,11 +60,11 @@ import com.hedera.mirror.common.domain.entity.EntityType; import com.hedera.mirror.common.domain.file.FileData; import com.hedera.mirror.common.domain.transaction.TransactionType; +import com.hedera.mirror.common.util.DomainUtils; import com.hedera.mirror.importer.MirrorProperties; import com.hedera.mirror.importer.exception.InvalidDatasetException; import com.hedera.mirror.importer.repository.AddressBookRepository; import com.hedera.mirror.importer.repository.FileDataRepository; -import com.hedera.mirror.importer.util.Utility; @Log4j2 @Named diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdService.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdService.java new file mode 100644 index 00000000000..09211c49494 --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdService.java @@ -0,0 +1,75 @@ +package com.hedera.mirror.importer.domain; + +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 - 2022 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. + * ‍ + */ + +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.ContractID; + +import com.hedera.mirror.common.domain.Aliasable; +import com.hedera.mirror.common.domain.entity.EntityId; + +/** + * This service is used to centralize the conversion logic from protobuf-based Hedera entities to its internal EntityId + * representation. Lookup methods encapsulate caching and alias resolution. + */ +public interface EntityIdService { + + /** + * Converts a protobuf AccountID to an EntityID, resolving any aliases that may be present. + * + * @param accountId The protobuf account ID + * @return The converted EntityId or EntityId.EMPTY if not resolved + */ + EntityId lookup(AccountID accountId); + + /** + * Specialized form of lookup(AccountID) that returns the first account ID parameter that resolves to a non-empty + * EntityId. + * + * @param accountIds The protobuf account IDs + * @return The converted EntityId or EntityId.EMPTY if none can be resolved + */ + EntityId lookup(AccountID... accountIds); + + /** + * Converts a protobuf ContractID to an EntityID, resolving any EVM addresses that may be present. + * + * @param contractId The protobuf contract ID + * @return The converted EntityId or EntityId.EMPTY if not resolved + */ + EntityId lookup(ContractID contractId); + + /** + * Specialized form of lookup(ContractID) that returns the first contract ID parameter that resolves to a non-empty + * EntityId. + * + * @param contractIds The protobuf contract IDs + * @return The converted EntityId or EntityId.EMPTY if none can be resolved + */ + EntityId lookup(ContractID... contractIds); + + /** + * Used to notify the system of new aliases for potential use in future lookups. + * + * @param aliasable Represents a mapping of alias to entity ID. + */ + void notify(Aliasable aliasable); +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdServiceImpl.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdServiceImpl.java new file mode 100644 index 00000000000..85024d6ba08 --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/domain/EntityIdServiceImpl.java @@ -0,0 +1,182 @@ +package com.hedera.mirror.importer.domain; + +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 - 2022 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. + * ‍ + */ + +import static com.hedera.mirror.common.domain.entity.EntityType.ACCOUNT; +import static com.hedera.mirror.common.domain.entity.EntityType.CONTRACT; +import static com.hedera.mirror.importer.config.CacheConfiguration.CACHE_MANAGER_ALIAS; + +import com.google.protobuf.ByteString; +import com.google.protobuf.GeneratedMessageV3; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.ContractID; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.function.Function; +import javax.inject.Named; +import lombok.extern.log4j.Log4j2; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import com.hedera.mirror.common.domain.Aliasable; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.entity.EntityType; +import com.hedera.mirror.common.exception.InvalidEntityException; +import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.exception.InvalidDatasetException; +import com.hedera.mirror.importer.repository.ContractRepository; +import com.hedera.mirror.importer.repository.EntityRepository; + +@Log4j2 +@Named +public class EntityIdServiceImpl implements EntityIdService { + + private final Cache cache; + private final ContractRepository contractRepository; + private final EntityRepository entityRepository; + + public EntityIdServiceImpl(@Named(CACHE_MANAGER_ALIAS) CacheManager cacheManager, + ContractRepository contractRepository, EntityRepository entityRepository) { + this.cache = cacheManager.getCache("entityId"); + this.contractRepository = contractRepository; + this.entityRepository = entityRepository; + } + + @Override + public EntityId lookup(AccountID accountId) { + return doLookup(accountId, () -> load(accountId)); + } + + @Override + public EntityId lookup(AccountID... accountIds) { + return doLookups(accountIds, this::load); + } + + @Override + public EntityId lookup(ContractID contractId) { + return doLookup(contractId, () -> load(contractId)); + } + + @Override + public EntityId lookup(ContractID... contractIds) { + return doLookups(contractIds, this::load); + } + + private EntityId doLookup(GeneratedMessageV3 entityIdProto, Callable loader) { + if (entityIdProto == null || entityIdProto.equals(entityIdProto.getDefaultInstanceForType())) { + return EntityId.EMPTY; + } + + EntityId entityId = cache.get(entityIdProto.hashCode(), loader); + + if (entityId == null) { + log.warn("No match found. It could be that the mirror node has partial data " + + "or the alias doesn't exist: {}", entityIdProto); + return EntityId.EMPTY; + } + + return entityId; + } + + private EntityId doLookups(T[] entityIdProtos, Function loader) { + for (T entityIdProto : entityIdProtos) { + try { + EntityId entityId = doLookup(entityIdProto, () -> loader.apply(entityIdProto)); + if (!EntityId.isEmpty(entityId)) { + return entityId; + } + } catch (Exception e) { + log.warn("Skipping entity ID {}: {}", entityIdProto, e.getMessage()); + } + } + return EntityId.EMPTY; + } + + @Override + public void notify(Aliasable aliasable) { + if (aliasable == null || (aliasable.getDeleted() != null && aliasable.getDeleted())) { + return; + } + + ByteString alias = DomainUtils.fromBytes(aliasable.getAlias()); + if (alias == null) { + return; + } + + EntityId entityId = aliasable.toEntityId(); + EntityType type = aliasable.getType(); + GeneratedMessageV3.Builder builder; + + switch (type) { + case ACCOUNT: + builder = AccountID.newBuilder() + .setShardNum(entityId.getShardNum()) + .setRealmNum(entityId.getRealmNum()) + .setAlias(alias); + break; + case CONTRACT: + builder = ContractID.newBuilder() + .setShardNum(entityId.getShardNum()) + .setRealmNum(entityId.getRealmNum()) + .setEvmAddress(alias); + break; + default: + throw new InvalidEntityException(String.format("%s entity can't have alias", type)); + } + + cache.put(builder.build().hashCode(), entityId); + } + + private EntityId load(AccountID accountId) { + switch (accountId.getAccountCase()) { + case ACCOUNTNUM: + return EntityId.of(accountId); + case ALIAS: + byte[] alias = DomainUtils.toBytes(accountId.getAlias()); + return entityRepository.findByAlias(alias) + .map(id -> EntityId.of(id, ACCOUNT)) + .orElse(null); + default: + throw new InvalidDatasetException("Invalid AccountID: " + accountId); + } + } + + @SuppressWarnings("deprecation") + private EntityId load(ContractID contractId) { + switch (contractId.getContractCase()) { + case CONTRACTNUM: + return EntityId.of(contractId); + case EVM_ADDRESS: + return findByEvmAddress(contractId); + default: + throw new InvalidDatasetException("Invalid ContractID: " + contractId); + } + } + + private EntityId findByEvmAddress(ContractID contractId) { + byte[] evmAddress = DomainUtils.toBytes(contractId.getEvmAddress()); + return Optional.ofNullable(DomainUtils.fromEvmAddress(evmAddress)) + // Verify shard and realm match when assuming evmAddress is in the 'shard.realm.num' form + .filter(e -> e.getShardNum() == contractId.getShardNum() && e.getRealmNum() == contractId.getRealmNum()) + .or(() -> contractRepository.findByEvmAddress(evmAddress).map(id -> EntityId.of(id, CONTRACT))) + .orElse(null); + } +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/migration/ContractResultMigration.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/migration/ContractResultMigration.java index 0c557b953a4..967e2014c6a 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/migration/ContractResultMigration.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/migration/ContractResultMigration.java @@ -119,7 +119,6 @@ private boolean process(MigrationContractResult contractResult) { for (int index = 0; index < contractFunctionResult.getLogInfoCount(); ++index) { ContractLoginfo contractLoginfo = contractFunctionResult.getLogInfo(index); - List topics = contractLoginfo.getTopicList(); MigrationContractLog migrationContractLog = new MigrationContractLog(); migrationContractLog.setBloom(DomainUtils.toBytes(contractLoginfo.getBloom())); @@ -143,6 +142,7 @@ private boolean process(MigrationContractResult contractResult) { return false; } + @SuppressWarnings("deprecation") private Long getContractId(ContractID contractID) { EntityId entityId = EntityId.of(contractID); return !EntityId.isEmpty(entityId) ? entityId.getId() : null; diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/NonFeeTransferExtractionStrategyImpl.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/NonFeeTransferExtractionStrategyImpl.java index b2a5cd3fbb5..3bb342331d6 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/NonFeeTransferExtractionStrategyImpl.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/NonFeeTransferExtractionStrategyImpl.java @@ -28,14 +28,22 @@ import com.hederahashgraph.api.proto.java.TransactionRecord; import java.util.Collections; import java.util.LinkedList; -import org.springframework.stereotype.Component; +import javax.inject.Named; +import lombok.RequiredArgsConstructor; + +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.importer.domain.EntityIdService; /** * Non-fee transfers are explicitly requested transfers. This implementation extracts non_fee_transfer requested by a * transaction into an iterable of transfers. */ -@Component +@Named +@RequiredArgsConstructor public class NonFeeTransferExtractionStrategyImpl implements NonFeeTransferExtractionStrategy { + + private final EntityIdService entityIdService; + /** * @return iterable of transfers. If transaction has no non-fee transfers, then iterable will have no elements. */ @@ -57,11 +65,18 @@ public Iterable extractNonFeeTransfers(TransactionBody body, Tran return extractForCreateEntity(body.getContractCreateInstance().getInitialBalance(), payerAccountId, contractIdToAccountId(transactionRecord.getReceipt().getContractID()), transactionRecord); } else { // contractCall + EntityId contractId = entityIdService.lookup(transactionRecord.getReceipt().getContractID(), + body.getContractCall().getContractID()); LinkedList result = new LinkedList<>(); var amount = body.getContractCall().getAmount(); - var contractAccountId = contractIdToAccountId(body.getContractCall().getContractID()); + + var contractAccountId = AccountID.newBuilder() + .setShardNum(contractId.getShardNum()) + .setRealmNum(contractId.getRealmNum()) + .setAccountNum(contractId.getEntityNum()) + .build(); result.add(AccountAmount.newBuilder().setAccountID(contractAccountId).setAmount(amount).build()); - result.add(AccountAmount.newBuilder().setAccountID(payerAccountId).setAmount(0 - amount).build()); + result.add(AccountAmount.newBuilder().setAccountID(payerAccountId).setAmount(-amount).build()); return result; } } @@ -69,7 +84,7 @@ public Iterable extractNonFeeTransfers(TransactionBody body, Tran private Iterable extractForCreateEntity( long initialBalance, AccountID payerAccountId, AccountID createdEntity, TransactionRecord txRecord) { LinkedList result = new LinkedList<>(); - result.add(AccountAmount.newBuilder().setAccountID(payerAccountId).setAmount(0 - initialBalance).build()); + result.add(AccountAmount.newBuilder().setAccountID(payerAccountId).setAmount(-initialBalance).build()); if (ResponseCodeEnum.SUCCESS == txRecord.getReceipt().getStatus()) { result.add(AccountAmount.newBuilder().setAccountID(createdEntity).setAmount(initialBalance).build()); } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/sql/SqlEntityListener.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/sql/SqlEntityListener.java index 265b409cb41..0536198a862 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/sql/SqlEntityListener.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/sql/SqlEntityListener.java @@ -59,6 +59,7 @@ import com.hedera.mirror.common.domain.transaction.RecordFile; import com.hedera.mirror.common.domain.transaction.Transaction; import com.hedera.mirror.common.domain.transaction.TransactionSignature; +import com.hedera.mirror.importer.domain.EntityIdService; import com.hedera.mirror.importer.exception.ImporterException; import com.hedera.mirror.importer.exception.ParserException; import com.hedera.mirror.importer.parser.batch.BatchPersister; @@ -76,6 +77,7 @@ public class SqlEntityListener implements EntityListener, RecordStreamFileListener { private final BatchPersister batchPersister; + private final EntityIdService entityIdService; private final ApplicationEventPublisher eventPublisher; private final RecordFileRepository recordFileRepository; private final SqlProperties sqlProperties; @@ -115,11 +117,13 @@ public class SqlEntityListener implements EntityListener, RecordStreamFileListen private final Map tokenAccountState; public SqlEntityListener(BatchPersister batchPersister, + EntityIdService entityIdService, ApplicationEventPublisher eventPublisher, RecordFileRepository recordFileRepository, SqlProperties sqlProperties, @Qualifier(TOKEN_DISSOCIATE_BATCH_PERSISTER) BatchPersister tokenDissociateTransferBatchPersister) { this.batchPersister = batchPersister; + this.entityIdService = entityIdService; this.eventPublisher = eventPublisher; this.recordFileRepository = recordFileRepository; this.sqlProperties = sqlProperties; @@ -259,6 +263,7 @@ public void onAssessedCustomFee(AssessedCustomFee assessedCustomFee) throws Impo @Override public void onContract(Contract contract) { + entityIdService.notify(contract); Contract merged = contractState.merge(contract.getId(), contract, this::mergeContract); contracts.add(merged); } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractContractCallTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractContractCallTransactionHandler.java index d3940237c00..26784910891 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractContractCallTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractContractCallTransactionHandler.java @@ -33,8 +33,10 @@ import com.hedera.mirror.common.domain.contract.ContractResult; import com.hedera.mirror.common.domain.contract.ContractStateChange; import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.transaction.RecordFile; import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.domain.EntityIdService; import com.hedera.mirror.importer.parser.record.entity.EntityListener; import com.hedera.mirror.importer.parser.record.entity.EntityProperties; import com.hedera.mirror.importer.util.Utility; @@ -42,28 +44,27 @@ @RequiredArgsConstructor abstract class AbstractContractCallTransactionHandler implements TransactionHandler { + protected final EntityIdService entityIdService; protected final EntityListener entityListener; protected final EntityProperties entityProperties; + @SuppressWarnings("deprecation") protected final void onContractResult(RecordItem recordItem, ContractResult contractResult, ContractFunctionResult functionResult) { // set function result related properties where applicable if (functionResult != ContractFunctionResult.getDefaultInstance()) { long consensusTimestamp = recordItem.getConsensusTimestamp(); List createdContractIds = new ArrayList<>(); - boolean persist = recordItem.isSuccessful() && entityProperties.getPersist().isContracts(); + boolean persist = shouldPersistCreatedContractIDs(recordItem); for (ContractID createdContractId : functionResult.getCreatedContractIDsList()) { - EntityId contractId = EntityId.of(createdContractId); + EntityId contractId = entityIdService.lookup(createdContractId); createdContractIds.add(contractId.getId()); // The parent contract ID can also sometimes appear in the created contract IDs list, so exclude it - if (persist && !EntityId.isEmpty(contractId) && !contractId.equals(contractResult.getContractId())) { - Contract contract = contractId.toEntity(); - contract.setCreatedTimestamp(consensusTimestamp); - contract.setDeleted(false); - contract.setModifiedTimestamp(consensusTimestamp); - doUpdateEntity(contract, recordItem); + if (persist && !EntityId.isEmpty(contractId) && !contractId.equals( + contractResult.getContractId())) { + doUpdateEntity(getContract(contractId, consensusTimestamp), recordItem); } } @@ -81,7 +82,7 @@ protected final void onContractResult(RecordItem recordItem, ContractResult cont ContractLog contractLog = new ContractLog(); contractLog.setBloom(DomainUtils.toBytes(contractLoginfo.getBloom())); contractLog.setConsensusTimestamp(consensusTimestamp); - contractLog.setContractId(EntityId.of(contractLoginfo.getContractID())); + contractLog.setContractId(entityIdService.lookup(contractLoginfo.getContractID())); contractLog.setData(DomainUtils.toBytes(contractLoginfo.getData())); contractLog.setIndex(index); contractLog.setRootContractId(contractResult.getContractId()); @@ -98,7 +99,7 @@ protected final void onContractResult(RecordItem recordItem, ContractResult cont for (int stateIndex = 0; stateIndex < functionResult.getStateChangesCount(); ++stateIndex) { var contractStateChangeInfo = functionResult.getStateChanges(stateIndex); - var contractId = EntityId.of(contractStateChangeInfo.getContractID()); + var contractId = entityIdService.lookup(contractStateChangeInfo.getContractID()); for (int storageIndex = 0; storageIndex < contractStateChangeInfo .getStorageChangesCount(); ++storageIndex) { StorageChange storageChange = contractStateChangeInfo.getStorageChanges(storageIndex); @@ -111,8 +112,7 @@ protected final void onContractResult(RecordItem recordItem, ContractResult cont contractStateChange.setValueRead(DomainUtils.toBytes(storageChange.getValueRead())); // If a value of zero is written the valueWritten will be present but the inner value will be - // absent. - // If a value was read and not written this value will not be present. + // absent. If a value was read and not written this value will not be present. if (storageChange.hasValueWritten()) { contractStateChange .setValueWritten(DomainUtils.toBytes(storageChange.getValueWritten().getValue())); @@ -123,9 +123,30 @@ protected final void onContractResult(RecordItem recordItem, ContractResult cont } } - // always persist a contract result whether partial or complete + // Always persist a contract result whether partial or complete entityListener.onContractResult(contractResult); } protected abstract void doUpdateEntity(Contract contract, RecordItem recordItem); + + protected Contract getContract(EntityId contractId, long consensusTimestamp) { + Contract contract = contractId.toEntity(); + contract.setCreatedTimestamp(consensusTimestamp); + contract.setDeleted(false); + contract.setModifiedTimestamp(consensusTimestamp); + return contract; + } + + /** + * Persist contract entities in createdContractIDs if it's prior to HAPI 0.23.0. After that the createdContractIDs + * list is also externalized as contract create child records so we only need to persist the complete contract + * entity from the child record. + * + * @param recordItem to check + * @return Whether the createdContractIDs list should be persisted. + */ + private boolean shouldPersistCreatedContractIDs(RecordItem recordItem) { + return recordItem.isSuccessful() && entityProperties.getPersist().isContracts() && + recordItem.getHapiVersion().isLessThan(RecordFile.HAPI_VERSION_0_23_0); + } } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCallTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCallTransactionHandler.java index 09c3c878fdb..7d8160f7997 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCallTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCallTransactionHandler.java @@ -20,6 +20,7 @@ * ‍ */ +import com.hederahashgraph.api.proto.java.ContractID; import javax.inject.Named; import com.hedera.mirror.common.domain.contract.Contract; @@ -29,19 +30,32 @@ import com.hedera.mirror.common.domain.transaction.Transaction; import com.hedera.mirror.common.domain.transaction.TransactionType; import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.domain.EntityIdService; import com.hedera.mirror.importer.parser.record.entity.EntityListener; import com.hedera.mirror.importer.parser.record.entity.EntityProperties; @Named class ContractCallTransactionHandler extends AbstractContractCallTransactionHandler { - ContractCallTransactionHandler(EntityListener entityListener, EntityProperties entityProperties) { - super(entityListener, entityProperties); + ContractCallTransactionHandler(EntityIdService entityIdService, EntityListener entityListener, + EntityProperties entityProperties) { + super(entityIdService, entityListener, entityProperties); } + /** + * First attempts to extract the contract ID from the receipt, which was populated in HAPI 0.23 for contract calls. + * Otherwise, falls back to checking the transaction body which may contain an EVM address. In case of partial + * mirror nodes, it's possible the database does not have the mapping for that EVM address in the body, hence the + * need for prioritizing the receipt. + * + * @param recordItem to check + * @return The contract ID associated with this contract call + */ @Override public EntityId getEntity(RecordItem recordItem) { - return EntityId.of(recordItem.getTransactionBody().getContractCall().getContractID()); + ContractID contractIdBody = recordItem.getTransactionBody().getContractCall().getContractID(); + ContractID contractIdReceipt = recordItem.getRecord().getReceipt().getContractID(); + return entityIdService.lookup(contractIdReceipt, contractIdBody); } @Override diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCreateTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCreateTransactionHandler.java index 16f5b70ee20..b7ee0ab22da 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCreateTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCreateTransactionHandler.java @@ -29,19 +29,21 @@ import com.hedera.mirror.common.domain.transaction.Transaction; import com.hedera.mirror.common.domain.transaction.TransactionType; import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.domain.EntityIdService; import com.hedera.mirror.importer.parser.record.entity.EntityListener; import com.hedera.mirror.importer.parser.record.entity.EntityProperties; @Named class ContractCreateTransactionHandler extends AbstractContractCallTransactionHandler { - ContractCreateTransactionHandler(EntityListener entityListener, EntityProperties entityProperties) { - super(entityListener, entityProperties); + ContractCreateTransactionHandler(EntityIdService entityIdService, EntityListener entityListener, + EntityProperties entityProperties) { + super(entityIdService, entityListener, entityProperties); } @Override public EntityId getEntity(RecordItem recordItem) { - return EntityId.of(recordItem.getRecord().getReceipt().getContractID()); + return entityIdService.lookup(recordItem.getRecord().getReceipt().getContractID()); } @Override @@ -62,11 +64,7 @@ public void updateTransaction(Transaction transaction, RecordItem recordItem) { transaction.setInitialBalance(transactionBody.getInitialBalance()); if (entityProperties.getPersist().isContracts() && recordItem.isSuccessful() && !EntityId.isEmpty(entityId)) { - Contract contract = entityId.toEntity(); - contract.setCreatedTimestamp(consensusTimestamp); - contract.setDeleted(false); - contract.setModifiedTimestamp(consensusTimestamp); - doUpdateEntity(contract, recordItem); + doUpdateEntity(getContract(entityId, consensusTimestamp), recordItem); } if (entityProperties.getPersist().isContracts()) { @@ -84,6 +82,7 @@ public void updateTransaction(Transaction transaction, RecordItem recordItem) { @Override protected void doUpdateEntity(Contract contract, RecordItem recordItem) { + var contractCreateResult = recordItem.getRecord().getContractCreateResult(); var transactionBody = recordItem.getTransactionBody().getContractCreateInstance(); if (transactionBody.hasAutoRenewPeriod()) { @@ -102,6 +101,10 @@ protected void doUpdateEntity(Contract contract, RecordItem recordItem) { contract.setFileId(EntityId.of(transactionBody.getFileID())); } + if (contractCreateResult.hasEvmAddress()) { + contract.setEvmAddress(DomainUtils.toBytes(contractCreateResult.getEvmAddress().getValue())); + } + contract.setMemo(transactionBody.getMemo()); entityListener.onContract(contract); } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractDeleteTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractDeleteTransactionHandler.java index e538db01640..29efa7a4167 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractDeleteTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractDeleteTransactionHandler.java @@ -20,24 +20,40 @@ * ‍ */ +import com.hederahashgraph.api.proto.java.ContractID; import javax.inject.Named; import com.hedera.mirror.common.domain.contract.Contract; import com.hedera.mirror.common.domain.entity.EntityId; -import com.hedera.mirror.common.domain.transaction.TransactionType; import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.common.domain.transaction.TransactionType; +import com.hedera.mirror.importer.domain.EntityIdService; import com.hedera.mirror.importer.parser.record.entity.EntityListener; @Named class ContractDeleteTransactionHandler extends AbstractEntityCrudTransactionHandler { - ContractDeleteTransactionHandler(EntityListener entityListener) { + private final EntityIdService entityIdService; + + ContractDeleteTransactionHandler(EntityIdService entityIdService, EntityListener entityListener) { super(entityListener, TransactionType.CONTRACTDELETEINSTANCE); + this.entityIdService = entityIdService; } + /** + * First attempts to extract the contract ID from the receipt, which was populated in HAPI 0.23 for contract + * deletes. Otherwise, falls back to checking the transaction body which may contain an EVM address. In case of + * partial mirror nodes, it's possible the database does not have the mapping for that EVM address in the body, + * hence the need for prioritizing the receipt. + * + * @param recordItem to check + * @return The contract ID associated with this contract delete + */ @Override public EntityId getEntity(RecordItem recordItem) { - return EntityId.of(recordItem.getTransactionBody().getContractDeleteInstance().getContractID()); + ContractID contractIdBody = recordItem.getTransactionBody().getContractDeleteInstance().getContractID(); + ContractID contractIdReceipt = recordItem.getRecord().getReceipt().getContractID(); + return entityIdService.lookup(contractIdReceipt, contractIdBody); } @Override @@ -48,7 +64,7 @@ protected void doUpdateEntity(Contract contract, RecordItem recordItem) { if (transactionBody.hasTransferAccountID()) { obtainerId = EntityId.of(transactionBody.getTransferAccountID()); } else if (transactionBody.hasTransferContractID()) { - obtainerId = EntityId.of(transactionBody.getTransferContractID()); + obtainerId = entityIdService.lookup(transactionBody.getTransferContractID()); } contract.setObtainerId(obtainerId); diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractUpdateTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractUpdateTransactionHandler.java index d7002b44ee5..c5f0cb56261 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractUpdateTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractUpdateTransactionHandler.java @@ -20,6 +20,7 @@ * ‍ */ +import com.hederahashgraph.api.proto.java.ContractID; import javax.inject.Named; import com.hedera.mirror.common.domain.contract.Contract; @@ -27,18 +28,33 @@ import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.common.domain.transaction.TransactionType; import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.domain.EntityIdService; import com.hedera.mirror.importer.parser.record.entity.EntityListener; @Named class ContractUpdateTransactionHandler extends AbstractEntityCrudTransactionHandler { - ContractUpdateTransactionHandler(EntityListener entityListener) { + private final EntityIdService entityIdService; + + ContractUpdateTransactionHandler(EntityIdService entityIdService, EntityListener entityListener) { super(entityListener, TransactionType.CONTRACTUPDATEINSTANCE); + this.entityIdService = entityIdService; } + /** + * First attempts to extract the contract ID from the receipt, which was populated in HAPI 0.23 for contract update. + * Otherwise, falls back to checking the transaction body which may contain an EVM address. In case of partial + * mirror nodes, it's possible the database does not have the mapping for that EVM address in the body, hence the + * need for prioritizing the receipt. + * + * @param recordItem to check + * @return The contract ID associated with this contract transaction + */ @Override public EntityId getEntity(RecordItem recordItem) { - return EntityId.of(recordItem.getTransactionBody().getContractUpdateInstance().getContractID()); + ContractID contractIdBody = recordItem.getTransactionBody().getContractUpdateInstance().getContractID(); + ContractID contractIdReceipt = recordItem.getRecord().getReceipt().getContractID(); + return entityIdService.lookup(contractIdReceipt, contractIdBody); } // We explicitly ignore the updated fileID field since hedera nodes do not allow changing the bytecode after create diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemDeleteTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemDeleteTransactionHandler.java index a2b67bfc7cb..e53fdba76fa 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemDeleteTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemDeleteTransactionHandler.java @@ -23,26 +23,30 @@ import com.hederahashgraph.api.proto.java.SystemDeleteTransactionBody; import javax.inject.Named; -import com.hedera.mirror.common.domain.entity.AbstractEntity; import com.hedera.mirror.common.domain.contract.Contract; +import com.hedera.mirror.common.domain.entity.AbstractEntity; import com.hedera.mirror.common.domain.entity.Entity; import com.hedera.mirror.common.domain.entity.EntityId; -import com.hedera.mirror.common.domain.transaction.TransactionType; import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.common.domain.transaction.TransactionType; +import com.hedera.mirror.importer.domain.EntityIdService; import com.hedera.mirror.importer.parser.record.entity.EntityListener; @Named class SystemDeleteTransactionHandler extends AbstractEntityCrudTransactionHandler { - SystemDeleteTransactionHandler(EntityListener entityListener) { + private final EntityIdService entityIdService; + + SystemDeleteTransactionHandler(EntityIdService entityIdService, EntityListener entityListener) { super(entityListener, TransactionType.SYSTEMDELETE); + this.entityIdService = entityIdService; } @Override public EntityId getEntity(RecordItem recordItem) { SystemDeleteTransactionBody systemDelete = recordItem.getTransactionBody().getSystemDelete(); if (systemDelete.hasContractID()) { - return EntityId.of(systemDelete.getContractID()); + return entityIdService.lookup(systemDelete.getContractID()); } else if (systemDelete.hasFileID()) { return EntityId.of(systemDelete.getFileID()); } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemUndeleteTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemUndeleteTransactionHandler.java index e7d5dd20f96..d874bb12cf7 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemUndeleteTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemUndeleteTransactionHandler.java @@ -23,26 +23,30 @@ import com.hederahashgraph.api.proto.java.SystemUndeleteTransactionBody; import javax.inject.Named; -import com.hedera.mirror.common.domain.entity.AbstractEntity; import com.hedera.mirror.common.domain.contract.Contract; +import com.hedera.mirror.common.domain.entity.AbstractEntity; import com.hedera.mirror.common.domain.entity.Entity; import com.hedera.mirror.common.domain.entity.EntityId; -import com.hedera.mirror.common.domain.transaction.TransactionType; import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.common.domain.transaction.TransactionType; +import com.hedera.mirror.importer.domain.EntityIdService; import com.hedera.mirror.importer.parser.record.entity.EntityListener; @Named class SystemUndeleteTransactionHandler extends AbstractEntityCrudTransactionHandler { - SystemUndeleteTransactionHandler(EntityListener entityListener) { + private final EntityIdService entityIdService; + + SystemUndeleteTransactionHandler(EntityIdService entityIdService, EntityListener entityListener) { super(entityListener, TransactionType.SYSTEMUNDELETE); + this.entityIdService = entityIdService; } @Override public EntityId getEntity(RecordItem recordItem) { SystemUndeleteTransactionBody systemUndelete = recordItem.getTransactionBody().getSystemUndelete(); if (systemUndelete.hasContractID()) { - return EntityId.of(systemUndelete.getContractID()); + return entityIdService.lookup(systemUndelete.getContractID()); } else if (systemUndelete.hasFileID()) { return EntityId.of(systemUndelete.getFileID()); } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/AbstractPreV5RecordFileReader.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/AbstractPreV5RecordFileReader.java index d7ac18e6dca..0bab1f67d84 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/AbstractPreV5RecordFileReader.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/AbstractPreV5RecordFileReader.java @@ -36,10 +36,10 @@ import com.hedera.mirror.common.domain.DigestAlgorithm; import com.hedera.mirror.common.domain.transaction.RecordFile; +import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.importer.domain.StreamFileData; import com.hedera.mirror.importer.exception.ImporterException; import com.hedera.mirror.importer.exception.StreamFileReaderException; -import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.importer.reader.ValidatedDataInputStream; @RequiredArgsConstructor @@ -117,7 +117,7 @@ private void readBody(ValidatedDataInputStream vdis, RecordFileDigest digest, Re vdis.readByte(RECORD_MARKER, "record marker"); byte[] transactionBytes = vdis.readLengthAndBytes(1, MAX_TRANSACTION_LENGTH, false, "transaction bytes"); byte[] recordBytes = vdis.readLengthAndBytes(1, MAX_TRANSACTION_LENGTH, false, "record bytes"); - RecordItem recordItem = new RecordItem(transactionBytes, recordBytes); + RecordItem recordItem = new RecordItem(recordFile.getHapiVersion(), transactionBytes, recordBytes); items.add(recordItem); if (count == 0) { diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/RecordFileReaderImplV5.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/RecordFileReaderImplV5.java index 85919401dba..68d130ee0ed 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/RecordFileReaderImplV5.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/reader/record/RecordFileReaderImplV5.java @@ -34,14 +34,15 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import org.apache.commons.codec.binary.Hex; +import org.springframework.data.util.Version; import reactor.core.publisher.Flux; import com.hedera.mirror.common.domain.DigestAlgorithm; import com.hedera.mirror.common.domain.transaction.RecordFile; +import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.importer.domain.StreamFileData; import com.hedera.mirror.importer.exception.InvalidStreamFileException; import com.hedera.mirror.importer.exception.StreamFileReaderException; -import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.importer.reader.AbstractStreamObject; import com.hedera.mirror.importer.reader.HashObject; import com.hedera.mirror.importer.reader.ValidatedDataInputStream; @@ -109,7 +110,7 @@ private void readBody(ValidatedDataInputStream vdis, DigestInputStream metadataD // read record stream objects while (!isHashObject(vdis, hashObjectClassId)) { - RecordStreamObject recordStreamObject = new RecordStreamObject(vdis); + RecordStreamObject recordStreamObject = new RecordStreamObject(vdis, recordFile.getHapiVersion()); items.add(recordStreamObject.getRecordItem()); if (count == 0) { @@ -163,12 +164,14 @@ private static class RecordStreamObject extends AbstractStreamObject { private static final int MAX_RECORD_LENGTH = 64 * 1024; + private final Version hapiVersion; private final byte[] recordBytes; private final byte[] transactionBytes; private RecordItem recordItem; - RecordStreamObject(ValidatedDataInputStream vdis) { + RecordStreamObject(ValidatedDataInputStream vdis, Version hapiVersion) { super(vdis); + this.hapiVersion = hapiVersion; try { recordBytes = vdis.readLengthAndBytes(1, MAX_RECORD_LENGTH, false, "record bytes"); @@ -184,7 +187,7 @@ RecordItem getRecordItem() { } if (recordItem == null) { - recordItem = new RecordItem(transactionBytes, recordBytes); + recordItem = new RecordItem(hapiVersion, transactionBytes, recordBytes); } return recordItem; diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/ContractRepository.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/ContractRepository.java index 43d9edd4ae9..0ec9edd4b6b 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/ContractRepository.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/ContractRepository.java @@ -20,9 +20,14 @@ * ‍ */ +import java.util.Optional; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import com.hedera.mirror.common.domain.contract.Contract; public interface ContractRepository extends CrudRepository { + + @Query(value = "select id from contract where evm_address = ?1 and deleted <> true", nativeQuery = true) + Optional findByEvmAddress(byte[] evmAddress); } diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.54.2__evm_address.sql b/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.54.2__evm_address.sql new file mode 100644 index 00000000000..a30cc45142d --- /dev/null +++ b/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.54.2__evm_address.sql @@ -0,0 +1,6 @@ +-- add evm_address to tables contract and contract_history + +alter table contract add column evm_address bytea null; +create index if not exists contract__evm_address on contract (evm_address) where evm_address is not null; + +alter table contract_history add column evm_address bytea null; diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.0__create_tables.sql b/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.0__create_tables.sql index 7101020f6b0..5755f3d9ad6 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.0__create_tables.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.0__create_tables.sql @@ -84,6 +84,7 @@ create table if not exists contract auto_renew_period bigint null, created_timestamp bigint null, deleted boolean null, + evm_address bytea null, expiration_timestamp bigint null, file_id bigint null, id bigint not null, diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.3__index_init.sql b/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.3__index_init.sql index 34e4bdb2267..9019bf92c4e 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.3__index_init.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.3__index_init.sql @@ -37,6 +37,7 @@ alter table if exists contract alter table if exists contract add constraint contract__type_check check (type = 'CONTRACT'); +create index if not exists contract__evm_address on contract (evm_address) where evm_address is not null; create index if not exists contract__public_key on contract (public_key) where public_key is not null; -- contract_history diff --git a/hedera-mirror-importer/src/main/resources/db/scripts/v2/csvRestoreTables.sql b/hedera-mirror-importer/src/main/resources/db/scripts/v2/csvRestoreTables.sql index e41705f8b10..7eace117517 100644 --- a/hedera-mirror-importer/src/main/resources/db/scripts/v2/csvRestoreTables.sql +++ b/hedera-mirror-importer/src/main/resources/db/scripts/v2/csvRestoreTables.sql @@ -14,9 +14,9 @@ \copy assessed_custom_fee (amount, collector_account_id, consensus_timestamp, token_id) from assessed_custom_fee.csv csv; -\copy contract (auto_renew_period, created_timestamp, deleted, expiration_timestamp, file_id, id, key, memo, num, obtainer_id, proxy_account_id, public_key, realm, shard, timestamp_range, type) from contract.csv csv; +\copy contract (auto_renew_period, created_timestamp, deleted, expiration_timestamp, file_id, id, key, memo, num, obtainer_id, proxy_account_id, public_key, realm, shard, timestamp_range, type, evm_address) from contract.csv csv; -\copy contract_history (auto_renew_period, created_timestamp, deleted, expiration_timestamp, file_id, id, key, memo, num, obtainer_id, proxy_account_id, public_key, realm, shard, timestamp_range, type) from contract_history.csv csv; +\copy contract_history (auto_renew_period, created_timestamp, deleted, expiration_timestamp, file_id, id, key, memo, num, obtainer_id, proxy_account_id, public_key, realm, shard, timestamp_range, type, evm_address) from contract_history.csv csv; \copy contract_log (bloom, consensus_timestamp, contract_id, data, index, topic0, topic1, topic2, topic3, root_contract_id) from contract_log.csv csv; diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/IntegrationTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/IntegrationTest.java index b0b08732b1a..4e93d4584a3 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/IntegrationTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/IntegrationTest.java @@ -80,7 +80,7 @@ void logTest(TestInfo testInfo) { log.info("Executing: {}", testInfo.getDisplayName()); } - private void reset() { + protected void reset() { cacheManagers.forEach(c -> c.getCacheNames().forEach(name -> c.getCache(name).clear())); mirrorDateRangePropertiesProcessor.clear(); mirrorProperties.setStartDate(Instant.EPOCH); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/EntityIdServiceImplTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/EntityIdServiceImplTest.java new file mode 100644 index 00000000000..a02323bd7ee --- /dev/null +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/domain/EntityIdServiceImplTest.java @@ -0,0 +1,286 @@ +package com.hedera.mirror.importer.domain; + +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 - 2022 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. + * ‍ + */ + +import static com.hedera.mirror.common.domain.entity.EntityType.ACCOUNT; +import static com.hedera.mirror.common.domain.entity.EntityType.CONTRACT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.api.proto.java.ContractID; +import javax.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.cache.Cache; + +import com.hedera.mirror.common.domain.DomainBuilder; +import com.hedera.mirror.common.domain.contract.Contract; +import com.hedera.mirror.common.domain.entity.Entity; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.IntegrationTest; +import com.hedera.mirror.importer.repository.ContractRepository; + +class EntityIdServiceImplTest extends IntegrationTest { + + // in the form 'shard.realm.num' + private static final byte[] PARSABLE_EVM_ADDRESS = new byte[] { + 0, 0, 0, 0, // shard + 0, 0, 0, 0, 0, 0, 0, 0, // realm + 0, 0, 0, 0, 0, 0, 0, 100, // num + }; + + @Resource + private ContractRepository contractRepository; + + @Resource + private DomainBuilder domainBuilder; + + @Resource + private EntityIdService entityIdService; + + @Test + void cache() { + Contract contract = domainBuilder.contract().persist(); + ContractID contractId = getProtoContractId(contract); + EntityId expected = contract.toEntityId(); + + // db query and cache put + assertThat(entityIdService.lookup(contractId)).isEqualTo(expected); + + // mark it as deleted + contract.setDeleted(true); + contractRepository.save(contract); + + // cache hit + assertThat(entityIdService.lookup(contractId)).isEqualTo(expected); + + contractRepository.deleteById(contract.getId()); + assertThat(entityIdService.lookup(contractId)).isEqualTo(expected); + + // cache miss + reset(); + assertThat(entityIdService.lookup(contractId)).isEqualTo(EntityId.EMPTY); + } + + @Test + void lookupAccountNum() { + AccountID accountId = AccountID.newBuilder().setAccountNum(100).build(); + assertThat(entityIdService.lookup(accountId)).isEqualTo(EntityId.of(100, ACCOUNT)); + } + + @Test + void lookupAccountAlias() { + Entity account = domainBuilder.entity().persist(); + assertThat(entityIdService.lookup(getProtoAccountId(account))).isEqualTo(account.toEntityId()); + } + + @Test + void lookupAccountAliasNoMatch() { + Entity account = domainBuilder.entity().get(); + assertThat(entityIdService.lookup(getProtoAccountId(account))).isEqualTo(EntityId.EMPTY); + } + + @Test + void lookupAccountAliasDeleted() { + Entity account = domainBuilder.entity().customize(e -> e.deleted(true)).persist(); + assertThat(entityIdService.lookup(getProtoAccountId(account))).isEqualTo(EntityId.EMPTY); + } + + @Test + void lookupAccountDefaultInstance() { + assertThat(entityIdService.lookup(AccountID.getDefaultInstance())).isEqualTo(EntityId.EMPTY); + } + + @Test + void lookupAccountNull() { + assertThat(entityIdService.lookup((AccountID) null)).isEqualTo(EntityId.EMPTY); + } + + @Test + void lookupAccountThrows() { + AccountID accountId = AccountID.newBuilder().setRealmNum(1).build(); + assertThrows(Cache.ValueRetrievalException.class, () -> entityIdService.lookup(accountId)); + } + + @Test + void lookupAccounts() { + AccountID accountId = AccountID.newBuilder().setAccountNum(100).build(); + AccountID accountIdInvalid = AccountID.newBuilder().setRealmNum(1).build(); + Entity accountDeleted = domainBuilder.entity().customize(e -> e.deleted(true)).persist(); + + EntityId entityId = entityIdService.lookup(null, + AccountID.getDefaultInstance(), + getProtoAccountId(accountDeleted), + accountIdInvalid, + accountId); + + assertThat(entityId).isEqualTo(EntityId.of(accountId)); + } + + @Test + void lookupAccountsReturnsFirst() { + AccountID accountId1 = AccountID.newBuilder().setAccountNum(100).build(); + AccountID accountId2 = AccountID.newBuilder().setAccountNum(101).build(); + EntityId entityId = entityIdService.lookup(accountId1, accountId2); + assertThat(entityId).isEqualTo(EntityId.of(accountId1)); + } + + @Test + void lookupContractNum() { + ContractID contractId = ContractID.newBuilder().setContractNum(100).build(); + assertThat(entityIdService.lookup(contractId)).isEqualTo(EntityId.of(100, CONTRACT)); + } + + @Test + void lookupContractCreate2EvmAddress() { + Contract contract = domainBuilder.contract().persist(); + assertThat(entityIdService.lookup(getProtoContractId(contract))).isEqualTo(contract.toEntityId()); + } + + @Test + void lookupContractCreate2EvmAddressNoMatch() { + Contract contract = domainBuilder.contract().get(); + assertThat(entityIdService.lookup(getProtoContractId(contract))).isEqualTo(EntityId.EMPTY); + } + + @Test + void lookupContractCreate2EvmAddressDeleted() { + Contract contract = domainBuilder.contract().customize((b) -> b.deleted(true)).persist(); + assertThat(entityIdService.lookup(getProtoContractId(contract))).isEqualTo(EntityId.EMPTY); + } + + @Test + void lookupContractDefaultInstance() { + assertThat(entityIdService.lookup(ContractID.getDefaultInstance())).isEqualTo(EntityId.EMPTY); + } + + @Test + void lookupContractNull() { + assertThat(entityIdService.lookup((ContractID) null)).isEqualTo(EntityId.EMPTY); + } + + @Test + void lookupParsableEvmAddress() { + var contractId = ContractID.newBuilder().setEvmAddress(DomainUtils.fromBytes(PARSABLE_EVM_ADDRESS)).build(); + assertThat(entityIdService.lookup(contractId)).isEqualTo(EntityId.of(100, CONTRACT)); + } + + @Test + void lookupParsableEvmAddressShardRealmMismatch() { + ContractID contractId = ContractID.newBuilder() + .setShardNum(1) + .setRealmNum(2) + .setEvmAddress(DomainUtils.fromBytes(PARSABLE_EVM_ADDRESS)) + .build(); + assertThat(entityIdService.lookup(contractId)).isEqualTo(EntityId.EMPTY); + } + + @Test + void lookupContractThrows() { + ContractID contractId = ContractID.newBuilder().setRealmNum(1).build(); + assertThrows(Cache.ValueRetrievalException.class, () -> entityIdService.lookup(contractId)); + } + + @Test + void lookupContracts() { + ContractID contractId = ContractID.newBuilder().setContractNum(100).build(); + ContractID contractIdInvalid = ContractID.newBuilder().setRealmNum(1).build(); + Contract contractDeleted = domainBuilder.contract().customize(e -> e.deleted(true)).persist(); + + EntityId entityId = entityIdService.lookup(null, + ContractID.getDefaultInstance(), + getProtoContractId(contractDeleted), + contractIdInvalid, + contractId); + + assertThat(entityId).isEqualTo(EntityId.of(contractId)); + } + + @Test + void lookupContractsReturnsFirst() { + ContractID contractId1 = ContractID.newBuilder().setContractNum(100).build(); + ContractID contractId2 = ContractID.newBuilder().setContractNum(101).build(); + EntityId entityId = entityIdService.lookup(contractId1, contractId2); + assertThat(entityId).isEqualTo(EntityId.of(contractId1)); + } + + @ParameterizedTest + @CsvSource(value = {"false", ","}) + void storeAccount(Boolean deleted) { + Entity account = domainBuilder.entity().customize(e -> e.deleted(deleted)).get(); + entityIdService.notify(account); + assertThat(entityIdService.lookup(getProtoAccountId(account))).isEqualTo(account.toEntityId()); + } + + @Test + void storeAccountDeleted() { + Entity account = domainBuilder.entity().customize(e -> e.deleted(true)).get(); + entityIdService.notify(account); + assertThat(entityIdService.lookup(getProtoAccountId(account))).isEqualTo(EntityId.EMPTY); + } + + @ParameterizedTest + @CsvSource(value = {"false", ","}) + void storeContract(Boolean deleted) { + Contract contract = domainBuilder.contract().customize(c -> c.deleted(deleted)).get(); + entityIdService.notify(contract); + assertThat(entityIdService.lookup(getProtoContractId(contract))).isEqualTo(contract.toEntityId()); + } + + @Test + void storeContractDeleted() { + Contract contract = domainBuilder.contract().customize(c -> c.deleted(true)).get(); + entityIdService.notify(contract); + assertThat(entityIdService.lookup(getProtoContractId(contract))).isEqualTo(EntityId.EMPTY); + } + + @Test + void storeNull() { + assertDoesNotThrow(() -> entityIdService.notify(null)); + } + + private AccountID getProtoAccountId(Entity account) { + var accountId = AccountID.newBuilder() + .setShardNum(account.getShard()) + .setRealmNum(account.getRealm()); + if (account.getAlias() == null) { + accountId.setAccountNum(account.getNum()); + } else { + accountId.setAlias(DomainUtils.fromBytes(account.getAlias())); + } + return accountId.build(); + } + + private ContractID getProtoContractId(Contract contract) { + var contractId = ContractID.newBuilder() + .setShardNum(contract.getShard()) + .setRealmNum(contract.getRealm()); + if (contract.getEvmAddress() == null) { + contractId.setContractNum(contract.getNum()); + } else { + contractId.setEvmAddress(DomainUtils.fromBytes(contract.getEvmAddress())); + } + return contractId.build(); + } +} diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/ContractResultMigrationTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/ContractResultMigrationTest.java index da5bb0b8a94..e8cff05f5b9 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/ContractResultMigrationTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/ContractResultMigrationTest.java @@ -89,6 +89,7 @@ void migrateWhenNoProtobufData() throws Exception { .allMatch(Objects::isNull); } + @SuppressWarnings("deprecation") @Test void migrate() throws Exception { ContractFunctionResult.Builder functionResult = contractFunctionResult(); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/RecordItemBuilder.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/RecordItemBuilder.java index fd13269aa8a..f32ac892735 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/RecordItemBuilder.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/domain/RecordItemBuilder.java @@ -27,13 +27,16 @@ import com.google.protobuf.ByteString; import com.google.protobuf.BytesValue; import com.google.protobuf.GeneratedMessageV3; +import com.google.protobuf.StringValue; import com.hederahashgraph.api.proto.java.AccountAmount; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.ContractCallTransactionBody; import com.hederahashgraph.api.proto.java.ContractCreateTransactionBody; +import com.hederahashgraph.api.proto.java.ContractDeleteTransactionBody; import com.hederahashgraph.api.proto.java.ContractFunctionResult; import com.hederahashgraph.api.proto.java.ContractID; import com.hederahashgraph.api.proto.java.ContractLoginfo; +import com.hederahashgraph.api.proto.java.ContractUpdateTransactionBody; import com.hederahashgraph.api.proto.java.Duration; import com.hederahashgraph.api.proto.java.FileID; import com.hederahashgraph.api.proto.java.Key; @@ -61,7 +64,9 @@ import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; +import org.springframework.data.util.Version; +import com.hedera.mirror.common.domain.transaction.RecordFile; import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.common.domain.transaction.TransactionType; import com.hedera.mirror.importer.util.Utility; @@ -83,7 +88,7 @@ public class RecordItemBuilder { private final SecureRandom random = new SecureRandom(); public Builder contractCall() { - ContractID contractId = contractId(); + var contractId = contractId(); ContractCallTransactionBody.Builder transactionBody = ContractCallTransactionBody.newBuilder() .setAmount(5_000L) .setContractID(contractId) @@ -91,11 +96,15 @@ public Builder contractCall() { .setGas(10_000L); return new Builder<>(TransactionType.CONTRACTCALL, transactionBody) + .receipt(r -> r.setContractID(contractId)) .record(r -> r.setContractCallResult(contractFunctionResult(contractId))); } public Builder contractCreate() { - ContractID contractId = contractId(); + return contractCreate(contractId()); + } + + public Builder contractCreate(ContractID contractId) { ContractCreateTransactionBody.Builder transactionBody = ContractCreateTransactionBody.newBuilder() .setAdminKey(key()) .setAutoRenewPeriod(duration(30)) @@ -111,15 +120,25 @@ public Builder contractCreate() { return new Builder<>(TransactionType.CONTRACTCREATEINSTANCE, transactionBody) .receipt(r -> r.setContractID(contractId)) - .record(r -> r.setContractCreateResult(contractFunctionResult(contractId))); + .record(r -> r.setContractCreateResult(contractFunctionResult(contractId) + .addCreatedContractIDs(contractId))); + } + + public Builder contractDelete() { + var contractId = contractId(); + ContractDeleteTransactionBody.Builder transactionBody = ContractDeleteTransactionBody.newBuilder() + .setContractID(contractId) + .setTransferAccountID(accountId()); + + return new Builder<>(TransactionType.CONTRACTDELETEINSTANCE, transactionBody) + .receipt(r -> r.setContractID(contractId)); } - private ContractFunctionResult.Builder contractFunctionResult(ContractID contractId) { + public ContractFunctionResult.Builder contractFunctionResult(ContractID contractId) { return ContractFunctionResult.newBuilder() .setBloom(bytes(256)) .setContractCallResult(bytes(16)) .setContractID(contractId) - .addCreatedContractIDs(contractId) .addCreatedContractIDs(contractId()) .setErrorMessage(text(10)) .setGasUsed(1000L) @@ -171,6 +190,20 @@ private ContractFunctionResult.Builder contractFunctionResult(ContractID contrac .build()); } + public Builder contractUpdate() { + var contractId = contractId(); + ContractUpdateTransactionBody.Builder transactionBody = ContractUpdateTransactionBody.newBuilder() + .setAdminKey(key()) + .setAutoRenewPeriod(duration(30)) + .setContractID(contractId) + .setExpirationTime(timestamp()) + .setMemoWrapper(StringValue.of(text(16))) + .setProxyAccountID(accountId()); + + return new Builder<>(TransactionType.CONTRACTUPDATEINSTANCE, transactionBody) + .receipt(r -> r.setContractID(contractId)); + } + public Builder tokenMint(TokenType tokenType) { TokenMintTransactionBody.Builder transactionBody = TokenMintTransactionBody.newBuilder().setToken(tokenId()); @@ -240,6 +273,7 @@ public class Builder { private final TransactionBody.Builder transactionBodyWrapper; private final TransactionRecord.Builder transactionRecord; private final AccountID payerAccountId; + private Version hapiVersion = RecordFile.HAPI_VERSION_NOT_SET; private Builder(TransactionType type, T transactionBody) { this.payerAccountId = accountId(); @@ -250,8 +284,17 @@ private Builder(TransactionType type, T transactionBody) { } public RecordItem build() { + var field = transactionBodyWrapper.getDescriptorForType().findFieldByNumber(type.getProtoId()); + transactionBodyWrapper.setField(field, this.transactionBody.build()); + Transaction transaction = transaction().build(); - return new RecordItem(transaction, transactionRecord.build()); + TransactionRecord record = transactionRecord.build(); + return new RecordItem(hapiVersion, transaction.toByteArray(), record.toByteArray()); + } + + public Builder hapiVersion(Version hapiVersion) { + this.hapiVersion = hapiVersion; + return this; } public Builder receipt(Consumer consumer) { @@ -287,17 +330,12 @@ private SignatureMap.Builder defaultSignatureMap() { } private TransactionBody.Builder defaultTransactionBody() { - TransactionBody.Builder transactionBody = TransactionBody.newBuilder() + return TransactionBody.newBuilder() .setMemo(type.name()) .setNodeAccountID(NODE) .setTransactionFee(100L) .setTransactionID(Utility.getTransactionId(payerAccountId)) .setTransactionValidDuration(duration(120)); - - var field = transactionBody.getDescriptorForType().findFieldByNumber(type.getProtoId()); - transactionBody.setField(field, this.transactionBody.build()); - - return transactionBody; } private TransactionRecord.Builder defaultTransactionRecord() { diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/NonFeeTransferExtractionStrategyImplTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/NonFeeTransferExtractionStrategyImplTest.java index 6ba06cb1b20..50673e10811 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/NonFeeTransferExtractionStrategyImplTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/NonFeeTransferExtractionStrategyImplTest.java @@ -21,11 +21,13 @@ */ import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; import com.hederahashgraph.api.proto.java.AccountAmount; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.ContractCallTransactionBody; import com.hederahashgraph.api.proto.java.ContractCreateTransactionBody; +import com.hederahashgraph.api.proto.java.ContractFunctionResult; import com.hederahashgraph.api.proto.java.ContractID; import com.hederahashgraph.api.proto.java.CryptoCreateTransactionBody; import com.hederahashgraph.api.proto.java.CryptoTransferTransactionBody; @@ -41,7 +43,18 @@ import java.util.List; import java.util.stream.StreamSupport; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.exception.InvalidEntityException; +import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.TestUtils; +import com.hedera.mirror.importer.domain.EntityIdService; + +@ExtendWith(MockitoExtension.class) class NonFeeTransferExtractionStrategyImplTest { private static final long payerAccountNum = 999L; private static final AccountID payerAccountId = AccountID.newBuilder().setAccountNum(payerAccountNum).build(); @@ -51,7 +64,11 @@ class NonFeeTransferExtractionStrategyImplTest { private static final long newEntityNum = 987654L; private static final AccountID newAccountId = AccountID.newBuilder().setAccountNum(newEntityNum).build(); - private final NonFeeTransferExtractionStrategyImpl extractionStrategy = new NonFeeTransferExtractionStrategyImpl(); + @Mock + private EntityIdService entityIdService; + + @InjectMocks + private NonFeeTransferExtractionStrategyImpl extractionStrategy; @Test void extractNonFeeTransfersCryptoTransfer() { @@ -69,7 +86,7 @@ void extractNonFeeTransfersCryptoCreate() { var result = extractionStrategy.extractNonFeeTransfers(transactionBody, getNewAccountTransactionRecord()); assertAll( () -> assertEquals(2, StreamSupport.stream(result.spliterator(), false).count()), - () -> assertResult(createAccountAmounts(payerAccountNum, 0 - initialBalance, + () -> assertResult(createAccountAmounts(payerAccountNum, -initialBalance, newEntityNum, initialBalance), result) ); } @@ -80,7 +97,7 @@ void extractNonFeeTransfersFailedCryptoCreate() { var result = extractionStrategy.extractNonFeeTransfers(transactionBody, getFailedTransactionRecord()); assertAll( () -> assertEquals(1, StreamSupport.stream(result.spliterator(), false).count()), - () -> assertResult(createAccountAmounts(payerAccountNum, 0 - initialBalance), result) + () -> assertResult(createAccountAmounts(payerAccountNum, -initialBalance), result) ); } @@ -90,7 +107,7 @@ void extractNonFeeTransfersContractCreate() { var result = extractionStrategy.extractNonFeeTransfers(transactionBody, getNewContractTransactionRecord()); assertAll( () -> assertEquals(2, StreamSupport.stream(result.spliterator(), false).count()), - () -> assertResult(createAccountAmounts(payerAccountNum, 0 - initialBalance, + () -> assertResult(createAccountAmounts(payerAccountNum, -initialBalance, newEntityNum, initialBalance), result) ); } @@ -101,23 +118,62 @@ void extractNonFeeTransfersFailedContractCreate() { var result = extractionStrategy.extractNonFeeTransfers(transactionBody, getFailedTransactionRecord()); assertAll( () -> assertEquals(1, StreamSupport.stream(result.spliterator(), false).count()), - () -> assertResult(createAccountAmounts(payerAccountNum, 0 - initialBalance), result) + () -> assertResult(createAccountAmounts(payerAccountNum, -initialBalance), result) ); } @Test - void extractNonFeeTransfersContractCall() { + void extractNonFeeTransfersContractCallBody() { var amount = 123456L; var contractNum = 8888L; + ContractID contractId = ContractID.newBuilder().setContractNum(contractNum).build(); var transactionBody = getContractCallTransactionBody(contractNum, amount); - var result = extractionStrategy.extractNonFeeTransfers(transactionBody, getSimpleTransactionRecord()); + var contractCallResult = ContractFunctionResult.newBuilder().setContractID(contractId); + var transactionRecord = getSimpleTransactionRecord().toBuilder() + .setContractCallResult(contractCallResult) + .build(); + when(entityIdService.lookup(ContractID.getDefaultInstance(), contractId)).thenReturn(EntityId.of(contractId)); + var result = extractionStrategy.extractNonFeeTransfers(transactionBody, transactionRecord); assertAll( () -> assertEquals(2, StreamSupport.stream(result.spliterator(), false).count()), - () -> assertResult(createAccountAmounts(contractNum, amount, - payerAccountNum, 0 - amount), result) + () -> assertResult(createAccountAmounts(contractNum, amount, payerAccountNum, -amount), result) ); } + @Test + void extractNonFeeTransfersContractCallReceipt() { + var amount = 123456L; + var contractNum = 8888L; + ContractID contractIdBody = ContractID.newBuilder().setContractNum(-1L).build(); + ContractID contractIdReceipt = ContractID.newBuilder().setContractNum(contractNum).build(); + var transactionBody = getContractCallTransactionBody(-1L, amount); + var contractCallResult = ContractFunctionResult.newBuilder().setContractID(contractIdReceipt); + + var receipt = TransactionReceipt.newBuilder().setContractID(contractIdReceipt) + .setStatus(ResponseCodeEnum.SUCCESS); + var transactionRecord = TransactionRecord.newBuilder() + .setReceipt(receipt) + .setContractCallResult(contractCallResult) + .build(); + when(entityIdService.lookup(contractIdReceipt, contractIdBody)).thenReturn(EntityId.of(contractIdReceipt)); + var result = extractionStrategy.extractNonFeeTransfers(transactionBody, transactionRecord); + assertAll( + () -> assertEquals(2, StreamSupport.stream(result.spliterator(), false).count()), + () -> assertResult(createAccountAmounts(contractNum, amount, payerAccountNum, -amount), result) + ); + } + + @Test + void extractNonFeeTransfersContractCallFailedThrows() { + var amount = 123456L; + var transactionBody = getContractCallTransactionBody(TestUtils.generateRandomByteArray(20), amount); + var transactionRecord = getSimpleTransactionRecord(); + when(entityIdService.lookup(ContractID.getDefaultInstance(), transactionBody.getContractCall().getContractID() + )).thenThrow(new InvalidEntityException("")); + assertThrows(InvalidEntityException.class, + () -> extractionStrategy.extractNonFeeTransfers(transactionBody, transactionRecord)); + } + @Test void extractNonFeeTransfersFileCreateNone() { var transactionBody = getFileCreateTransactionBody(); @@ -182,6 +238,13 @@ private TransactionBody getContractCallTransactionBody(long contractNum, long am return transactionBodyBuilder().setContractCall(innerBody).build(); } + private TransactionBody getContractCallTransactionBody(byte[] evmAddress, long amount) { + var innerBody = ContractCallTransactionBody.newBuilder() + .setContractID(ContractID.newBuilder().setEvmAddress(DomainUtils.fromBytes(evmAddress))) + .setAmount(amount).build(); + return transactionBodyBuilder().setContractCall(innerBody).build(); + } + private TransactionBody getFileCreateTransactionBody() { var innerBody = FileCreateTransactionBody.newBuilder().build(); return transactionBodyBuilder().setFileCreate(innerBody).build(); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/RecordFileParserTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/RecordFileParserTest.java index 9109d94fc3c..c198feeab50 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/RecordFileParserTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/RecordFileParserTest.java @@ -44,17 +44,17 @@ import org.mockito.Mock; import reactor.core.publisher.Flux; -import com.hedera.mirror.importer.config.MirrorDateRangePropertiesProcessor; -import com.hedera.mirror.importer.config.MirrorDateRangePropertiesProcessor.DateRangeFilter; import com.hedera.mirror.common.domain.DigestAlgorithm; +import com.hedera.mirror.common.domain.StreamFile; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.EntityType; import com.hedera.mirror.common.domain.transaction.RecordFile; -import com.hedera.mirror.common.domain.StreamFile; +import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.importer.config.MirrorDateRangePropertiesProcessor; +import com.hedera.mirror.importer.config.MirrorDateRangePropertiesProcessor.DateRangeFilter; import com.hedera.mirror.importer.domain.StreamFilename; import com.hedera.mirror.importer.exception.ParserSQLException; import com.hedera.mirror.importer.parser.AbstractStreamFileParserTest; -import com.hedera.mirror.common.domain.transaction.RecordItem; class RecordFileParserTest extends AbstractStreamFileParserTest { @@ -113,6 +113,9 @@ protected StreamFile getStreamFile() { recordFile.setCount(id); recordFile.setDigestAlgorithm(DigestAlgorithm.SHA384); recordFile.setFileHash("fileHash" + id); + recordFile.setHapiVersionMajor(0); + recordFile.setHapiVersionMinor(23); + recordFile.setHapiVersionPatch(0); recordFile.setHash("hash" + id); recordFile.setLoadEnd(id); recordFile.setLoadStart(id); @@ -198,6 +201,6 @@ private RecordItem recordItem(long timestamp) { TransactionRecord transactionRecord = TransactionRecord.newBuilder() .setConsensusTimestamp(Timestamp.newBuilder().setNanos((int) timestamp)) .build(); - return new RecordItem(transaction.toByteArray(), transactionRecord.toByteArray()); + return new RecordItem(transaction, transactionRecord); } } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerContractTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerContractTest.java index 93218cfce0f..9ec2d26e85b 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerContractTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerContractTest.java @@ -42,17 +42,24 @@ import com.hederahashgraph.api.proto.java.TokenType; import com.hederahashgraph.api.proto.java.Transaction; import com.hederahashgraph.api.proto.java.TransactionBody; +import com.hederahashgraph.api.proto.java.TransactionReceipt; import com.hederahashgraph.api.proto.java.TransactionRecord; +import java.nio.ByteBuffer; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; import java.util.stream.Collectors; import javax.annotation.Resource; import lombok.SneakyThrows; +import lombok.Value; import org.assertj.core.api.ObjectAssert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.data.util.Version; import com.hedera.mirror.common.domain.contract.Contract; import com.hedera.mirror.common.domain.contract.ContractLog; @@ -62,18 +69,25 @@ import com.hedera.mirror.common.domain.entity.EntityType; import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.TestUtils; import com.hedera.mirror.importer.parser.domain.RecordItemBuilder; import com.hedera.mirror.importer.repository.ContractLogRepository; import com.hedera.mirror.importer.repository.ContractStateChangeRepository; import com.hedera.mirror.importer.util.Utility; +@SuppressWarnings("deprecation") class EntityRecordItemListenerContractTest extends AbstractEntityRecordItemListenerTest { - private static final ContractID CONTRACT_ID = ContractID.newBuilder().setContractNum(1001).build(); - private static final ContractID CREATED_CONTRACT_ID = ContractID.newBuilder().setContractNum(1002).build(); + private static final ContractID CONTRACT_ID = ContractID.newBuilder().setContractNum(901).build(); + private static final ContractID CREATED_CONTRACT_ID = ContractID.newBuilder().setContractNum(902).build(); + private static final Version HAPI_VERSION_0_23_0 = new Version(0, 23, 0); + + // saves the mapping from proto ContractID to EntityId so as not to use EntityIdService to verify itself + private Map contractIds; @Resource private ContractLogRepository contractLogRepository; + @Resource private ContractStateChangeRepository contractStateChangeRepository; @@ -82,6 +96,7 @@ class EntityRecordItemListenerContractTest extends AbstractEntityRecordItemListe @BeforeEach void before() { + contractIds = new HashMap<>(); entityProperties.getPersist().setFiles(true); entityProperties.getPersist().setSystemFiles(true); entityProperties.getPersist().setContracts(true); @@ -103,14 +118,91 @@ var record = recordItem.getRecord(); () -> assertEquals(1, contractResultRepository.count()), () -> assertEquals(3, cryptoTransferRepository.count()), () -> assertContractEntity(recordItem), + () -> assertThat(contractResultRepository.findAll()).hasSize(1), () -> assertContractCreateResult(transactionBody, record) ); } + @Test + void contractCreateWithEvmAddress() { + // no child tx, creates a single contract with evm address set + byte[] evmAddress = domainBuilder.create2EvmAddress(); + RecordItem recordItem = recordItemBuilder.contractCreate(CONTRACT_ID) + .record(r -> r.setContractCreateResult(r.getContractCreateResultBuilder() + .clearCreatedContractIDs() + .addCreatedContractIDs(CONTRACT_ID) + .setEvmAddress(BytesValue.of(DomainUtils.fromBytes(evmAddress))) + )) + .build(); + var record = recordItem.getRecord(); + var transactionBody = recordItem.getTransactionBody().getContractCreateInstance(); + + parseRecordItemAndCommit(recordItem); + + assertAll( + () -> assertEquals(1, transactionRepository.count()), + () -> assertEquals(1, contractRepository.count()), + () -> assertEquals(0, entityRepository.count()), + () -> assertEquals(1, contractResultRepository.count()), + () -> assertEquals(3, cryptoTransferRepository.count()), + () -> assertContractEntity(recordItem), + () -> assertThat(contractResultRepository.findAll()).hasSize(1), + () -> assertContractCreateResult(transactionBody, record) + ); + } + + @Test + void contractCreateWithEvmAddressAndChildCreate() { + // given contractCreate with child contractCreate + var parentEvmAddress = domainBuilder.create2EvmAddress(); + var parentRecordItem = recordItemBuilder.contractCreate() + .record(r -> r.setContractCreateResult(r.getContractCreateResultBuilder() + .setEvmAddress(BytesValue.of(DomainUtils.fromBytes(parentEvmAddress))) + )) + .hapiVersion(HAPI_VERSION_0_23_0) + .build(); + + var contractCreateResult = parentRecordItem.getRecord().getContractCreateResult(); + var childContractId = contractCreateResult.getCreatedContractIDsList().stream() + .filter(c -> !c.equals(contractCreateResult.getContractID())) + .findFirst().get(); + var childEvmAddress = domainBuilder.create2EvmAddress(); + var childConsensusTimestamp = TestUtils.toTimestamp(parentRecordItem.getConsensusTimestamp() + 1); + var childTransactionId = parentRecordItem.getRecord().getTransactionID().toBuilder().setNonce(1); + var childRecordItem = recordItemBuilder.contractCreate(childContractId) + .record(r -> r.setConsensusTimestamp(childConsensusTimestamp) + .setTransactionID(childTransactionId) + .setContractCreateResult(r.getContractCreateResultBuilder() + .clearCreatedContractIDs() + .clearStateChanges() + .setEvmAddress(BytesValue.of(DomainUtils.fromBytes(childEvmAddress))))) + .hapiVersion(HAPI_VERSION_0_23_0) + .build(); + + // when + parseRecordItemsAndCommit(List.of(parentRecordItem, childRecordItem)); + + // then + var parentTransactionBody = parentRecordItem.getTransactionBody().getContractCreateInstance(); + var childTransactionBody = childRecordItem.getTransactionBody().getContractCreateInstance(); + assertAll( + () -> assertEquals(2, transactionRepository.count()), + () -> assertEquals(2, contractRepository.count()), + () -> assertEquals(0, entityRepository.count()), + () -> assertEquals(2, contractResultRepository.count()), + () -> assertEquals(6, cryptoTransferRepository.count()), + () -> assertContractEntity(parentRecordItem), + () -> assertContractEntity(childRecordItem), + () -> assertThat(contractResultRepository.findAll()).hasSize(2), + () -> assertContractCreateResult(parentTransactionBody, parentRecordItem.getRecord()), + () -> assertContractCreateResult(childTransactionBody, childRecordItem.getRecord()) + ); + } + @Test void contractCreateFailedWithResult() { RecordItem recordItem = recordItemBuilder.contractCreate() - .record(r -> r.setContractCreateResult(ContractFunctionResult.getDefaultInstance())) + .record(TransactionRecord.Builder::clearContractCreateResult) .receipt(r -> r.clearContractID().setStatus(ResponseCodeEnum.CONTRACT_EXECUTION_EXCEPTION)) .build(); var record = recordItem.getRecord(); @@ -168,8 +260,34 @@ var record = recordItem.getRecord(); } @ParameterizedTest - @ValueSource(booleans = {true, false}) - void contractUpdateAllToExisting(boolean updateMemoWrapperOrMemo) { + @EnumSource(ContractIdType.class) + void contractUpdateAllToExisting(ContractIdType contractIdType) { + // first create the contract + SetupResult setupResult = setupContract(CONTRACT_ID, contractIdType, true, true, c -> c.obtainerId(null)); + Contract contract = setupResult.contract; + + // now update + Transaction transaction = contractUpdateAllTransaction(setupResult.protoContractId, true); + TransactionBody transactionBody = getTransactionBody(transaction); + TransactionRecord record = createOrUpdateRecord(transactionBody); + ContractUpdateTransactionBody contractUpdateTransactionBody = transactionBody.getContractUpdateInstance(); + + parseRecordItemAndCommit(new RecordItem(transaction, record)); + + assertAll( + () -> assertEquals(1, transactionRepository.count()), + () -> assertEntities(setupResult.contract.toEntityId()), + () -> assertEquals(0, contractResultRepository.count()), + () -> assertEquals(3, cryptoTransferRepository.count()), + () -> assertTransactionAndRecord(transactionBody, record), + () -> assertContractEntity(contractUpdateTransactionBody, record.getConsensusTimestamp()) + .returns(contract.getCreatedTimestamp(), Contract::getCreatedTimestamp) + .returns(contract.getFileId(), Contract::getFileId) // FileId is ignored on updates by HAPI + ); + } + + @Test + void contractUpdateAllWithMemoToExisting() { // first create the contract EntityId contractId = EntityId.of(CONTRACT_ID); Contract contract = domainBuilder.contract() @@ -177,7 +295,7 @@ void contractUpdateAllToExisting(boolean updateMemoWrapperOrMemo) { .persist(); // now update - Transaction transaction = contractUpdateAllTransaction(updateMemoWrapperOrMemo); + Transaction transaction = contractUpdateAllTransaction(false); TransactionBody transactionBody = getTransactionBody(transaction); TransactionRecord record = createOrUpdateRecord(transactionBody); ContractUpdateTransactionBody contractUpdateTransactionBody = transactionBody.getContractUpdateInstance(); @@ -197,9 +315,11 @@ void contractUpdateAllToExisting(boolean updateMemoWrapperOrMemo) { } @ParameterizedTest - @ValueSource(booleans = {true, false}) - void contractUpdateAllToNew(boolean updateMemoWrapperOrMemo) { - Transaction transaction = contractUpdateAllTransaction(updateMemoWrapperOrMemo); + @EnumSource(value = ContractIdType.class, names = {"PLAIN", "PARSABLE_EVM"}) + void contractUpdateAllToNew(ContractIdType contractIdType) { + SetupResult setupResult = setupContract(CONTRACT_ID, contractIdType, false, false, c -> c.obtainerId(null)); + + Transaction transaction = contractUpdateAllTransaction(setupResult.protoContractId, true); TransactionBody transactionBody = getTransactionBody(transaction); TransactionRecord record = createOrUpdateRecord(transactionBody); ContractUpdateTransactionBody contractUpdateTransactionBody = transactionBody.getContractUpdateInstance(); @@ -208,7 +328,7 @@ void contractUpdateAllToNew(boolean updateMemoWrapperOrMemo) { assertAll( () -> assertEquals(1, transactionRepository.count()), - () -> assertEntities(EntityId.of(CONTRACT_ID)), + () -> assertEntities(setupResult.contract.toEntityId()), () -> assertEquals(0, contractResultRepository.count()), () -> assertEquals(3, cryptoTransferRepository.count()), () -> assertTransactionAndRecord(transactionBody, record), @@ -219,13 +339,53 @@ void contractUpdateAllToNew(boolean updateMemoWrapperOrMemo) { } @Test - void contractUpdateAllToExistingInvalidTransaction() { - EntityId contractId = EntityId.of(CONTRACT_ID); - Contract contract = domainBuilder.contract() - .customize(c -> c.id(contractId.getId()).num(contractId.getEntityNum())) - .persist(); + void contractUpdateAllToNewCreate2EvmAddress() { + SetupResult setupResult = setupContract(CONTRACT_ID, ContractIdType.CREATE2_EVM, false, false); - Transaction transaction = contractUpdateAllTransaction(true); + Transaction transaction = contractUpdateAllTransaction(setupResult.protoContractId, true); + TransactionBody transactionBody = getTransactionBody(transaction); + TransactionRecord record = createOrUpdateRecord(transactionBody); + + parseRecordItemAndCommit(new RecordItem(transaction, record)); + + var dbTransaction = getDbTransaction(record.getConsensusTimestamp()); + assertAll( + () -> assertEquals(1, transactionRepository.count()), + () -> assertEntities(setupResult.contract.toEntityId()), + () -> assertEquals(0, contractResultRepository.count()), + () -> assertEquals(3, cryptoTransferRepository.count()), + () -> assertTransactionAndRecord(transactionBody, record), + () -> assertThat(dbTransaction.getEntityId()).isEqualTo(setupResult.contract.toEntityId()) + ); + } + + @Test + void contractUpdateAllWithMemoToNew() { + Transaction transaction = contractUpdateAllTransaction(false); + TransactionBody transactionBody = getTransactionBody(transaction); + TransactionRecord record = createOrUpdateRecord(transactionBody); + ContractUpdateTransactionBody contractUpdateTransactionBody = transactionBody.getContractUpdateInstance(); + + parseRecordItemAndCommit(new RecordItem(transaction, record)); + + assertAll( + () -> assertEquals(1, transactionRepository.count()), + () -> assertEntities(EntityId.of(CONTRACT_ID)), + () -> assertEquals(0, contractResultRepository.count()), + () -> assertEquals(3, cryptoTransferRepository.count()), + () -> assertTransactionAndRecord(transactionBody, record), + () -> assertContractEntity(contractUpdateTransactionBody, record.getConsensusTimestamp()) + .returns(null, Contract::getCreatedTimestamp) + .returns(null, Contract::getFileId) // FileId is ignored on updates by HAPI + ); + } + + @ParameterizedTest + @EnumSource(value = ContractIdType.class) + void contractUpdateAllToExistingInvalidTransaction(ContractIdType contractIdType) { + SetupResult setupResult = setupContract(CONTRACT_ID, contractIdType, true, true); + + Transaction transaction = contractUpdateAllTransaction(setupResult.protoContractId, true); TransactionBody transactionBody = getTransactionBody(transaction); TransactionRecord record = createOrUpdateRecord(transactionBody, ResponseCodeEnum.INSUFFICIENT_ACCOUNT_BALANCE); RecordItem recordItem = new RecordItem(transaction, record); @@ -237,18 +397,16 @@ void contractUpdateAllToExistingInvalidTransaction() { () -> assertEquals(0, contractResultRepository.count()), () -> assertEquals(3, cryptoTransferRepository.count()), () -> assertTransactionAndRecord(transactionBody, record), - () -> assertThat(contractRepository.findAll()).containsExactly(contract) + () -> assertThat(contractRepository.findAll()).containsExactly(setupResult.contract) ); } - @Test - void contractDeleteToExisting() { - EntityId contractId = EntityId.of(CONTRACT_ID); - Contract contract = domainBuilder.contract() - .customize(c -> c.obtainerId(null).id(contractId.getId()).num(contractId.getEntityNum())) - .persist(); + @ParameterizedTest + @EnumSource(ContractIdType.class) + void contractDeleteToExisting(ContractIdType contractIdType) { + SetupResult setupResult = setupContract(CONTRACT_ID, contractIdType, true, true); - Transaction transaction = contractDeleteTransaction(); + Transaction transaction = contractDeleteTransaction(setupResult.protoContractId); TransactionBody transactionBody = getTransactionBody(transaction); TransactionRecord record = createOrUpdateRecord(transactionBody); RecordItem recordItem = new RecordItem(transaction, record); @@ -261,7 +419,7 @@ void contractDeleteToExisting() { () -> assertEquals(1, transactionRepository.count()), () -> assertEquals(0, contractResultRepository.count()), () -> assertEquals(3, cryptoTransferRepository.count()), - () -> assertEntities(contractId), + () -> assertEntities(setupResult.contract.toEntityId()), () -> assertTransactionAndRecord(transactionBody, record), () -> assertThat(dbContractEntity) .isNotNull() @@ -270,13 +428,17 @@ void contractDeleteToExisting() { .returns(EntityId.of(PAYER), Contract::getObtainerId) .usingRecursiveComparison() .ignoringFields("deleted", "obtainerId", "timestampRange") - .isEqualTo(contract) + .isEqualTo(setupResult.contract) ); } - @Test - void contractDeleteToNew() { - Transaction transaction = contractDeleteTransaction(); + @ParameterizedTest + @EnumSource(value = ContractIdType.class, names = {"PLAIN", "PARSABLE_EVM"}) + void contractDeleteToNew(ContractIdType contractIdType) { + // The contract is not in db, it should still work for PLAIN and PARSABLE_EVM + SetupResult setupResult = setupContract(CONTRACT_ID, contractIdType, false, false); + + Transaction transaction = contractDeleteTransaction(setupResult.protoContractId); TransactionBody transactionBody = getTransactionBody(transaction); TransactionRecord record = createOrUpdateRecord(transactionBody); RecordItem recordItem = new RecordItem(transaction, record); @@ -303,6 +465,28 @@ void contractDeleteToNew() { ); } + @Test + void contractDeleteToNewCreate2EvmAddress() { + SetupResult setupResult = setupContract(CONTRACT_ID, ContractIdType.CREATE2_EVM, false, false); + + Transaction transaction = contractDeleteTransaction(setupResult.protoContractId); + TransactionBody transactionBody = getTransactionBody(transaction); + TransactionRecord record = createOrUpdateRecord(transactionBody); + RecordItem recordItem = new RecordItem(transaction, record); + + parseRecordItemAndCommit(recordItem); + + var dbTransaction = getDbTransaction(record.getConsensusTimestamp()); + assertAll( + () -> assertEquals(1, transactionRepository.count()), + () -> assertEquals(0, contractResultRepository.count()), + () -> assertEquals(3, cryptoTransferRepository.count()), + () -> assertEntities(setupResult.contract.toEntityId()), + () -> assertTransactionAndRecord(transactionBody, record), + () -> assertThat(dbTransaction.getEntityId()).isEqualTo(setupResult.contract.toEntityId()) + ); + } + @Test void contractDeleteToNewInvalidTransaction() { Transaction transaction = contractDeleteTransaction(); @@ -321,15 +505,13 @@ void contractDeleteToNewInvalidTransaction() { ); } - @Test - void contractCallToExisting() { - EntityId parentId = EntityId.of(CONTRACT_ID); - Contract parent = domainBuilder.contract() - .customize(c -> c.id(parentId.getId()).num(parentId.getEntityNum())) - .persist(); + @ParameterizedTest + @EnumSource(ContractIdType.class) + void contractCallToExisting(ContractIdType contractIdType) { + SetupResult setupResult = setupContract(CONTRACT_ID, contractIdType, true, true); // now call - Transaction transaction = contractCallTransaction(); + Transaction transaction = contractCallTransaction(setupResult.protoContractId); TransactionBody transactionBody = getTransactionBody(transaction); TransactionRecord record = callRecord(transactionBody); ContractCallTransactionBody contractCallTransactionBody = transactionBody.getContractCall(); @@ -344,13 +526,71 @@ void contractCallToExisting() { () -> assertEntities(EntityId.of(CONTRACT_ID), EntityId.of(CREATED_CONTRACT_ID)), () -> assertTransactionAndRecord(transactionBody, record), () -> assertContractCallResult(contractCallTransactionBody, record), - () -> assertThat(contractRepository.findById(parentId.getId())).get().isEqualTo(parent) // No change + () -> assertThat(contractRepository.findAll()).contains(setupResult.contract) ); } - @Test - void contractCallToNew() { - Transaction transaction = contractCallTransaction(); + @ParameterizedTest + @EnumSource(ContractIdType.class) + void contractCallToExistingWithChildContractCreate(ContractIdType contractIdType) { + // given contractCall with child contractCreate + var setupResult = setupContract(CONTRACT_ID, contractIdType, true, true); + var parentId = setupResult.contract.toEntityId(); + + var parentRecordItem = recordItemBuilder.contractCall() + .receipt(r -> r.setContractID(CONTRACT_ID)) + .transactionBody(b -> b.setContractID(setupResult.protoContractId)) + .record(r -> r.clearContractCallResult() + .setContractCallResult(recordItemBuilder.contractFunctionResult(CONTRACT_ID))) + .hapiVersion(HAPI_VERSION_0_23_0) + .build(); + + var childEvmAddress = domainBuilder.create2EvmAddress(); + var record = parentRecordItem.getRecord(); + var childConsensusTimestamp = TestUtils.toTimestamp(parentRecordItem.getConsensusTimestamp() + 1); + var childContractId = record.getContractCallResult().getCreatedContractIDs(0); + var childTransactionId = record.getTransactionID().toBuilder().setNonce(1).build(); + var childRecordItem = recordItemBuilder.contractCreate(childContractId) + .record(r -> r.setConsensusTimestamp(childConsensusTimestamp) + .setContractCreateResult(r.getContractCreateResultBuilder() + .clearCreatedContractIDs() + .clearStateChanges() + .setEvmAddress(BytesValue.of(DomainUtils.fromBytes(childEvmAddress)))) + .setTransactionID(childTransactionId)) + .hapiVersion(HAPI_VERSION_0_23_0) + .build(); + + // when + parseRecordItemsAndCommit(List.of(parentRecordItem, childRecordItem)); + + // then + var parentTransactionBody = parentRecordItem.getTransactionBody().getContractCall(); + var childTransactionBody = childRecordItem.getTransactionBody().getContractCreateInstance(); + assertAll( + () -> assertEquals(2, transactionRepository.count()), + () -> assertEquals(2, contractRepository.count()), + () -> assertEquals(2, contractResultRepository.count()), + () -> assertEquals(6, cryptoTransferRepository.count()), + () -> assertEntities(parentId, EntityId.of(childContractId)), + () -> assertTransactionAndRecord(parentRecordItem.getTransactionBody(), parentRecordItem.getRecord()), + () -> assertTransactionAndRecord(childRecordItem.getTransactionBody(), childRecordItem.getRecord()), + () -> assertThat(contractRepository.findAll()).contains(setupResult.contract), + () -> assertCreatedContract(childRecordItem), + () -> assertContractCallResult(parentTransactionBody, parentRecordItem.getRecord()), + () -> assertContractCreateResult(childTransactionBody, childRecordItem.getRecord()) + ); + } + + @ParameterizedTest + @EnumSource(ContractIdType.class) + void contractCallToNew(ContractIdType contractIdType) { + // The contract is not in db, it should still work. Note for the create2 evm address, + // ContractCallTransactionHandler will get the correct plain contractId from the transaction record instead + // only cache the create2 evm address to verify it later + SetupResult setupResult = setupContract(CONTRACT_ID, contractIdType, false, + contractIdType == ContractIdType.CREATE2_EVM); + + Transaction transaction = contractCallTransaction(setupResult.protoContractId); TransactionBody transactionBody = getTransactionBody(transaction); TransactionRecord record = callRecord(transactionBody); ContractCallTransactionBody contractCallTransactionBody = transactionBody.getContractCall(); @@ -466,7 +706,11 @@ void contractCallDoNotPersist() { void cryptoTransferBadContractId() { Transaction transaction = contractCallTransaction(ContractID.newBuilder().setContractNum(-1L).build()); var transactionBody = getTransactionBody(transaction); - TransactionRecord record = callRecord(transactionBody, ResponseCodeEnum.INVALID_CONTRACT_ID); + TransactionRecord record = buildTransactionRecord(recordBuilder -> { + var contractFunctionResult = recordBuilder.getContractCallResultBuilder(); + buildContractFunctionResult(contractFunctionResult); + contractFunctionResult.removeCreatedContractIDs(0); // Only contract create can contain parent ID + }, transactionBody, ResponseCodeEnum.INVALID_CONTRACT_ID.getNumber()); parseRecordItemAndCommit(new RecordItem(transaction, record)); @@ -509,6 +753,9 @@ private void assertContractEntity(RecordItem recordItem) { var transactionBody = recordItem.getTransactionBody().getContractCreateInstance(); var adminKey = transactionBody.getAdminKey().toByteArray(); var transaction = transactionRepository.findById(createdTimestamp).get(); + var contractCreateResult = recordItem.getRecord().getContractCreateResult(); + byte[] evmAddress = contractCreateResult.hasEvmAddress() ? + DomainUtils.toBytes(contractCreateResult.getEvmAddress().getValue()) : null; EntityId entityId = transaction.getEntityId(); Contract contract = getEntity(entityId); @@ -521,6 +768,7 @@ private void assertContractEntity(RecordItem recordItem) { .returns(transactionBody.getAutoRenewPeriod().getSeconds(), Contract::getAutoRenewPeriod) .returns(createdTimestamp, Contract::getCreatedTimestamp) .returns(false, Contract::getDeleted) + .returns(evmAddress, Contract::getEvmAddress) .returns(null, Contract::getExpirationTimestamp) .returns(entityId.getId(), Contract::getId) .returns(EntityId.of(transactionBody.getFileID()), Contract::getFileId) @@ -538,11 +786,15 @@ private void assertContractEntity(RecordItem recordItem) { } private void assertCreatedContract(RecordItem recordItem) { + var contractCreateResult = recordItem.getRecord().getContractCreateResult(); + byte[] evmAddress = contractCreateResult.hasEvmAddress() ? + DomainUtils.toBytes(contractCreateResult.getEvmAddress().getValue()) : null; EntityId createdId = EntityId.of(recordItem.getRecord().getReceipt().getContractID()); assertThat(contractRepository.findById(createdId.getId())) .get() .returns(recordItem.getConsensusTimestamp(), Contract::getCreatedTimestamp) .returns(false, Contract::getDeleted) + .returns(evmAddress, Contract::getEvmAddress) .returns(createdId.getId(), Contract::getId) .returns(recordItem.getConsensusTimestamp(), Contract::getModifiedTimestamp) .returns(createdId.getEntityNum(), Contract::getNum) @@ -572,51 +824,53 @@ private ObjectAssert assertContractEntity(ContractUpdateTransactionBod private void assertContractCreateResult(ContractCreateTransactionBody transactionBody, TransactionRecord record) { long consensusTimestamp = DomainUtils.timestampInNanosMax(record.getConsensusTimestamp()); + TransactionReceipt receipt = record.getReceipt(); ContractFunctionResult result = record.getContractCreateResult(); ObjectAssert contractResult = assertThat(contractResultRepository.findAll()) + .filteredOn(c -> c.getConsensusTimestamp().equals(consensusTimestamp)) .hasSize(1) .first() .returns(transactionBody.getInitialBalance(), ContractResult::getAmount) .returns(consensusTimestamp, ContractResult::getConsensusTimestamp) - .returns(EntityId.of(record.getReceipt().getContractID()), ContractResult::getContractId) + .returns(EntityId.of(receipt.getContractID()), ContractResult::getContractId) .returns(toBytes(transactionBody.getConstructorParameters()), ContractResult::getFunctionParameters) .returns(transactionBody.getGas(), ContractResult::getGasLimit); - var status = record.getReceipt().getStatus(); - if (status == ResponseCodeEnum.SUCCESS) { - contractResult - .returns(EntityId.of(record.getReceipt().getContractID()), ContractResult::getContractId); + if (receipt.getStatus() == ResponseCodeEnum.SUCCESS) { + contractResult.returns(EntityId.of(receipt.getContractID()), ContractResult::getContractId); } assertContractResult(consensusTimestamp, result, result.getLogInfoList(), contractResult, - result.getStateChangesList(), - status == ResponseCodeEnum.SUCCESS ? EntityId.of(result.getContractID()) : null); + result.getStateChangesList()); } private void assertContractCallResult(ContractCallTransactionBody transactionBody, TransactionRecord record) { long consensusTimestamp = DomainUtils.timestampInNanosMax(record.getConsensusTimestamp()); ContractFunctionResult result = record.getContractCallResult(); + // get the corresponding entity id from the local cache, fall back to parseContractId if not found. + ContractID protoContractId = transactionBody.getContractID(); + EntityId contractId = contractIds.getOrDefault(protoContractId, parseContractId(protoContractId)); + ObjectAssert contractResult = assertThat(contractResultRepository.findAll()) .filteredOn(c -> c.getConsensusTimestamp().equals(consensusTimestamp)) .hasSize(1) .first() .returns(transactionBody.getAmount(), ContractResult::getAmount) + .returns(contractId, ContractResult::getContractId) .returns(consensusTimestamp, ContractResult::getConsensusTimestamp) - .returns(EntityId.of(transactionBody.getContractID()), ContractResult::getContractId) .returns(toBytes(transactionBody.getFunctionParameters()), ContractResult::getFunctionParameters) .returns(transactionBody.getGas(), ContractResult::getGasLimit); assertContractResult(consensusTimestamp, result, result.getLogInfoList(), contractResult, - result.getStateChangesList(), EntityId.of(result.getContractID())); + result.getStateChangesList()); } private void assertContractResult(long consensusTimestamp, ContractFunctionResult result, List logInfoList, ObjectAssert contractResult, - List stageChangeList, - EntityId rootContractId) { + List stageChangeList) { List createdContractIds = result.getCreatedContractIDsList() .stream() .map(ContractID::getContractNum) @@ -650,10 +904,7 @@ private void assertContractResult(long consensusTimestamp, ContractFunctionResul .returns(Utility.getTopic(logInfo, 3), ContractLog::getTopic3); } - for (int i = 0; i < stageChangeList.size(); i++) { - int index = i; - com.hederahashgraph.api.proto.java.ContractStateChange contractStateChangeInfo = stageChangeList.get(i); - + for (var contractStateChangeInfo : stageChangeList) { EntityId contractId = EntityId.of(contractStateChangeInfo.getContractID()); for (int j = 0; j < contractStateChangeInfo.getStorageChangesCount(); ++j) { StorageChange storageChange = contractStateChangeInfo.getStorageChanges(j); @@ -681,7 +932,6 @@ private void assertContractResult(long consensusTimestamp, ContractFunctionResul private void assertPartialContractCreateResult(ContractCreateTransactionBody transactionBody, TransactionRecord record) { long consensusTimestamp = DomainUtils.timestampInNanosMax(record.getConsensusTimestamp()); - ContractFunctionResult result = record.getContractCreateResult(); ObjectAssert contractResult = assertThat(contractResultRepository.findAll()) .hasSize(1) @@ -815,14 +1065,35 @@ private Transaction contractUpdateAllTransaction(boolean setMemoWrapperOrMemo) { }); } - private Transaction contractDeleteTransaction() { + private Transaction contractUpdateAllTransaction(ContractID contractId, boolean setMemoWrapperOrMemo) { + return buildTransaction(builder -> { + ContractUpdateTransactionBody.Builder contractUpdate = builder.getContractUpdateInstanceBuilder(); + contractUpdate.setAdminKey(keyFromString(KEY)); + contractUpdate.setAutoRenewPeriod(Duration.newBuilder().setSeconds(400).build()); + contractUpdate.setContractID(contractId); + contractUpdate.setExpirationTime(Timestamp.newBuilder().setSeconds(8000).setNanos(10).build()); + contractUpdate.setFileID(FileID.newBuilder().setShardNum(0).setRealmNum(0).setFileNum(2000).build()); + if (setMemoWrapperOrMemo) { + contractUpdate.setMemoWrapper(StringValue.of("contract update memo")); + } else { + contractUpdate.setMemo("contract update memo"); + } + contractUpdate.setProxyAccountID(PROXY_UPDATE); + }); + } + + private Transaction contractDeleteTransaction(ContractID contractId) { return buildTransaction(builder -> { ContractDeleteTransactionBody.Builder contractDelete = builder.getContractDeleteInstanceBuilder(); - contractDelete.setContractID(CONTRACT_ID); + contractDelete.setContractID(contractId); contractDelete.setTransferAccountID(PAYER); }); } + private Transaction contractDeleteTransaction() { + return contractDeleteTransaction(CONTRACT_ID); + } + private Transaction contractCallTransaction() { return contractCallTransaction(CONTRACT_ID); } @@ -851,4 +1122,77 @@ private String getMemoFromContractUpdateTransactionBody(ContractUpdateTransactio return null; } } + + private ContractID getContractId(ContractID contractId, byte[] evmAddress) { + if (evmAddress == null) { + return contractId; + } + + return contractId.toBuilder().clearContractNum().setEvmAddress(ByteString.copyFrom(evmAddress)).build(); + } + + private byte[] getEvmAddress(ContractIdType contractIdType, EntityId contractId) { + switch (contractIdType) { + case PARSABLE_EVM: + return DomainUtils.toEvmAddress(contractId); + case CREATE2_EVM: + return domainBuilder.create2EvmAddress(); + default: + return null; + } + } + + private EntityId parseContractId(ContractID contractId) { + switch (contractId.getContractCase()) { + case CONTRACTNUM: + return EntityId.of(contractId); + case EVM_ADDRESS: + ByteBuffer buffer = ByteBuffer.wrap(DomainUtils.toBytes(contractId.getEvmAddress())); + long shard = buffer.getInt(); + long realm = buffer.getLong(); + long num = buffer.getLong(); + if (shard == contractId.getShardNum() && realm == contractId.getRealmNum()) { + return EntityId.of(shard, realm, num, EntityType.CONTRACT); + } + + // the create2 evm address + return null; + default: + return null; + } + } + + private SetupResult setupContract(ContractID contractId, ContractIdType contractIdType, boolean persist, + boolean cache) { + return setupContract(contractId, contractIdType, persist, cache, null); + } + + private SetupResult setupContract(ContractID contractId, ContractIdType contractIdType, boolean persist, + boolean cache, Consumer customizer) { + EntityId entityId = EntityId.of(contractId); + byte[] evmAddress = getEvmAddress(contractIdType, entityId); + ContractID protoContractId = getContractId(CONTRACT_ID, evmAddress); + var builder = domainBuilder.contract() + .customize(c -> c.evmAddress(evmAddress).id(entityId.getId()).num(entityId.getEntityNum())); + if (customizer != null) { + builder.customize(customizer); + } + Contract contract = persist ? builder.persist() : builder.get(); + if (cache) { + contractIds.put(protoContractId, entityId); + } + return new SetupResult(contract, protoContractId); + } + + enum ContractIdType { + PLAIN, + PARSABLE_EVM, + CREATE2_EVM, + } + + @Value + private static class SetupResult { + Contract contract; + ContractID protoContractId; + } } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerCryptoTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerCryptoTest.java index 7f8b565f9ca..e406963fe22 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerCryptoTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerCryptoTest.java @@ -74,6 +74,7 @@ class EntityRecordItemListenerCryptoTest extends AbstractEntityRecordItemListene void before() { entityProperties.getPersist().setClaims(true); entityProperties.getPersist().setCryptoTransferAmounts(true); + entityProperties.getPersist().setTransactionBytes(false); } @Test @@ -700,7 +701,7 @@ private void testRawBytes(Transaction transaction, byte[] expectedBytes) { TransactionRecord record = transactionRecordSuccess(transactionBody); // when - parseRecordItemAndCommit(new RecordItem(transaction.toByteArray(), record.toByteArray())); + parseRecordItemAndCommit(new RecordItem(transaction, record)); // then var dbTransaction = getDbTransaction(record.getConsensusTimestamp()); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerNonFeeTransferTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerNonFeeTransferTest.java index 18321ab34b5..2f4bc7088ba 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerNonFeeTransferTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerNonFeeTransferTest.java @@ -244,7 +244,10 @@ private Transaction contractCall() { private void contractCallWithTransferList(TransferList.Builder transferList) { var transaction = contractCall(); var transactionBody = getTransactionBody(transaction); - var record = transactionRecordSuccess(transactionBody, transferList).build(); + var recordBuilder = transactionRecordSuccess(transactionBody, transferList); + var contractCallResult = recordBuilder.getContractCallResultBuilder() + .setContractID(ContractID.newBuilder().setContractNum(NEW_CONTRACT_NUM)); + var record = recordBuilder.setContractCallResult(contractCallResult).build(); expectedTransactions.add(new TransactionContext(transaction, record)); parseRecordItemAndCommit(new RecordItem(transaction, record)); @@ -265,7 +268,10 @@ private Transaction contractCreate() { private void contractCreateWithTransferList(TransferList.Builder transferList) { var transaction = contractCreate(); var transactionBody = getTransactionBody(transaction); - var record = transactionRecordSuccess(transactionBody, transferList).build(); + var recordBuilder = transactionRecordSuccess(transactionBody, transferList); + var contractCreateResult = recordBuilder.getContractCreateResultBuilder() + .addCreatedContractIDs(ContractID.newBuilder().setContractNum(NEW_CONTRACT_NUM)); + var record = recordBuilder.setContractCreateResult(contractCreateResult).build(); expectedTransactions.add(new TransactionContext(transaction, record)); parseRecordItemAndCommit(new RecordItem(transaction, record)); @@ -301,8 +307,8 @@ private Transaction cryptoTransfer() { private Transaction cryptoTransfer(long entityNum) { var nonFeeTransfers = TransferList.newBuilder() - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - TRANSFER_AMOUNT)) - .addAccountAmounts(accountAmount(entityNum, 0 - TRANSFER_AMOUNT)); + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -TRANSFER_AMOUNT)) + .addAccountAmounts(accountAmount(entityNum, -TRANSFER_AMOUNT)); var inner = CryptoTransferTransactionBody.newBuilder() .setTransfers(nonFeeTransfers); @@ -456,9 +462,9 @@ private TransferList.Builder transferListForContractCallAggregated() { private TransferList.Builder transferListForContractCreateItemized() { return TransferList.newBuilder() - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - TRANSFER_AMOUNT)) - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - CHARGED_FEE)) - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - THRESHOLD_RECORD_FEE)) + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -TRANSFER_AMOUNT)) + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -CHARGED_FEE)) + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -THRESHOLD_RECORD_FEE)) .addAccountAmounts(accountAmount(NEW_CONTRACT_NUM, TRANSFER_AMOUNT)) .addAccountAmounts(accountAmount(NODE_ACCOUNT_NUM, NODE_FEE)) .addAccountAmounts(accountAmount(TREASURY_ACCOUNT_NUM, NETWORK_FEE)) @@ -468,7 +474,7 @@ private TransferList.Builder transferListForContractCreateItemized() { private TransferList.Builder transferListForContractCreateAggregated() { return TransferList.newBuilder() - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - TRANSFER_AMOUNT - CHARGED_FEE)) + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -TRANSFER_AMOUNT - CHARGED_FEE)) .addAccountAmounts(accountAmount(NEW_CONTRACT_NUM, TRANSFER_AMOUNT)) .addAccountAmounts(accountAmount(NODE_ACCOUNT_NUM, NODE_FEE)) .addAccountAmounts(accountAmount(TREASURY_ACCOUNT_NUM, NETWORK_SERVICE_FEE)); @@ -476,9 +482,9 @@ private TransferList.Builder transferListForContractCreateAggregated() { private TransferList.Builder transferListForCryptoCreateItemized() { return TransferList.newBuilder() - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - TRANSFER_AMOUNT)) - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - NODE_FEE)) - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - NETWORK_SERVICE_FEE)) + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -TRANSFER_AMOUNT)) + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -NODE_FEE)) + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -NETWORK_SERVICE_FEE)) .addAccountAmounts(accountAmount(NEW_ACCOUNT_NUM, TRANSFER_AMOUNT)) .addAccountAmounts(accountAmount(NODE_ACCOUNT_NUM, NODE_FEE)) .addAccountAmounts(accountAmount(TREASURY_ACCOUNT_NUM, NETWORK_FEE)) @@ -487,7 +493,7 @@ private TransferList.Builder transferListForCryptoCreateItemized() { private TransferList.Builder transferListForCryptoCreateAggregated() { return TransferList.newBuilder() - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - TRANSFER_AMOUNT - CHARGED_FEE)) + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -TRANSFER_AMOUNT - CHARGED_FEE)) .addAccountAmounts(accountAmount(NEW_ACCOUNT_NUM, TRANSFER_AMOUNT)) .addAccountAmounts(accountAmount(NODE_ACCOUNT_NUM, NODE_FEE)) .addAccountAmounts(accountAmount(TREASURY_ACCOUNT_NUM, NETWORK_SERVICE_FEE)); @@ -505,13 +511,13 @@ private TransferList.Builder transferListForCryptoTransferAggregated() { private TransferList.Builder transferListForFailedCryptoTransferItemized() { return TransferList.newBuilder() - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - NODE_FEE)) + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -NODE_FEE)) .addAccountAmounts(accountAmount(NODE_ACCOUNT_NUM, NODE_FEE)); } private TransferList.Builder transferListForFailedCryptoTransferAggregated() { return TransferList.newBuilder() - .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, 0 - NODE_FEE)) + .addAccountAmounts(accountAmount(PAYER_ACCOUNT_NUM, -NODE_FEE)) .addAccountAmounts(accountAmount(NODE_ACCOUNT_NUM, NODE_FEE)); } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/sql/SqlEntityListenerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/sql/SqlEntityListenerTest.java index 7a682e71590..9595b90b5e0 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/sql/SqlEntityListenerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/sql/SqlEntityListenerTest.java @@ -168,7 +168,7 @@ void isEnabled() { void onContract() { // given Contract contract1 = domainBuilder.contract().get(); - Contract contract2 = domainBuilder.contract().get(); + Contract contract2 = domainBuilder.contract().customize(c -> c.evmAddress(null)).get(); // when sqlEntityListener.onContract(contract1); @@ -189,6 +189,7 @@ void onContractHistory(int commitIndex) { Contract contractUpdate = contractCreate.toEntityId().toEntity(); contractUpdate.setAutoRenewPeriod(30L); + contractUpdate.setEvmAddress(contractCreate.getEvmAddress()); contractUpdate.setExpirationTimestamp(500L); contractUpdate.setKey(domainBuilder.key()); contractUpdate.setMemo("updated"); @@ -197,6 +198,7 @@ void onContractHistory(int commitIndex) { Contract contractDelete = contractCreate.toEntityId().toEntity(); contractDelete.setDeleted(true); + contractDelete.setEvmAddress(contractCreate.getEvmAddress()); contractDelete.setModifiedTimestamp(contractCreate.getModifiedTimestamp() + 2); contractDelete.setObtainerId(EntityId.of(999L, EntityType.CONTRACT)); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/pubsub/PubSubRecordItemListenerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/pubsub/PubSubRecordItemListenerTest.java index 9878f5b67b1..b2950100647 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/pubsub/PubSubRecordItemListenerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/pubsub/PubSubRecordItemListenerTest.java @@ -71,7 +71,6 @@ import com.hedera.mirror.importer.exception.ParserException; import com.hedera.mirror.importer.parser.domain.PubSubMessage; import com.hedera.mirror.importer.parser.record.NonFeeTransferExtractionStrategy; -import com.hedera.mirror.importer.parser.record.NonFeeTransferExtractionStrategyImpl; import com.hedera.mirror.importer.parser.record.transactionhandler.TransactionHandler; import com.hedera.mirror.importer.parser.record.transactionhandler.TransactionHandlerFactory; import com.hedera.mirror.importer.repository.FileDataRepository; @@ -84,10 +83,7 @@ class PubSubRecordItemListenerTest { .setConsensusTimestamp(Utility.instantToTimestamp(Instant.ofEpochSecond(0L, CONSENSUS_TIMESTAMP))) .setReceipt(TransactionReceipt.newBuilder().setStatus(ResponseCodeEnum.SUCCESS).build()) .build(); - private static final byte[] DEFAULT_RECORD_BYTES = DEFAULT_RECORD.toByteArray(); private static final FileID ADDRESS_BOOK_FILE_ID = FileID.newBuilder().setFileNum(102).build(); - private static final NonFeeTransferExtractionStrategy nonFeeTransferExtractionStrategy = - new NonFeeTransferExtractionStrategyImpl(); private static final NodeAddressBook UPDATED = addressBook(3); @@ -100,6 +96,9 @@ class PubSubRecordItemListenerTest { @Mock private FileDataRepository fileDataRepository; + @Mock + private NonFeeTransferExtractionStrategy nonFeeTransferExtractionStrategy; + @Mock private TransactionHandler transactionHandler; @@ -162,7 +161,7 @@ void testPubSubMessage() throws Exception { Transaction transaction = buildTransaction(builder -> builder.setConsensusSubmitMessage(submitMessage)); // when doReturn(topicIdEntity).when(transactionHandler).getEntity(any()); - pubSubRecordItemListener.onItem(new RecordItem(transaction.toByteArray(), DEFAULT_RECORD_BYTES)); + pubSubRecordItemListener.onItem(new RecordItem(transaction, DEFAULT_RECORD)); // then var pubSubMessage = assertPubSubMessage(buildPubSubTransaction(transaction), 1); @@ -183,7 +182,7 @@ void testPubSubMessageNullEntityId() throws Exception { Transaction transaction = buildTransaction(builder -> builder.setConsensusSubmitMessage(submitMessage)); // when doReturn(null).when(transactionHandler).getEntity(any()); - pubSubRecordItemListener.onItem(new RecordItem(transaction.toByteArray(), DEFAULT_RECORD_BYTES)); + pubSubRecordItemListener.onItem(new RecordItem(transaction, DEFAULT_RECORD)); // then var pubSubMessage = assertPubSubMessage(buildPubSubTransaction(transaction), 1); @@ -204,9 +203,12 @@ void testPubSubMessageWithNonFeeTransferAndNullEntityId() throws Exception { .build()) .build(); Transaction transaction = buildTransaction(builder -> builder.setCryptoTransfer(cryptoTransfer)); + var recordItem = new RecordItem(transaction, DEFAULT_RECORD); + when(nonFeeTransferExtractionStrategy.extractNonFeeTransfers(recordItem.getTransactionBody(), + recordItem.getRecord())).thenReturn(cryptoTransfer.getTransfers().getAccountAmountsList()); // when - pubSubRecordItemListener.onItem(new RecordItem(transaction.toByteArray(), DEFAULT_RECORD_BYTES)); + pubSubRecordItemListener.onItem(recordItem); // then var pubSubMessage = assertPubSubMessage(buildPubSubTransaction(transaction), 1); @@ -227,8 +229,7 @@ void testNonRetryableError() { // then assertThatThrownBy( - () -> pubSubRecordItemListener.onItem( - new RecordItem(transaction.toByteArray(), DEFAULT_RECORD_BYTES))) + () -> pubSubRecordItemListener.onItem(new RecordItem(transaction, DEFAULT_RECORD))) .isInstanceOf(ParserException.class) .hasMessageContaining("Error sending transaction to pubsub"); verify(messageChannel, times(1)).send(any()); @@ -248,7 +249,7 @@ void testSendRetries() throws Exception { .thenThrow(MessageTimeoutException.class) .thenThrow(MessageTimeoutException.class) .thenReturn(true); - pubSubRecordItemListener.onItem(new RecordItem(transaction.toByteArray(), DEFAULT_RECORD_BYTES)); + pubSubRecordItemListener.onItem(new RecordItem(transaction, DEFAULT_RECORD)); // then var pubSubMessage = assertPubSubMessage(buildPubSubTransaction(transaction), 3); @@ -269,8 +270,8 @@ void testNetworkAddressBookAppend() throws Exception { // when EntityId entityId = EntityId.of(ADDRESS_BOOK_FILE_ID); - doReturn(EntityId.of(ADDRESS_BOOK_FILE_ID)).when(transactionHandler).getEntity(any()); - pubSubRecordItemListener.onItem(new RecordItem(transaction.toByteArray(), DEFAULT_RECORD_BYTES)); + doReturn(entityId).when(transactionHandler).getEntity(any()); + pubSubRecordItemListener.onItem(new RecordItem(transaction, DEFAULT_RECORD)); // then FileData fileData = new FileData(100L, fileContents, entityId, TransactionType.FILEAPPEND @@ -289,9 +290,8 @@ void testNetworkAddressBookUpdate() throws Exception { Transaction transaction = buildTransaction(builder -> builder.setFileUpdate(fileUpdate)); // when - EntityId entityId = EntityId.of(ADDRESS_BOOK_FILE_ID); doReturn(EntityId.of(ADDRESS_BOOK_FILE_ID)).when(transactionHandler).getEntity(any()); - pubSubRecordItemListener.onItem(new RecordItem(transaction.toByteArray(), DEFAULT_RECORD_BYTES)); + pubSubRecordItemListener.onItem(new RecordItem(transaction, DEFAULT_RECORD)); // then FileData fileData = new FileData(100L, fileContents, EntityId diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTransactionHandlerTest.java index 11ec204bb7a..f92edea755c 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTransactionHandlerTest.java @@ -29,6 +29,10 @@ import com.google.protobuf.Int32Value; import com.google.protobuf.Message; import com.google.protobuf.StringValue; + +import com.hedera.mirror.importer.parser.domain.RecordItemBuilder; + +import com.hederahashgraph.api.proto.java.ContractID; import com.hederahashgraph.api.proto.java.Duration; import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.KeyList; @@ -70,6 +74,7 @@ import com.hedera.mirror.common.domain.entity.EntityType; import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.domain.EntityIdService; import com.hedera.mirror.importer.parser.record.entity.EntityListener; import com.hedera.mirror.importer.repository.EntityRepository; import com.hedera.mirror.importer.util.Utility; @@ -107,8 +112,15 @@ abstract class AbstractTransactionHandlerTest { protected final Logger log = LogManager.getLogger(getClass()); + protected final ContractID contractId = ContractID.newBuilder().setContractNum(DEFAULT_ENTITY_NUM).build(); + + protected final RecordItemBuilder recordItemBuilder = new RecordItemBuilder(); + protected TransactionHandler transactionHandler; + @Mock(lenient = true) + protected EntityIdService entityIdService; + @Mock protected EntityListener entityListener; @@ -137,7 +149,7 @@ protected TransactionReceipt.Builder getTransactionReceipt(ResponseCodeEnum resp protected TransactionRecord.Builder getDefaultTransactionRecord() { TransactionRecord.Builder builder = TransactionRecord.newBuilder(); - if (transactionHandler instanceof AbstractEntityCrudTransactionHandler) { + if (isCrudTransactionHandler(transactionHandler)) { Timestamp consensusTimestamp = transactionHandler.getType().getEntityOperation() == EntityOperation.CREATE ? CREATED_TIMESTAMP : MODIFIED_TIMESTAMP; builder.setConsensusTimestamp(consensusTimestamp); @@ -172,7 +184,7 @@ void testGetEntityId() { @TestFactory Stream testUpdateEntity() { - if (!(transactionHandler instanceof AbstractEntityCrudTransactionHandler)) { + if (!isCrudTransactionHandler(transactionHandler)) { // empty test if the handler does not update entity return Stream.empty(); } @@ -242,7 +254,7 @@ protected void testGetEntityIdHelper( } protected AbstractEntity getEntity() { - EntityId entityId = EntityId.of(0L, 0L, 1L, getExpectedEntityIdType()); + EntityId entityId = EntityId.of(0L, 0L, DEFAULT_ENTITY_NUM, getExpectedEntityIdType()); return entityId.toEntity(); } @@ -531,6 +543,11 @@ protected RecordItem getRecordItem(TransactionBody body, TransactionRecord recor return new RecordItem(transaction, record); } + private boolean isCrudTransactionHandler(TransactionHandler transactionHandler) { + return transactionHandler instanceof AbstractEntityCrudTransactionHandler || + transactionHandler instanceof ContractCreateTransactionHandler; + } + @Builder @Value static class UpdateEntityTestSpec { diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCallTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCallTransactionHandlerTest.java index e9855f513f6..bc15328e18e 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCallTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCallTransactionHandlerTest.java @@ -20,31 +20,54 @@ * ‍ */ -import com.hedera.mirror.common.domain.entity.EntityType; +import static com.hedera.mirror.common.domain.entity.EntityType.CONTRACT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + import com.hederahashgraph.api.proto.java.ContractCallTransactionBody; import com.hederahashgraph.api.proto.java.ContractID; import com.hederahashgraph.api.proto.java.TransactionBody; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.entity.EntityType; import com.hedera.mirror.importer.parser.record.entity.EntityProperties; class ContractCallTransactionHandlerTest extends AbstractTransactionHandlerTest { private final EntityProperties entityProperties = new EntityProperties(); + @BeforeEach + void beforeEach() { + when(entityIdService.lookup(ContractID.getDefaultInstance(), contractId)).thenReturn(EntityId.of(DEFAULT_ENTITY_NUM, CONTRACT)); + } + @Override protected TransactionHandler getTransactionHandler() { - return new ContractCallTransactionHandler(entityListener, entityProperties); + return new ContractCallTransactionHandler(entityIdService, entityListener, entityProperties); } @Override protected TransactionBody.Builder getDefaultTransactionBody() { return TransactionBody.newBuilder() - .setContractCall(ContractCallTransactionBody.newBuilder() - .setContractID(ContractID.newBuilder().setContractNum(DEFAULT_ENTITY_NUM).build())); + .setContractCall(ContractCallTransactionBody.newBuilder().setContractID(contractId)); } @Override protected EntityType getExpectedEntityIdType() { return EntityType.CONTRACT; } + + @Test + void testGetEntityIdReceipt() { + var recordItem = recordItemBuilder.contractCall().build(); + ContractID contractIdBody = recordItem.getTransactionBody().getContractCall().getContractID(); + ContractID contractIdReceipt = recordItem.getRecord().getReceipt().getContractID(); + EntityId expectedEntityId = EntityId.of(contractIdReceipt); + + when(entityIdService.lookup(contractIdReceipt, contractIdBody)).thenReturn(expectedEntityId); + EntityId entityId = transactionHandler.getEntity(recordItem); + assertThat(entityId).isEqualTo(expectedEntityId); + } } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCreateTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCreateTransactionHandlerTest.java index 9d9d65287f7..746781f912a 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCreateTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractCreateTransactionHandlerTest.java @@ -20,22 +20,41 @@ * ‍ */ +import static com.hedera.mirror.common.domain.entity.EntityType.CONTRACT; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.Descriptors; +import com.google.protobuf.Message; import com.hederahashgraph.api.proto.java.ContractCreateTransactionBody; -import com.hederahashgraph.api.proto.java.ContractID; +import com.hederahashgraph.api.proto.java.ContractFunctionResult; import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.TransactionBody; import com.hederahashgraph.api.proto.java.TransactionReceipt; +import com.hederahashgraph.api.proto.java.TransactionRecord; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import com.hedera.mirror.common.domain.contract.Contract; +import com.hedera.mirror.common.domain.entity.AbstractEntity; +import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.EntityType; +import com.hedera.mirror.importer.TestUtils; import com.hedera.mirror.importer.parser.record.entity.EntityProperties; class ContractCreateTransactionHandlerTest extends AbstractTransactionHandlerTest { private final EntityProperties entityProperties = new EntityProperties(); + @BeforeEach + void beforeEach() { + when(entityIdService.lookup(contractId)).thenReturn(EntityId.of(DEFAULT_ENTITY_NUM, CONTRACT)); + } + @Override protected TransactionHandler getTransactionHandler() { - return new ContractCreateTransactionHandler(entityListener, entityProperties); + return new ContractCreateTransactionHandler(entityIdService, entityListener, entityProperties); } @Override @@ -46,12 +65,44 @@ protected TransactionBody.Builder getDefaultTransactionBody() { @Override protected TransactionReceipt.Builder getTransactionReceipt(ResponseCodeEnum responseCodeEnum) { - return TransactionReceipt.newBuilder().setStatus(responseCodeEnum) - .setContractID(ContractID.newBuilder().setContractNum(DEFAULT_ENTITY_NUM).build()); + return TransactionReceipt.newBuilder().setStatus(responseCodeEnum).setContractID(contractId); + } + + @Override + protected List getUpdateEntityTestSpecsForCreateTransaction( + Descriptors.FieldDescriptor memoField) { + List testSpecs = super.getUpdateEntityTestSpecsForCreateTransaction(memoField); + + TransactionBody body = getTransactionBodyForUpdateEntityWithoutMemo(); + Message innerBody = getInnerBody(body); + body = getTransactionBody(body, innerBody); + byte[] evmAddress = TestUtils.generateRandomByteArray(20); + var contractCreateResult = ContractFunctionResult.newBuilder() + .setEvmAddress(BytesValue.of(ByteString.copyFrom(evmAddress))); + var recordBuilder = getDefaultTransactionRecord().setContractCreateResult(contractCreateResult); + + AbstractEntity expected = getExpectedUpdatedEntity(); + ((Contract) expected).setEvmAddress(evmAddress); + expected.setMemo(""); + testSpecs.add( + UpdateEntityTestSpec.builder() + .description("create contract entity with evm address in record") + .expected(expected) + .recordItem(getRecordItem(body, recordBuilder.build())) + .build() + ); + + return testSpecs; + } + + @Override + protected TransactionRecord.Builder getDefaultTransactionRecord() { + return super.getDefaultTransactionRecord() + .setContractCreateResult(ContractFunctionResult.newBuilder().addCreatedContractIDs(contractId)); } @Override protected EntityType getExpectedEntityIdType() { - return EntityType.CONTRACT; + return CONTRACT; } } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractDeleteTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractDeleteTransactionHandlerTest.java index 26e9245fa82..52c2d2faa50 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractDeleteTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractDeleteTransactionHandlerTest.java @@ -20,24 +20,39 @@ * ‍ */ +import static com.hedera.mirror.common.domain.entity.EntityType.CONTRACT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.ContractDeleteTransactionBody; import com.hederahashgraph.api.proto.java.ContractID; import com.hederahashgraph.api.proto.java.TransactionBody; import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import com.hedera.mirror.common.domain.contract.Contract; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.EntityType; +import org.junit.jupiter.api.Test; + class ContractDeleteTransactionHandlerTest extends AbstractDeleteOrUndeleteTransactionHandlerTest { - private static final long OBTAINER_ID = 99L; + private static final long OBTAINER_NUM = 99L; + + private final ContractID obtainerId = ContractID.newBuilder().setContractNum(OBTAINER_NUM).build(); + + @BeforeEach + void beforeEach() { + when(entityIdService.lookup(ContractID.getDefaultInstance(), contractId)).thenReturn(EntityId.of(DEFAULT_ENTITY_NUM, CONTRACT)); + when(entityIdService.lookup(obtainerId)).thenReturn(EntityId.of(OBTAINER_NUM, CONTRACT)); + } @Override protected TransactionHandler getTransactionHandler() { - return new ContractDeleteTransactionHandler(entityListener); + return new ContractDeleteTransactionHandler(entityIdService, entityListener); } @Override @@ -45,7 +60,7 @@ protected TransactionBody.Builder getDefaultTransactionBody() { return TransactionBody.newBuilder() .setContractDeleteInstance(ContractDeleteTransactionBody.newBuilder() .setContractID(ContractID.newBuilder().setContractNum(DEFAULT_ENTITY_NUM).build()) - .setTransferAccountID(AccountID.newBuilder().setAccountNum(OBTAINER_ID).build())); + .setTransferAccountID(AccountID.newBuilder().setAccountNum(OBTAINER_NUM).build())); } @Override @@ -58,7 +73,7 @@ protected List getUpdateEntityTestSpecs() { List specs = new ArrayList<>(); Contract expected = (Contract) getExpectedEntityWithTimestamp(); expected.setDeleted(true); - expected.setObtainerId(EntityId.of(OBTAINER_ID, EntityType.ACCOUNT)); + expected.setObtainerId(EntityId.of(OBTAINER_NUM, EntityType.ACCOUNT)); specs.add( UpdateEntityTestSpec.builder() @@ -72,7 +87,7 @@ protected List getUpdateEntityTestSpecs() { TransactionBody.Builder transactionBody = TransactionBody.newBuilder() .setContractDeleteInstance(ContractDeleteTransactionBody.newBuilder() .setContractID(ContractID.newBuilder().setContractNum(DEFAULT_ENTITY_NUM).build()) - .setTransferContractID(ContractID.newBuilder().setContractNum(OBTAINER_ID).build())); + .setTransferContractID(ContractID.newBuilder().setContractNum(OBTAINER_NUM).build())); specs.add( UpdateEntityTestSpec.builder() @@ -84,4 +99,16 @@ protected List getUpdateEntityTestSpecs() { ); return specs; } + + @Test + void testGetEntityIdReceipt() { + var recordItem = recordItemBuilder.contractDelete().build(); + ContractID contractIdBody = recordItem.getTransactionBody().getContractDeleteInstance().getContractID(); + ContractID contractIdReceipt = recordItem.getRecord().getReceipt().getContractID(); + EntityId expectedEntityId = EntityId.of(contractIdReceipt); + + when(entityIdService.lookup(contractIdReceipt, contractIdBody)).thenReturn(expectedEntityId); + EntityId entityId = transactionHandler.getEntity(recordItem); + assertThat(entityId).isEqualTo(expectedEntityId); + } } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractUpdateTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractUpdateTransactionHandlerTest.java index 572466f5966..14fbdd6b971 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractUpdateTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/ContractUpdateTransactionHandlerTest.java @@ -20,16 +20,30 @@ * ‍ */ -import com.hedera.mirror.common.domain.entity.EntityType; +import static com.hedera.mirror.common.domain.entity.EntityType.CONTRACT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + import com.hederahashgraph.api.proto.java.ContractID; import com.hederahashgraph.api.proto.java.ContractUpdateTransactionBody; import com.hederahashgraph.api.proto.java.TransactionBody; +import org.junit.jupiter.api.BeforeEach; + +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.entity.EntityType; + +import org.junit.jupiter.api.Test; class ContractUpdateTransactionHandlerTest extends AbstractTransactionHandlerTest { + @BeforeEach + void beforeEach() { + when(entityIdService.lookup(ContractID.getDefaultInstance(), contractId)).thenReturn(EntityId.of(DEFAULT_ENTITY_NUM, CONTRACT)); + } + @Override protected TransactionHandler getTransactionHandler() { - return new ContractUpdateTransactionHandler(entityListener); + return new ContractUpdateTransactionHandler(entityIdService, entityListener); } @Override @@ -43,4 +57,16 @@ protected TransactionBody.Builder getDefaultTransactionBody() { protected EntityType getExpectedEntityIdType() { return EntityType.CONTRACT; } + + @Test + void testGetEntityIdReceipt() { + var recordItem = recordItemBuilder.contractUpdate().build(); + ContractID contractIdBody = recordItem.getTransactionBody().getContractUpdateInstance().getContractID(); + ContractID contractIdReceipt = recordItem.getRecord().getReceipt().getContractID(); + EntityId expectedEntityId = EntityId.of(contractIdReceipt); + + when(entityIdService.lookup(contractIdReceipt, contractIdBody)).thenReturn(expectedEntityId); + EntityId entityId = transactionHandler.getEntity(recordItem); + assertThat(entityId).isEqualTo(expectedEntityId); + } } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemDeleteTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemDeleteTransactionHandlerTest.java index 2bce454a662..7e57bad33b9 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemDeleteTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemDeleteTransactionHandlerTest.java @@ -20,10 +20,14 @@ * ‍ */ +import static com.hedera.mirror.common.domain.entity.EntityType.CONTRACT; +import static org.mockito.Mockito.when; + import com.hederahashgraph.api.proto.java.ContractID; import com.hederahashgraph.api.proto.java.FileID; import com.hederahashgraph.api.proto.java.SystemDeleteTransactionBody; import com.hederahashgraph.api.proto.java.TransactionBody; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.hedera.mirror.common.domain.entity.EntityId; @@ -31,9 +35,14 @@ class SystemDeleteTransactionHandlerTest extends AbstractDeleteOrUndeleteTransactionHandlerTest { + @BeforeEach + void beforeEach() { + when(entityIdService.lookup(contractId)).thenReturn(EntityId.of(DEFAULT_ENTITY_NUM, CONTRACT)); + } + @Override protected TransactionHandler getTransactionHandler() { - return new SystemDeleteTransactionHandler(entityListener); + return new SystemDeleteTransactionHandler(entityIdService, entityListener); } @Override diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemUndeleteTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemUndeleteTransactionHandlerTest.java index ef34bf8c4d6..85bb00e50e9 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemUndeleteTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/SystemUndeleteTransactionHandlerTest.java @@ -20,24 +20,33 @@ * ‍ */ -import com.hedera.mirror.common.domain.entity.EntityType; +import static com.hedera.mirror.common.domain.entity.EntityType.CONTRACT; +import static org.mockito.Mockito.when; + import com.hederahashgraph.api.proto.java.ContractID; import com.hederahashgraph.api.proto.java.FileID; import com.hederahashgraph.api.proto.java.SystemUndeleteTransactionBody; import com.hederahashgraph.api.proto.java.TransactionBody; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.entity.EntityType; class SystemUndeleteTransactionHandlerTest extends AbstractDeleteOrUndeleteTransactionHandlerTest { - public SystemUndeleteTransactionHandlerTest() { + SystemUndeleteTransactionHandlerTest() { super(false); } + @BeforeEach + void beforeEach() { + when(entityIdService.lookup(contractId)).thenReturn(EntityId.of(DEFAULT_ENTITY_NUM, CONTRACT)); + } + @Override protected TransactionHandler getTransactionHandler() { - return new SystemUndeleteTransactionHandler(entityListener); + return new SystemUndeleteTransactionHandler(entityIdService, entityListener); } @Override diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/ContractRepositoryTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/ContractRepositoryTest.java index 3ae24962252..a449465a4bf 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/ContractRepositoryTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/ContractRepositoryTest.java @@ -36,6 +36,24 @@ class ContractRepositoryTest extends AbstractRepositoryTest { @Resource private ContractRepository contractRepository; + @Test + void findByEvmAddress() { + Contract contract = domainBuilder.contract().persist(); + assertThat(contractRepository.findByEvmAddress(contract.getEvmAddress())).get().isEqualTo(contract.getId()); + } + + @Test + void findByEvmAddressDeleted() { + Contract contract = domainBuilder.contract().customize((b) -> b.deleted(true)).persist(); + assertThat(contractRepository.findByEvmAddress(contract.getEvmAddress())).isEmpty(); + } + + @Test + void findByEvmAddressNotFound() { + Contract contract = domainBuilder.contract().get(); + assertThat(contractRepository.findByEvmAddress(contract.getEvmAddress())).isEmpty(); + } + @Test void save() { Contract contract = domainBuilder.contract().persist(); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/upsert/GenericUpsertQueryGeneratorTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/upsert/GenericUpsertQueryGeneratorTest.java index a1b365fa6f1..db21dbb328d 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/upsert/GenericUpsertQueryGeneratorTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/upsert/GenericUpsertQueryGeneratorTest.java @@ -27,8 +27,8 @@ import javax.annotation.Resource; import org.junit.jupiter.api.Test; -import com.hedera.mirror.importer.IntegrationTest; import com.hedera.mirror.common.domain.contract.Contract; +import com.hedera.mirror.importer.IntegrationTest; class GenericUpsertQueryGeneratorTest extends IntegrationTest { @@ -69,13 +69,14 @@ void getInsertQuery() { assertThat(generator).isInstanceOf(GenericUpsertQueryGenerator.class); assertThat(format(generator.getInsertQuery())).isEqualTo(format("with existing as (" + " insert into contract_history (" + - " auto_renew_period, created_timestamp, deleted, expiration_timestamp, file_id, id, key, memo, " + - " num, obtainer_id, proxy_account_id, public_key, realm, shard, timestamp_range, type" + + " auto_renew_period, created_timestamp, deleted, evm_address, expiration_timestamp, file_id, id," + + " key, memo, num, obtainer_id, proxy_account_id, public_key, realm, shard, timestamp_range, type" + " )" + " select" + " e.auto_renew_period," + " e.created_timestamp," + " e.deleted," + + " e.evm_address," + " e.expiration_timestamp," + " e.file_id," + " e.id," + @@ -96,13 +97,14 @@ void getInsertQuery() { ")," + "history as (" + " insert into contract_history (" + - " auto_renew_period, created_timestamp, deleted, expiration_timestamp, file_id, id, key, memo, " + - " num, obtainer_id, proxy_account_id, public_key, realm, shard, timestamp_range, type" + + " auto_renew_period, created_timestamp, deleted, evm_address, expiration_timestamp, file_id, id," + + " key, memo, num, obtainer_id, proxy_account_id, public_key, realm, shard, timestamp_range, type" + " )" + " select distinct" + " coalesce(t.auto_renew_period, e.auto_renew_period, null)," + " coalesce(t.created_timestamp, e.created_timestamp, null)," + " coalesce(t.deleted, e.deleted, null)," + + " coalesce(t.evm_address, e.evm_address, null)," + " coalesce(t.expiration_timestamp, e.expiration_timestamp, null)," + " coalesce(t.file_id, e.file_id, null)," + " coalesce(t.id, e.id, null)," + @@ -121,13 +123,14 @@ void getInsertQuery() { " where upper(t.timestamp_range) is not null returning *" + ")" + "insert into contract (" + - " auto_renew_period, created_timestamp, deleted, expiration_timestamp, file_id, id, key, memo, " + - " num, obtainer_id, proxy_account_id, public_key, realm, shard, timestamp_range, type" + + " auto_renew_period, created_timestamp, deleted, evm_address, expiration_timestamp, file_id, id," + + " key, memo, num, obtainer_id, proxy_account_id, public_key, realm, shard, timestamp_range, type" + ")" + "select" + " coalesce(t.auto_renew_period, e.auto_renew_period, null)," + " coalesce(t.created_timestamp, e.created_timestamp, null)," + " coalesce(t.deleted, e.deleted, null)," + + " coalesce(t.evm_address, e.evm_address, null)," + " coalesce(t.expiration_timestamp, e.expiration_timestamp, null)," + " coalesce(t.file_id, e.file_id, null)," + " coalesce(t.id, e.id, null)," + diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/upsert/UpsertQueryGeneratorFactoryTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/upsert/UpsertQueryGeneratorFactoryTest.java index bd5cee7e22d..5b4b6fc14a5 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/upsert/UpsertQueryGeneratorFactoryTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/upsert/UpsertQueryGeneratorFactoryTest.java @@ -59,8 +59,8 @@ void getGenericGenerator() { @Test void contract() { - String allColumns = "auto_renew_period,created_timestamp,deleted,expiration_timestamp,file_id,id,key," + - "memo,num,obtainer_id,proxy_account_id,public_key,realm,shard,timestamp_range,type"; + String allColumns = "auto_renew_period,created_timestamp,deleted,evm_address,expiration_timestamp,file_id," + + "id,key,memo,num,obtainer_id,proxy_account_id,public_key,realm,shard,timestamp_range,type"; String updatableColumns = "auto_renew_period,deleted,expiration_timestamp,key,memo,obtainer_id," + "proxy_account_id,public_key,timestamp_range"; @@ -75,7 +75,7 @@ void contract() { .returns(allColumns, e -> e.columns("{0}")) .returns(updatableColumns, e -> e.columns(UpsertColumn::isUpdatable, "{0}")) .extracting(UpsertEntity::getColumns, InstanceOfAssertFactories.ITERABLE) - .hasSize(16); + .hasSize(17); } @Test diff --git a/hedera-mirror-rest/__tests__/controllers/contractController.test.js b/hedera-mirror-rest/__tests__/controllers/contractController.test.js index 316548196e9..2060e2e5d4e 100644 --- a/hedera-mirror-rest/__tests__/controllers/contractController.test.js +++ b/hedera-mirror-rest/__tests__/controllers/contractController.test.js @@ -37,6 +37,7 @@ const contractFields = [ Contract.AUTO_RENEW_PERIOD, Contract.CREATED_TIMESTAMP, Contract.DELETED, + Contract.EVM_ADDRESS, Contract.EXPIRATION_TIMESTAMP, Contract.FILE_ID, Contract.ID, @@ -222,6 +223,7 @@ describe('formatContractRow', () => { auto_renew_period: '1000', created_timestamp: '999123456789', deleted: false, + evm_address: null, expiration_timestamp: '99999999000000000', file_id: '2800', id: '3001', @@ -240,12 +242,12 @@ describe('formatContractRow', () => { contract_id: '0.0.3001', created_timestamp: '999.123456789', deleted: false, + evm_address: '0x0000000000000000000000000000000000000bb9', expiration_timestamp: '99999999.000000000', file_id: '0.0.2800', memo: 'sample contract', obtainer_id: '0.0.2005', proxy_account_id: '0.0.2002', - solidity_address: '0x0000000000000000000000000000000000000bb9', timestamp: { from: '1000.123456789', to: '2000.123456789', diff --git a/hedera-mirror-rest/__tests__/entityId.test.js b/hedera-mirror-rest/__tests__/entityId.test.js index 2586f92f0ed..2c310b79eb8 100644 --- a/hedera-mirror-rest/__tests__/entityId.test.js +++ b/hedera-mirror-rest/__tests__/entityId.test.js @@ -306,17 +306,17 @@ describe('EntityId parse from encoded entityId', () => { } }); -describe('EntityId toSolidityAddress', () => { +describe('EntityId toEvmAddress', () => { test('0.0.0', () => { - expect(EntityId.of(0n, 0n, 0n).toSolidityAddress()).toEqual('0x0000000000000000000000000000000000000000'); + expect(EntityId.of(0n, 0n, 0n).toEvmAddress()).toEqual('0x0000000000000000000000000000000000000000'); }); test('0.0.7', () => { - expect(EntityId.of(1n, 2n, 7n).toSolidityAddress()).toEqual('0x0000000100000000000000020000000000000007'); + expect(EntityId.of(1n, 2n, 7n).toEvmAddress()).toEqual('0x0000000100000000000000020000000000000007'); }); test('32767.65535.4294967295', () => { - expect(EntityId.of(32767n, 65535n, 4294967295n).toSolidityAddress()).toEqual( + expect(EntityId.of(32767n, 65535n, 4294967295n).toEvmAddress()).toEqual( '0x00007fff000000000000ffff00000000ffffffff' ); }); diff --git a/hedera-mirror-rest/__tests__/integrationDomainOps.js b/hedera-mirror-rest/__tests__/integrationDomainOps.js index 2c1bb093de0..d78d725872c 100644 --- a/hedera-mirror-rest/__tests__/integrationDomainOps.js +++ b/hedera-mirror-rest/__tests__/integrationDomainOps.js @@ -554,6 +554,7 @@ const addContract = async (contract) => { 'auto_renew_period', 'created_timestamp', 'deleted', + 'evm_address', 'expiration_timestamp', 'file_id', 'id', @@ -571,6 +572,7 @@ const addContract = async (contract) => { contract = { auto_renew_period: null, deleted: false, + evm_address: null, expiration_timestamp: null, key: null, memo: 'contract memo', @@ -581,6 +583,7 @@ const addContract = async (contract) => { timestamp_range: '[0,)', ...contract, }; + contract.evm_address = contract.evm_address != null ? Buffer.from(contract.evm_address) : null; contract.id = EntityId.of(BigInt(contract.shard), BigInt(contract.realm), BigInt(contract.num)).getEncodedId(); contract.key = contract.key != null ? Buffer.from(contract.key) : null; diff --git a/hedera-mirror-rest/__tests__/specs/contracts-01-specific-id.spec.json b/hedera-mirror-rest/__tests__/specs/contracts-01-specific-id.spec.json index d0e4ef1613d..dfe332347d8 100644 --- a/hedera-mirror-rest/__tests__/specs/contracts-01-specific-id.spec.json +++ b/hedera-mirror-rest/__tests__/specs/contracts-01-specific-id.spec.json @@ -83,12 +83,12 @@ "contract_id": "0.0.8001", "created_timestamp": "987654.000123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f41", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f41", "timestamp": { "from": "997654.000123457", "to": null diff --git a/hedera-mirror-rest/__tests__/specs/contracts-02-specific-id-timestamp.spec.json b/hedera-mirror-rest/__tests__/specs/contracts-02-specific-id-timestamp.spec.json index 5b0c7bea8f6..1d2683f9245 100644 --- a/hedera-mirror-rest/__tests__/specs/contracts-02-specific-id-timestamp.spec.json +++ b/hedera-mirror-rest/__tests__/specs/contracts-02-specific-id-timestamp.spec.json @@ -94,12 +94,12 @@ "contract_id": "0.0.8001", "created_timestamp": "987654.999123200", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f41", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f41", "timestamp": { "from": "987654.999123300", "to": null diff --git a/hedera-mirror-rest/__tests__/specs/contracts-03-specific-id-timestamp-historical-missing-file.spec.json b/hedera-mirror-rest/__tests__/specs/contracts-03-specific-id-timestamp-historical-missing-file.spec.json index c76ad67fb2a..53caba50ad4 100644 --- a/hedera-mirror-rest/__tests__/specs/contracts-03-specific-id-timestamp-historical-missing-file.spec.json +++ b/hedera-mirror-rest/__tests__/specs/contracts-03-specific-id-timestamp-historical-missing-file.spec.json @@ -72,12 +72,12 @@ "contract_id": "0.0.8001", "created_timestamp": "987654.000123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f41", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f41", "timestamp": { "from": "987654.000123456", "to": "997654.000123457" diff --git a/hedera-mirror-rest/__tests__/specs/contracts-09-no-args.spec.json b/hedera-mirror-rest/__tests__/specs/contracts-09-no-args.spec.json index 0f021ad1385..68758cdf9eb 100644 --- a/hedera-mirror-rest/__tests__/specs/contracts-09-no-args.spec.json +++ b/hedera-mirror-rest/__tests__/specs/contracts-09-no-args.spec.json @@ -50,12 +50,12 @@ "contract_id": "0.0.8003", "created_timestamp": "987654.222123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f43", "expiration_timestamp": "1236987654.000000123", "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": "0.0.7001", "proxy_account_id": "0.0.7005", - "solidity_address": "0x0000000000000000000000000000000000001f43", "timestamp": { "from": "987654.222123456", "to": null @@ -70,12 +70,12 @@ "contract_id": "0.0.8002", "created_timestamp": "987654.111123456", "deleted": true, + "evm_address": "0x0000000000000000000000000000000000001f42", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f42", "timestamp": { "from": "987654.111123456", "to": null @@ -90,12 +90,12 @@ "contract_id": "0.0.8001", "created_timestamp": "987654.000123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f41", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f41", "timestamp": { "from": "997654.000123457", "to": null diff --git a/hedera-mirror-rest/__tests__/specs/contracts-10-order-asc.spec.json b/hedera-mirror-rest/__tests__/specs/contracts-10-order-asc.spec.json index 70abd49af10..7b0e552aa38 100644 --- a/hedera-mirror-rest/__tests__/specs/contracts-10-order-asc.spec.json +++ b/hedera-mirror-rest/__tests__/specs/contracts-10-order-asc.spec.json @@ -49,12 +49,12 @@ "contract_id": "0.0.8001", "created_timestamp": "987654.000123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f41", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f41", "timestamp": { "from": "997654.000123457", "to": null @@ -69,12 +69,12 @@ "contract_id": "0.0.8002", "created_timestamp": "987654.111123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f42", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f42", "timestamp": { "from": "987654.111123456", "to": null @@ -89,12 +89,12 @@ "contract_id": "0.0.8003", "created_timestamp": "987654.222123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f43", "expiration_timestamp": "1236987654.000000123", "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": "0.0.7001", "proxy_account_id": "0.0.7005", - "solidity_address": "0x0000000000000000000000000000000000001f43", "timestamp": { "from": "987654.222123456", "to": null diff --git a/hedera-mirror-rest/__tests__/specs/contracts-11-limit.spec.json b/hedera-mirror-rest/__tests__/specs/contracts-11-limit.spec.json index 59ccfe7e0cb..23667d3a7b9 100644 --- a/hedera-mirror-rest/__tests__/specs/contracts-11-limit.spec.json +++ b/hedera-mirror-rest/__tests__/specs/contracts-11-limit.spec.json @@ -18,6 +18,7 @@ }, { "created_timestamp": "987654111123456", + "evm_address": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], "file_id": "5001", "key": [2, 2, 2], "num": "8002", @@ -49,12 +50,12 @@ "contract_id": "0.0.8003", "created_timestamp": "987654.222123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f43", "expiration_timestamp": "1236987654.000000123", "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": "0.0.7001", "proxy_account_id": "0.0.7005", - "solidity_address": "0x0000000000000000000000000000000000001f43", "timestamp": { "from": "987654.222123456", "to": null @@ -69,12 +70,12 @@ "contract_id": "0.0.8002", "created_timestamp": "987654.111123456", "deleted": false, + "evm_address": "0x0102030405060708090a0b0c0d0e0f1011121314", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f42", "timestamp": { "from": "987654.111123456", "to": null diff --git a/hedera-mirror-rest/__tests__/specs/contracts-12-contract-id.spec.json b/hedera-mirror-rest/__tests__/specs/contracts-12-contract-id.spec.json index 62c303374b2..0ba595d13d5 100644 --- a/hedera-mirror-rest/__tests__/specs/contracts-12-contract-id.spec.json +++ b/hedera-mirror-rest/__tests__/specs/contracts-12-contract-id.spec.json @@ -53,12 +53,12 @@ "contract_id": "0.0.8002", "created_timestamp": "987654.111123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f42", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f42", "timestamp": { "from": "987654.111123456", "to": null diff --git a/hedera-mirror-rest/__tests__/specs/contracts-13-multiple-contract-ids.spec.json b/hedera-mirror-rest/__tests__/specs/contracts-13-multiple-contract-ids.spec.json index 0633fc6283d..f8604f4fd3e 100644 --- a/hedera-mirror-rest/__tests__/specs/contracts-13-multiple-contract-ids.spec.json +++ b/hedera-mirror-rest/__tests__/specs/contracts-13-multiple-contract-ids.spec.json @@ -52,12 +52,12 @@ "contract_id": "0.0.8002", "created_timestamp": "987654.111123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f42", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f42", "timestamp": { "from": "987654.111123456", "to": null @@ -72,12 +72,12 @@ "contract_id": "0.0.8001", "created_timestamp": "987654.000123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f41", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f41", "timestamp": { "from": "997654.000123457", "to": null diff --git a/hedera-mirror-rest/__tests__/specs/contracts-14-all-params.spec.json b/hedera-mirror-rest/__tests__/specs/contracts-14-all-params.spec.json index 079f8729aa2..4ed75544e25 100644 --- a/hedera-mirror-rest/__tests__/specs/contracts-14-all-params.spec.json +++ b/hedera-mirror-rest/__tests__/specs/contracts-14-all-params.spec.json @@ -49,12 +49,12 @@ "contract_id": "0.0.8002", "created_timestamp": "987654.111123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f42", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f42", "timestamp": { "from": "987654.111123456", "to": null @@ -69,12 +69,12 @@ "contract_id": "0.0.8001", "created_timestamp": "987654.000123456", "deleted": false, + "evm_address": "0x0000000000000000000000000000000000001f41", "expiration_timestamp": null, "file_id": "0.0.5001", "memo": "contract memo", "obtainer_id": null, "proxy_account_id": null, - "solidity_address": "0x0000000000000000000000000000000000001f41", "timestamp": { "from": "997654.000123457", "to": null diff --git a/hedera-mirror-rest/__tests__/viewmodel/contractViewModel.test.js b/hedera-mirror-rest/__tests__/viewmodel/contractViewModel.test.js index dd41eae1280..1459b00e950 100644 --- a/hedera-mirror-rest/__tests__/viewmodel/contractViewModel.test.js +++ b/hedera-mirror-rest/__tests__/viewmodel/contractViewModel.test.js @@ -28,6 +28,10 @@ describe('ContractViewModel', () => { autoRenewPeriod: '1000', createdTimestamp: '999123456789', deleted: false, + evmAddress: Buffer.from([ + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, + 0x23, + ]), expirationTimestamp: '99999999000000000', fileId: '2800', id: '3001', @@ -46,12 +50,12 @@ describe('ContractViewModel', () => { contract_id: '0.0.3001', created_timestamp: '999.123456789', deleted: false, + evm_address: '0x101112131415161718191a1b1c1d1e1f20212223', expiration_timestamp: '99999999.000000000', file_id: '0.0.2800', memo: 'sample contract', obtainer_id: '0.0.2005', proxy_account_id: '0.0.2002', - solidity_address: '0x0000000000000000000000000000000000000bb9', timestamp: { from: '1000.123456789', to: '2000.123456789', @@ -86,6 +90,18 @@ describe('ContractViewModel', () => { }); }); + test('null evm address', () => { + expect( + new ContractViewModel({ + ...defaultContract, + evmAddress: null, + }) + ).toEqual({ + ...defaultExpected, + evm_address: '0x0000000000000000000000000000000000000bb9', + }); + }); + test('open-ended timestamp range', () => { expect( new ContractViewModel({ diff --git a/hedera-mirror-rest/api/v1/openapi.yml b/hedera-mirror-rest/api/v1/openapi.yml index c4090f77c43..34b58dd3257 100644 --- a/hedera-mirror-rest/api/v1/openapi.yml +++ b/hedera-mirror-rest/api/v1/openapi.yml @@ -1028,6 +1028,8 @@ components: deleted: type: boolean example: false + evm_address: + $ref: '#/components/schemas/EvmAddress' expiration_timestamp: $ref: '#/components/schemas/TimestampNullable' file_id: @@ -1039,8 +1041,6 @@ components: $ref: '#/components/schemas/EntityId' proxy_account_id: $ref: '#/components/schemas/EntityId' - solidity_address: - $ref: '#/components/schemas/SolidityAddress' timestamp: $ref: '#/components/schemas/TimestampRange' Contracts: @@ -1053,8 +1053,9 @@ components: - type: object properties: root_contract_id: - description: The executed contract that created this contract log - $ref: '#/components/schemas/EntityId' + allOf: + - $ref: '#/components/schemas/EntityId' + - description: The executed contract that created this contract log timestamp: $ref: '#/components/schemas/Timestamp' ContractLogTopics: @@ -1096,7 +1097,7 @@ components: nullable: true type: string from: - $ref: '#/components/schemas/SolidityAddress' + $ref: '#/components/schemas/EvmAddress' function_parameters: description: The hex encoded parameters passed to the function example: "0xbb9f02dc6f0e3289f57a1f33b71c73aa8548ab8b" @@ -1124,7 +1125,7 @@ components: timestamp: $ref: '#/components/schemas/Timestamp' to: - $ref: '#/components/schemas/SolidityAddressNullable' + $ref: '#/components/schemas/EvmAddressNullable' ContractResultDetails: allOf: - $ref: '#/components/schemas/ContractResult' @@ -1153,7 +1154,7 @@ components: type: object properties: address: - description: The hex encoded Solidity address of the contract + description: The hex encoded EVM address of the contract example: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' pattern: ^0x[0-9A-Fa-f]{40}$ type: string @@ -1182,7 +1183,7 @@ components: type: object properties: address: - $ref: '#/components/schemas/SolidityAddress' + $ref: '#/components/schemas/EvmAddress' contract_id: $ref: '#/components/schemas/EntityId' slot: @@ -1252,6 +1253,23 @@ components: properties: message: type: string + EvmAddress: + type: string + description: A network entity encoded as an EVM address in hex + format: binary + minLength: 42 + maxLength: 42 + pattern: '^(0x)?[A-Fa-f0-9]{40}$' + example: "0x0000000000000000000000000000000000001f41" + EvmAddressNullable: + type: string + description: A network entity encoded as an EVM address in hex + format: binary + minLength: 42 + maxLength: 42 + nullable: true + pattern: '^0x[A-Fa-f0-9]{40}$' + example: "0x0000000000000000000000000000000000001f41" FixedFee: type: object properties: @@ -1448,23 +1466,6 @@ components: type: string enum: [ CONTRACT, ED25519, RSA_3072, ECDSA_384, ECDSA_SECP256K1, UNKNOWN ] example: "ED25519" - SolidityAddress: - type: string - description: A network entity encoded as a solidity address in hex - format: binary - minLength: 42 - maxLength: 42 - pattern: '^(0x)?[A-Fa-f0-9]{40}$' - example: "0x0000000000000000000000000000000000001f41" - SolidityAddressNullable: - type: string - description: A network entity encoded as a solidity address in hex - format: binary - minLength: 42 - maxLength: 42 - nullable: true - pattern: '^0x[A-Fa-f0-9]{40}$' - example: "0x0000000000000000000000000000000000001f41" StateProofFiles: type: object properties: @@ -2170,11 +2171,11 @@ components: fromQueryParam: name: from in: query - description: Account ID or solidity address executing the contract + description: Account ID or EVM address executing the contract schema: oneOf: - $ref: '#/components/schemas/EntityId' - - $ref: '#/components/schemas/SolidityAddress' + - $ref: '#/components/schemas/EvmAddress' logIndexQueryParam: name: index in: query diff --git a/hedera-mirror-rest/controllers/contractController.js b/hedera-mirror-rest/controllers/contractController.js index 75e2ab3540e..6da7e9f3a6b 100644 --- a/hedera-mirror-rest/controllers/contractController.js +++ b/hedera-mirror-rest/controllers/contractController.js @@ -51,6 +51,7 @@ const contractSelectFields = [ Contract.AUTO_RENEW_PERIOD, Contract.CREATED_TIMESTAMP, Contract.DELETED, + Contract.EVM_ADDRESS, Contract.EXPIRATION_TIMESTAMP, Contract.FILE_ID, Contract.ID, diff --git a/hedera-mirror-rest/entityId.js b/hedera-mirror-rest/entityId.js index 341b47b3f8a..9d763fc18fe 100644 --- a/hedera-mirror-rest/entityId.js +++ b/hedera-mirror-rest/entityId.js @@ -45,9 +45,9 @@ const maxShard = 2n ** shardBits - 1n; const maxEncodedId = 2n ** 63n - 1n; -const solidityAddressRegex = /^(0x)?[A-Fa-f0-9]{40}$/; const entityIdRegex = /^(\d{1,5}\.){1,2}\d{1,10}$/; const encodedEntityIdRegex = /^\d{1,19}$/; +const evmAddressRegex = /^(0x)?[A-Fa-f0-9]{40}$/; class EntityId { constructor(shard, realm, num) { @@ -68,9 +68,9 @@ class EntityId { } /** - * Converts the entity id to the 20-byte solidity address in hex with '0x' prefix + * Converts the entity id to the 20-byte EVM address in hex with '0x' prefix */ - toSolidityAddress() { + toEvmAddress() { // shard, realm, and num take 4, 8, and 8 bytes respectively from the left return this.num === null ? null @@ -91,9 +91,9 @@ const toHex = (num) => { return num.toString(16); }; -const isValidSolidityAddress = (address) => { +const isValidEvmAddress = (address) => { // Accepted forms: 0x... - return typeof address === 'string' && solidityAddressRegex.test(address); + return typeof address === 'string' && evmAddressRegex.test(address); }; const isValidEntityId = (entityId) => { @@ -155,11 +155,11 @@ const parseFromEncodedId = (id, error) => { }; /** - * Parses shard, realm, num from solidity address string. + * Parses shard, realm, num from EVM address string. * @param {string} address * @return {bigint[3]} */ -const parseFromSolidityAddress = (address) => { +const parseFromEvmAddress = (address) => { // extract shard from index 0->8, realm from 8->23, num from 24->40 and parse from hex to decimal const hexDigits = address.replace('0x', ''); const parts = [ @@ -195,7 +195,7 @@ const entityIdCacheOptions = { const parseMemoized = mem( /** - * Parses entity ID string, can be shard.realm.num, realm.num, the encoded entity ID or a solidity contract address. + * Parses entity ID string, can be shard.realm.num, realm.num, the encoded entity ID or an evm contract address. * @param {string} id * @param {Function} error * @return {EntityId} @@ -204,8 +204,8 @@ const parseMemoized = mem( let shard, realm, num; if (isValidEntityId(id)) { [shard, realm, num] = id.includes('.') ? parseFromString(id) : parseFromEncodedId(id, error); - } else if (isValidSolidityAddress(id)) { - [shard, realm, num] = parseFromSolidityAddress(id); + } else if (isValidEvmAddress(id)) { + [shard, realm, num] = parseFromEvmAddress(id); } else { throw error(); } @@ -251,7 +251,7 @@ const parse = (id, ...rest) => { module.exports = { isValidEntityId, - isValidSolidityAddress, + isValidEvmAddress, of, parse, }; diff --git a/hedera-mirror-rest/model/contract.js b/hedera-mirror-rest/model/contract.js index dbdf8fe8c45..216773d45f0 100644 --- a/hedera-mirror-rest/model/contract.js +++ b/hedera-mirror-rest/model/contract.js @@ -40,6 +40,7 @@ class Contract { static AUTO_RENEW_PERIOD = 'auto_renew_period'; static CREATED_TIMESTAMP = 'created_timestamp'; static DELETED = 'deleted'; + static EVM_ADDRESS = 'evm_address'; static EXPIRATION_TIMESTAMP = 'expiration_timestamp'; static FILE_ID = 'file_id'; static ID = 'id'; diff --git a/hedera-mirror-rest/utils.js b/hedera-mirror-rest/utils.js index 94356875e6d..671499496f5 100644 --- a/hedera-mirror-rest/utils.js +++ b/hedera-mirror-rest/utils.js @@ -196,7 +196,7 @@ const filterValidityChecks = (param, op, val) => { ret = isValidPublicKeyQuery(val); break; case constants.filterKeys.FROM: - ret = EntityId.isValidEntityId(val) || EntityId.isValidSolidityAddress(val); + ret = EntityId.isValidEntityId(val) || EntityId.isValidEvmAddress(val); break; case constants.filterKeys.INDEX: ret = isNumeric(val) && val >= 0; diff --git a/hedera-mirror-rest/viewmodel/contractResultLogViewModel.js b/hedera-mirror-rest/viewmodel/contractResultLogViewModel.js index e138676e986..df3393e8716 100644 --- a/hedera-mirror-rest/viewmodel/contractResultLogViewModel.js +++ b/hedera-mirror-rest/viewmodel/contractResultLogViewModel.js @@ -36,7 +36,7 @@ class ContractResultLogViewModel { constructor(contractLog) { const contractId = EntityId.parse(contractLog.contractId, constants.filterKeys.CONTRACTID); Object.assign(this, { - address: contractId.toSolidityAddress(), + address: contractId.toEvmAddress(), bloom: utils.toHexString(contractLog.bloom, true), contract_id: contractId.toString(), data: utils.toHexString(contractLog.data, true), diff --git a/hedera-mirror-rest/viewmodel/contractResultStateChangeViewModel.js b/hedera-mirror-rest/viewmodel/contractResultStateChangeViewModel.js index e8b1c825bfa..9ff75f94e28 100644 --- a/hedera-mirror-rest/viewmodel/contractResultStateChangeViewModel.js +++ b/hedera-mirror-rest/viewmodel/contractResultStateChangeViewModel.js @@ -35,7 +35,7 @@ class ContractResultStateChangeViewModel { */ constructor(contractStateChange) { const contractId = EntityId.parse(contractStateChange.contractId, constants.filterKeys.CONTRACTID); - this.address = contractId.toSolidityAddress(); + this.address = contractId.toEvmAddress(); this.contract_id = contractId.toString(); this.slot = utils.toHexString(contractStateChange.slot, true, 64); this.value_read = utils.toHexString(contractStateChange.valueRead, true, 64); diff --git a/hedera-mirror-rest/viewmodel/contractResultViewModel.js b/hedera-mirror-rest/viewmodel/contractResultViewModel.js index dad71671839..9717f1f0ba0 100644 --- a/hedera-mirror-rest/viewmodel/contractResultViewModel.js +++ b/hedera-mirror-rest/viewmodel/contractResultViewModel.js @@ -41,12 +41,12 @@ class ContractResultViewModel { this.contract_id = contractId.toString(); this.created_contract_ids = _.toArray(contractResult.createdContractIds).map((id) => EntityId.parse(id).toString()); this.error_message = _.isEmpty(contractResult.errorMessage) ? null : contractResult.errorMessage; - this.from = EntityId.parse(contractResult.payerAccountId).toSolidityAddress(); + this.from = EntityId.parse(contractResult.payerAccountId).toEvmAddress(); this.function_parameters = utils.toHexString(contractResult.functionParameters, true); this.gas_limit = Number(contractResult.gasLimit); this.gas_used = _.isNil(contractResult.gasUsed) ? null : Number(contractResult.gasUsed); this.timestamp = utils.nsToSecNs(contractResult.consensusTimestamp); - this.to = contractId.toSolidityAddress(); + this.to = contractId.toEvmAddress(); } } diff --git a/hedera-mirror-rest/viewmodel/contractViewModel.js b/hedera-mirror-rest/viewmodel/contractViewModel.js index 7542df5344c..94161f90966 100644 --- a/hedera-mirror-rest/viewmodel/contractViewModel.js +++ b/hedera-mirror-rest/viewmodel/contractViewModel.js @@ -34,23 +34,22 @@ class ContractViewModel { */ constructor(contract) { const contractId = EntityId.parse(contract.id); - Object.assign(this, { - admin_key: utils.encodeKey(contract.key), - auto_renew_period: contract.autoRenewPeriod && Number(contract.autoRenewPeriod), - contract_id: contractId.toString(), - created_timestamp: utils.nsToSecNs(contract.createdTimestamp), - deleted: contract.deleted, - expiration_timestamp: utils.nsToSecNs(contract.expirationTimestamp), - file_id: EntityId.parse(contract.fileId, true).toString(), - memo: contract.memo, - obtainer_id: EntityId.parse(contract.obtainerId, true).toString(), - proxy_account_id: EntityId.parse(contract.proxyAccountId, true).toString(), - solidity_address: contractId.toSolidityAddress(), - timestamp: { - from: utils.nsToSecNs(contract.timestampRange.begin), - to: utils.nsToSecNs(contract.timestampRange.end), - }, - }); + this.admin_key = utils.encodeKey(contract.key); + this.auto_renew_period = contract.autoRenewPeriod && Number(contract.autoRenewPeriod); + this.contract_id = contractId.toString(); + this.created_timestamp = utils.nsToSecNs(contract.createdTimestamp); + this.deleted = contract.deleted; + this.evm_address = + contract.evmAddress !== null ? utils.toHexString(contract.evmAddress, true) : contractId.toEvmAddress(); + this.expiration_timestamp = utils.nsToSecNs(contract.expirationTimestamp); + this.file_id = EntityId.parse(contract.fileId, true).toString(); + this.memo = contract.memo; + this.obtainer_id = EntityId.parse(contract.obtainerId, true).toString(); + this.proxy_account_id = EntityId.parse(contract.proxyAccountId, true).toString(); + this.timestamp = { + from: utils.nsToSecNs(contract.timestampRange.begin), + to: utils.nsToSecNs(contract.timestampRange.end), + }; if (contract.bytecode !== undefined) { this.bytecode = utils.toHexString(contract.bytecode, true);