diff --git a/docs/design/allowances.md b/docs/design/allowances.md new file mode 100644 index 00000000000..7bee31e290d --- /dev/null +++ b/docs/design/allowances.md @@ -0,0 +1,244 @@ +# HIP-336 Approval and Allowance + +## Purpose + +[HIP-336](https://hips.hedera.com/hip/hip-336) describes new APIs to approve and exercise allowances to a delegate +account. An allowance grants a spender the right to transfer a predetermined maximum limit of the payer's hbars or +tokens to another account of the spender's choice. + +## Goals + +* Enhance the database schema to store an account's allowances +* Store the historical state of allowances +* Enhance the REST API to show an account's crypto and token allowances + +## Non-Goals + +* Store the live state of allowances adjusted for each crypto transfer +* Enhance gRPC APIs with allowance information +* Enhance Web3 APIs with allowance information + +## Architecture + +### Database + +#### Crypto Allowance + +```sql +create table if not exists crypto_allowance +( + amount bigint not null, + owner bigint not null, + payer_account_id bigint not null, + spender bigint not null, + timestamp_range int8range not null, + primary key (owner, spender) +); +``` + +```sql +create table if not exists crypto_allowance_history +( + like crypto_allowance including defaults, + primary key (owner, spender, timestamp_range) +); +``` + +#### NFT Allowance + +```sql +create table if not exists nft_allowance +( + approved_for_all boolean not null, + owner bigint not null, + payer_account_id bigint not null, + serial_numbers bigint[] not null, + spender bigint not null, + timestamp_range int8range not null, + token_id bigint not null, + primary key (owner, spender, token_id) +); +``` + +```sql +create table if not exists nft_allowance_history +( + like nft_allowance including defaults, + primary key (owner, spender, token_id, timestamp_range) +); +``` + +#### Token Allowance + +```sql +create table if not exists token_allowance +( + amount bigint not null, + owner bigint not null, + payer_account_id bigint not null, + spender bigint not null, + timestamp_range int8range not null, + token_id bigint not null, + primary key (owner, spender, token_id) +); +``` + +```sql +create table if not exists token_allowance_history +( + like token_allowance including defaults, + primary key (owner, spender, token_id, timestamp_range) +); +``` + +### REST API + +#### Crypto Allowances + +`/api/v1/accounts/{accountId}/allowances/crypto` + +```json +{ + "allowances": [ + { + "amount": 10, + "owner": "0.0.1000", + "payer_account_id": "0.0.1000", + "spender": "0.0.8488", + "timestamp": { + "from": "1633466229.96874612", + "to": "1633466568.31556926" + } + }, + { + "amount": 5, + "owner": "0.0.1000", + "payer_account_id": "0.0.1001", + "spender": "0.0.9857", + "timestamp": { + "from": "1633466229.96874612", + "to": null + } + } + ], + "links": {} +} +``` + +Optional Filters + +* `limit`: The maximum amount of items to return. +* `order`: Order by `spender`. Accepts `asc` or `desc` with a default of `asc`. +* `spender`: Filter by the spender account ID. Only need to support `eq` operator and allow multiple. + +#### NFT Allowances + +`/api/v1/accounts/{accountId}/allowances/nfts` + +```json +{ + "allowances": [ + { + "approved_for_all": false, + "owner": "0.0.1000", + "payer_account_id": "0.0.1000", + "serial_numbers": [ + 1, + 2, + 3 + ], + "spender": "0.0.8488", + "token_id": "0.0.1032", + "timestamp": { + "from": "1633466229.96874612", + "to": "1633466568.31556926" + } + }, + { + "approved_for_all": true, + "owner": "0.0.1000", + "payer_account_id": "0.0.1000", + "serial_numbers": [], + "spender": "0.0.9857", + "token_id": "0.0.1032", + "timestamp": { + "from": "1633466229.96874612", + "to": null + } + } + ], + "links": {} +} +``` + +Optional Filters + +* `limit`: The maximum amount of items to return. +* `order`: Order by `spender` and `token_id`. Accepts `asc` or `desc` with a default of `asc`. +* `spender`: Filter by the spender account ID. Only need to support `eq` operator and allow multiple. +* `token.id`: Filter by the token ID. Only need to support `eq` operator and allow multiple. + +#### Token Allowances + +`/api/v1/accounts/{accountId}/allowances/tokens` + +```json +{ + "allowances": [ + { + "amount": 10, + "owner": "0.0.1000", + "payer_account_id": "0.0.1000", + "spender": "0.0.8488", + "token_id": "0.0.1032", + "timestamp": { + "from": "1633466229.96874612", + "to": "1633466568.31556926" + } + }, + { + "amount": 5, + "owner": "0.0.1000", + "payer_account_id": "0.0.1000", + "spender": "0.0.9857", + "token_id": "0.0.1032", + "timestamp": { + "from": "1633466229.96874612", + "to": null + } + } + ], + "links": {} +} +``` + +Optional Filters + +* `limit`: The maximum amount of items to return. +* `order`: Order by `spender` and `token_id`. Accepts `asc` or `desc` with a default of `asc`. +* `spender`: Filter by the spender account ID. Only need to support `eq` operator and allow multiple. +* `token.id`: Filter by the token ID. Only need to support `eq` operator and allow multiple. + +#### Transactions APIs + +Update all APIs that show transfers to return `is_approval` in its response. Including `/api/v1/accounts/:id` and all +the transactions REST APIs. + +## Non-Functional Requirements + +* Ingest new transaction types at the same rate as consensus nodes + +## Open Questions + +1) How will we do REST API pagination using multiple columns? + +## Answered Questions + +1) How will we handle adjust allowance for serial numbers? + + The full list of allowed serials that result from the transaction will be provided in the record. + +2) What happens if client populates both `approvedForAll` and `serialNumbers`? + + It is an error if they populate `approvedForAll=true` and a non-empty `serialNumbers`. It is allowed, but not + required, to populate `approvedForAll=false` when providing a non-empty `serialNumbers`. diff --git a/docs/design/smart-contracts.md b/docs/design/smart-contracts.md index 04dc2a3ea78..1daeee6c774 100644 --- a/docs/design/smart-contracts.md +++ b/docs/design/smart-contracts.md @@ -50,11 +50,9 @@ create table if not exists contract realm bigint not null, shard bigint not null, timestamp_range int8range not null, - type entity_type default 'CONTRACT' not null + type entity_type default 'CONTRACT' not null, + primary key (id) ); - -alter table if exists contract - add primary key (id); ``` #### Contract History @@ -159,7 +157,7 @@ create table if not exists contract_state_change ); ``` -## Importer +### Importer - Add a `Contract` domain object with fields that match the schema. - Add a `ContractAccess` domain object with fields that match the schema. @@ -179,9 +177,9 @@ create table if not exists contract_state_change the `Contract` domain object. - Remove logic specific to contracts in `EntityRecordItemListener`. -## REST API +### REST API -### List Contracts +#### List Contracts `GET /api/v1/contracts` @@ -221,7 +219,7 @@ Optional filters - `limit` - `order` -### Get Contract +#### Get Contract `GET /api/v1/contracts/{id}` @@ -254,7 +252,7 @@ Optional filters - `timestamp` Return the historical state of the contract. Supports all the operators but returns the latest version of the contract within that time range. -### List Contract Results +#### List Contract Results `GET /api/v1/contracts/{id}/results` @@ -294,7 +292,7 @@ Optional filters - `timestamp` - `from` -### Get Contract Result +#### Get Contract Result `GET /api/v1/contracts/{id}/results/{timestamp}` & `GET /api/v1/contracts/results/{transactionId}` @@ -382,7 +380,7 @@ Optional filters > _Note:_ `/api/v1/contracts/results/{transactionId}` will have to extract the correlating contractId and timestamp to > retrieve the correct contract_result row -### Get Contract Logs +#### Get Contract Logs `GET /api/v1/contracts/{id}/results/logs` @@ -439,7 +437,7 @@ Optional filters > _Note3:_ This API will only return logs for the given contract id. It will not return logs > generated by a child or parent contract. -## JSON-RPC +### JSON-RPC On the Ethereum network, all client nodes implement the [Ethereum JSON-RPC Specification](https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/ethereum/eth1.0-apis/assembled-spec/openrpc.json) @@ -453,7 +451,7 @@ The Mirror Node should implement a subset of the standard calls used to: - Support existing Ethereum developers who may call the JSON-RPC endpoints directly. - Encompass Hedera EVM translation logic that can be wrapped by potential Web3 modules. -### Setup +#### Setup - Create a new Maven module `hedera-mirror-web3` - Create a new Maven module `hedera-mirror-common` that encompasses all domain POJOs and repositories @@ -468,7 +466,7 @@ The Mirror Node should implement a subset of the standard calls used to: Existing domain classes can be utilized from the `hedera-mirror-common` dependencies. Applicable CRUD repositories can be created using Spring based on `hedera-mirror-common` domains to extract information from the database. -### JSON-RPC Service +#### Service - `Web3Service` interface that describes the supported rpc methods - Implement `Web3Service` for each of the supported RPC methods. Methods query the appropriate tables and return data in diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/History.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/History.java new file mode 100644 index 00000000000..9887268eef6 --- /dev/null +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/History.java @@ -0,0 +1,51 @@ +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.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.collect.Range; + +public interface History { + + Range getTimestampRange(); + + void setTimestampRange(Range timestampRange); + + @JsonIgnore + default Long getTimestampLower() { + var timestampRange = getTimestampRange(); + return timestampRange != null && timestampRange.hasLowerBound() ? timestampRange.lowerEndpoint() : null; + } + + default void setTimestampLower(long timestampLower) { + setTimestampRange(Range.atLeast(timestampLower)); + } + + @JsonIgnore + default Long getTimestampUpper() { + var timestampRange = getTimestampRange(); + return timestampRange != null && timestampRange.hasUpperBound() ? timestampRange.upperEndpoint() : null; + } + + default void setTimestampUpper(long timestampUpper) { + setTimestampRange(Range.closedOpen(getTimestampLower(), timestampUpper)); + } +} diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/contract/ContractStateChange.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/contract/ContractStateChange.java index c795cb1f167..706606522fc 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/contract/ContractStateChange.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/contract/ContractStateChange.java @@ -48,7 +48,8 @@ public class ContractStateChange implements Persistable private long consensusTimestamp; @Convert(converter = ContractIdConverter.class) - private EntityId contractId; + @javax.persistence.Id + private long contractId; @Convert(converter = AccountIdConverter.class) private EntityId payerAccountId; @@ -79,11 +80,15 @@ public boolean isNew() { return true; } + public void setContractId(EntityId contractId) { + this.contractId = contractId.getId(); + } + @Data public static class Id implements Serializable { private static final long serialVersionUID = -3677350664183037811L; private long consensusTimestamp; - private EntityId contractId; + private long contractId; private byte[] slot; } } diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/AbstractEntity.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/AbstractEntity.java index f3f536add2f..0dccb239c8d 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/AbstractEntity.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/AbstractEntity.java @@ -20,7 +20,6 @@ * ‍ */ -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.collect.Range; @@ -42,6 +41,7 @@ import com.hedera.mirror.common.converter.AccountIdConverter; import com.hedera.mirror.common.converter.RangeToStringDeserializer; import com.hedera.mirror.common.converter.RangeToStringSerializer; +import com.hedera.mirror.common.domain.History; import com.hedera.mirror.common.domain.Upsertable; import com.hedera.mirror.common.util.DomainUtils; @@ -58,7 +58,7 @@ typeClass = PostgreSQLEnumType.class ) @Upsertable(history = true) -public abstract class AbstractEntity { +public abstract class AbstractEntity implements History { private Long autoRenewPeriod; @@ -100,19 +100,6 @@ public abstract class AbstractEntity { @JsonSerialize(using = RangeToStringSerializer.class) private Range timestampRange; - public void setTimestampRangeUpper(long timestampRangeUpper) { - setTimestampRange(Range.closedOpen(getModifiedTimestamp(), timestampRangeUpper)); - } - - @JsonIgnore - public Long getModifiedTimestamp() { - return timestampRange != null ? timestampRange.lowerEndpoint() : null; - } - - public void setModifiedTimestamp(long modifiedTimestamp) { - timestampRange = Range.atLeast(modifiedTimestamp); - } - public void setKey(byte[] key) { this.key = key; publicKey = DomainUtils.getPublicKey(key); diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/CryptoAllowance.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/CryptoAllowance.java new file mode 100644 index 00000000000..637ceac339b --- /dev/null +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/CryptoAllowance.java @@ -0,0 +1,85 @@ +package com.hedera.mirror.common.domain.entity; + +/*- + * ‌ + * 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.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.collect.Range; +import com.vladmihalcea.hibernate.type.range.guava.PostgreSQLGuavaRangeType; +import java.io.Serializable; +import javax.persistence.Convert; +import javax.persistence.IdClass; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.TypeDef; + +import com.hedera.mirror.common.converter.AccountIdConverter; +import com.hedera.mirror.common.converter.RangeToStringDeserializer; +import com.hedera.mirror.common.converter.RangeToStringSerializer; +import com.hedera.mirror.common.domain.History; +import com.hedera.mirror.common.domain.Upsertable; + +@Data +@javax.persistence.Entity +@IdClass(CryptoAllowance.Id.class) +@NoArgsConstructor +@SuperBuilder +@TypeDef( + defaultForType = Range.class, + typeClass = PostgreSQLGuavaRangeType.class +) +@Upsertable(history = true) +public class CryptoAllowance implements History { + + private long amount; + + @javax.persistence.Id + private long owner; + + @Convert(converter = AccountIdConverter.class) + private EntityId payerAccountId; + + @javax.persistence.Id + private long spender; + + @JsonDeserialize(using = RangeToStringDeserializer.class) + @JsonSerialize(using = RangeToStringSerializer.class) + private Range timestampRange; + + @JsonIgnore + public Id getId() { + Id id = new Id(); + id.setOwner(owner); + id.setSpender(spender); + return id; + } + + @Data + public static class Id implements Serializable { + + private static final long serialVersionUID = 4078820027811154183L; + + private long owner; + private long spender; + } +} diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/NftAllowance.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/NftAllowance.java new file mode 100644 index 00000000000..075443d4a51 --- /dev/null +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/NftAllowance.java @@ -0,0 +1,98 @@ +package com.hedera.mirror.common.domain.entity; + +/*- + * ‌ + * 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.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.collect.Range; +import com.vladmihalcea.hibernate.type.range.guava.PostgreSQLGuavaRangeType; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import javax.persistence.Convert; +import javax.persistence.IdClass; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; + +import com.hedera.mirror.common.converter.AccountIdConverter; +import com.hedera.mirror.common.converter.LongListToStringSerializer; +import com.hedera.mirror.common.converter.RangeToStringDeserializer; +import com.hedera.mirror.common.converter.RangeToStringSerializer; +import com.hedera.mirror.common.domain.History; +import com.hedera.mirror.common.domain.Upsertable; + +@Data +@javax.persistence.Entity +@IdClass(NftAllowance.Id.class) +@NoArgsConstructor +@SuperBuilder +@TypeDef( + defaultForType = Range.class, + typeClass = PostgreSQLGuavaRangeType.class +) +@Upsertable(history = true) +public class NftAllowance implements History { + + private boolean approvedForAll; + + @javax.persistence.Id + private long owner; + + @Convert(converter = AccountIdConverter.class) + private EntityId payerAccountId; + + @Type(type = "com.vladmihalcea.hibernate.type.array.ListArrayType") + @JsonSerialize(using = LongListToStringSerializer.class) + private List serialNumbers = Collections.emptyList(); + + @javax.persistence.Id + private long spender; + + @JsonDeserialize(using = RangeToStringDeserializer.class) + @JsonSerialize(using = RangeToStringSerializer.class) + private Range timestampRange; + + @javax.persistence.Id + private long tokenId; + + @JsonIgnore + public NftAllowance.Id getId() { + Id id = new Id(); + id.setOwner(owner); + id.setSpender(spender); + id.setTokenId(tokenId); + return id; + } + + @Data + public static class Id implements Serializable { + + private static final long serialVersionUID = 4078820027811154183L; + + private long owner; + private long spender; + private long tokenId; + } +} diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/TokenAllowance.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/TokenAllowance.java new file mode 100644 index 00000000000..f4452192f5c --- /dev/null +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/entity/TokenAllowance.java @@ -0,0 +1,90 @@ +package com.hedera.mirror.common.domain.entity; + +/*- + * ‌ + * 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.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.collect.Range; +import com.vladmihalcea.hibernate.type.range.guava.PostgreSQLGuavaRangeType; +import java.io.Serializable; +import javax.persistence.Convert; +import javax.persistence.IdClass; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.TypeDef; + +import com.hedera.mirror.common.converter.AccountIdConverter; +import com.hedera.mirror.common.converter.RangeToStringDeserializer; +import com.hedera.mirror.common.converter.RangeToStringSerializer; +import com.hedera.mirror.common.domain.History; +import com.hedera.mirror.common.domain.Upsertable; + +@Data +@javax.persistence.Entity +@IdClass(TokenAllowance.Id.class) +@NoArgsConstructor +@SuperBuilder +@TypeDef( + defaultForType = Range.class, + typeClass = PostgreSQLGuavaRangeType.class +) +@Upsertable(history = true) +public class TokenAllowance implements History { + + private long amount; + + @javax.persistence.Id + private long owner; + + @Convert(converter = AccountIdConverter.class) + private EntityId payerAccountId; + + @javax.persistence.Id + private long spender; + + @JsonDeserialize(using = RangeToStringDeserializer.class) + @JsonSerialize(using = RangeToStringSerializer.class) + private Range timestampRange; + + @javax.persistence.Id + private long tokenId; + + @JsonIgnore + public TokenAllowance.Id getId() { + Id id = new Id(); + id.setOwner(owner); + id.setSpender(spender); + id.setTokenId(tokenId); + return id; + } + + @Data + public static class Id implements Serializable { + + private static final long serialVersionUID = 4078820027811154183L; + + private long owner; + private long spender; + private long tokenId; + } +} diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/NftTransfer.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/NftTransfer.java index 5b9edbdfabf..265120573d3 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/NftTransfer.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/NftTransfer.java @@ -26,9 +26,6 @@ import javax.persistence.Convert; import javax.persistence.EmbeddedId; import javax.persistence.Entity; - -import com.hedera.mirror.common.domain.entity.EntityId; - import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -38,6 +35,7 @@ import com.hedera.mirror.common.converter.AccountIdConverter; import com.hedera.mirror.common.converter.EntityIdSerializer; +import com.hedera.mirror.common.domain.entity.EntityId; @AllArgsConstructor(access = AccessLevel.PRIVATE) // For Builder @Builder @@ -50,6 +48,8 @@ public class NftTransfer implements Persistable { @JsonUnwrapped private NftTransferId id; + private Boolean isApproval; + @Convert(converter = AccountIdConverter.class) private EntityId payerAccountId; diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/TokenTransfer.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/TokenTransfer.java index cbe16c05c41..70ee6886fe8 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/TokenTransfer.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/TokenTransfer.java @@ -29,9 +29,6 @@ import javax.persistence.EmbeddedId; import javax.persistence.Entity; import javax.persistence.Transient; - -import com.hedera.mirror.common.domain.entity.EntityId; - import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -42,6 +39,7 @@ import com.hedera.mirror.common.converter.AccountIdConverter; import com.hedera.mirror.common.converter.EntityIdSerializer; import com.hedera.mirror.common.converter.TokenIdConverter; +import com.hedera.mirror.common.domain.entity.EntityId; @AllArgsConstructor(access = AccessLevel.PRIVATE) // For Builder @Builder @@ -56,6 +54,8 @@ public class TokenTransfer implements Persistable { private long amount; + private Boolean isApproval; + @Convert(converter = AccountIdConverter.class) private EntityId payerAccountId; diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/CryptoTransfer.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/CryptoTransfer.java index a96d6318fab..9c845bc9cba 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/CryptoTransfer.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/CryptoTransfer.java @@ -27,15 +27,13 @@ import javax.persistence.Embeddable; import javax.persistence.EmbeddedId; import javax.persistence.Entity; - -import com.hedera.mirror.common.domain.entity.EntityId; - import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.data.domain.Persistable; import com.hedera.mirror.common.converter.AccountIdConverter; +import com.hedera.mirror.common.domain.entity.EntityId; @Data @Entity @@ -52,6 +50,8 @@ public class CryptoTransfer implements Persistable { @JsonUnwrapped private Id id; + private Boolean isApproval; + @Convert(converter = AccountIdConverter.class) private EntityId payerAccountId; diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/NonFeeTransfer.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/NonFeeTransfer.java index 6f7b2936472..675eddde617 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/NonFeeTransfer.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/NonFeeTransfer.java @@ -27,9 +27,6 @@ import javax.persistence.Embeddable; import javax.persistence.EmbeddedId; import javax.persistence.Entity; - -import com.hedera.mirror.common.domain.entity.EntityId; - import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -38,6 +35,7 @@ import org.springframework.data.domain.Persistable; import com.hedera.mirror.common.converter.AccountIdConverter; +import com.hedera.mirror.common.domain.entity.EntityId; @AllArgsConstructor(access = AccessLevel.PRIVATE) // For Builder @Builder @@ -52,6 +50,8 @@ public class NonFeeTransfer implements Persistable { @JsonUnwrapped private NonFeeTransfer.Id id; + private Boolean isApproval; + @Convert(converter = AccountIdConverter.class) private EntityId payerAccountId; diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/Transaction.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/Transaction.java index 470c298ed71..ee70d098c92 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/Transaction.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/Transaction.java @@ -25,10 +25,9 @@ import javax.persistence.Convert; import javax.persistence.Entity; import javax.persistence.Id; - -import com.hedera.mirror.common.domain.entity.EntityId; - +import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; @@ -37,12 +36,13 @@ import com.hedera.mirror.common.converter.AccountIdConverter; import com.hedera.mirror.common.converter.EntityIdSerializer; import com.hedera.mirror.common.converter.UnknownIdConverter; +import com.hedera.mirror.common.domain.entity.EntityId; +@AllArgsConstructor(access = AccessLevel.PRIVATE) // For builder +@Builder @Data @Entity @NoArgsConstructor -@AllArgsConstructor -@ToString(exclude = {"memo", "transactionHash", "transactionBytes"}) public class Transaction implements Persistable { @Id @@ -56,6 +56,7 @@ public class Transaction implements Persistable { private Long initialBalance; + @ToString.Exclude private byte[] memo; private Long maxFee; @@ -76,8 +77,10 @@ public class Transaction implements Persistable { private boolean scheduled; + @ToString.Exclude private byte[] transactionBytes; + @ToString.Exclude private byte[] transactionHash; private Integer type; diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/TransactionType.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/TransactionType.java index e813ae61dcb..4ec45c53769 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/TransactionType.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/transaction/TransactionType.java @@ -73,7 +73,9 @@ public enum TransactionType { SCHEDULESIGN(44, EntityOperation.NONE), TOKENFEESCHEDULEUPDATE(45, EntityOperation.NONE), TOKENPAUSE(46, EntityOperation.NONE), - TOKENUNPAUSE(47, EntityOperation.NONE); + TOKENUNPAUSE(47, EntityOperation.NONE), + CRYPTOADJUSTALLOWANCE(48, EntityOperation.NONE), + CRYPTOAPPROVEALLOWANCE(49, EntityOperation.NONE); private static final Map idMap = Arrays.stream(values()) .collect(Collectors.toMap(TransactionType::getProtoId, Function.identity())); 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 ed5fca4594d..439310064c2 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 @@ -29,6 +29,7 @@ import com.google.common.collect.Range; import com.google.protobuf.ByteString; import com.hederahashgraph.api.proto.java.Key; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.SignaturePair; import java.net.InetAddress; import java.net.UnknownHostException; @@ -57,9 +58,12 @@ import com.hedera.mirror.common.domain.contract.ContractLog; import com.hedera.mirror.common.domain.contract.ContractResult; import com.hedera.mirror.common.domain.contract.ContractStateChange; +import com.hedera.mirror.common.domain.entity.CryptoAllowance; import com.hedera.mirror.common.domain.entity.Entity; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.EntityType; +import com.hedera.mirror.common.domain.entity.NftAllowance; +import com.hedera.mirror.common.domain.entity.TokenAllowance; import com.hedera.mirror.common.domain.schedule.Schedule; import com.hedera.mirror.common.domain.token.NftTransfer; import com.hedera.mirror.common.domain.token.NftTransferId; @@ -69,7 +73,9 @@ import com.hedera.mirror.common.domain.token.TokenTransfer; import com.hedera.mirror.common.domain.transaction.NonFeeTransfer; 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.common.domain.transaction.TransactionType; import com.hedera.mirror.common.util.DomainUtils; @Component @@ -208,7 +214,7 @@ public DomainWrapper contr public DomainWrapper contractStateChange() { ContractStateChange.ContractStateChangeBuilder builder = ContractStateChange.builder() .consensusTimestamp(timestamp()) - .contractId(entityId(CONTRACT)) + .contractId(entityId(CONTRACT).getId()) .payerAccountId(entityId(ACCOUNT)) .slot(bytes(128)) .valueRead(bytes(64)) @@ -216,6 +222,16 @@ public DomainWrapper(builder, builder::build); } + public DomainWrapper cryptoAllowance() { + var builder = CryptoAllowance.builder() + .amount(10) + .owner(entityId(ACCOUNT).getId()) + .payerAccountId(entityId(ACCOUNT)) + .spender(entityId(ACCOUNT).getId()) + .timestampRange(Range.atLeast(timestamp())); + return new DomainWrapperImpl<>(builder, builder::build); + } + public DomainWrapper entity() { long id = id(); long timestamp = timestamp(); @@ -312,6 +328,29 @@ public DomainWrapper token() { return new DomainWrapperImpl<>(builder, builder::build); } + public DomainWrapper nftAllowance() { + var builder = NftAllowance.builder() + .approvedForAll(false) + .owner(entityId(ACCOUNT).getId()) + .payerAccountId(entityId(ACCOUNT)) + .serialNumbers(List.of(1L, 2L, 3L)) + .spender(entityId(ACCOUNT).getId()) + .timestampRange(Range.atLeast(timestamp())) + .tokenId(entityId(TOKEN).getId()); + return new DomainWrapperImpl<>(builder, builder::build); + } + + public DomainWrapper tokenAllowance() { + var builder = TokenAllowance.builder() + .amount(10L) + .owner(entityId(ACCOUNT).getId()) + .payerAccountId(entityId(ACCOUNT)) + .spender(entityId(ACCOUNT).getId()) + .timestampRange(Range.atLeast(timestamp())) + .tokenId(entityId(TOKEN).getId()); + return new DomainWrapperImpl<>(builder, builder::build); + } + public DomainWrapper tokenTransfer() { TokenTransfer.TokenTransferBuilder builder = TokenTransfer.builder() .amount(100L) @@ -322,6 +361,28 @@ public DomainWrapper tokenTra return new DomainWrapperImpl<>(builder, builder::build); } + public DomainWrapper transaction() { + Transaction.TransactionBuilder builder = Transaction.builder() + .chargedTxFee(10000000L) + .consensusTimestamp(timestamp()) + .entityId(entityId(ACCOUNT)) + .initialBalance(10000000L) + .maxFee(100000000L) + .memo(bytes(10)) + .nodeAccountId(entityId(ACCOUNT)) + .nonce(0) + .parentConsensusTimestamp(timestamp()) + .payerAccountId(entityId(ACCOUNT)) + .result(ResponseCodeEnum.SUCCESS.getNumber()) + .scheduled(false) + .transactionBytes(bytes(100)) + .transactionHash(bytes(48)) + .type(TransactionType.CRYPTOTRANSFER.getProtoId()) + .validStartNs(timestamp()) + .validDurationSeconds(120L); + return new DomainWrapperImpl<>(builder, builder::build); + } + public DomainWrapper transactionSignature() { TransactionSignature.TransactionSignatureBuilder builder = TransactionSignature.builder() .consensusTimestamp(timestamp()) diff --git a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/entity/EntityTest.java b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/entity/EntityTest.java index fae27a975b5..175f41ebee2 100644 --- a/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/entity/EntityTest.java +++ b/hedera-mirror-common/src/test/java/com/hedera/mirror/common/domain/entity/EntityTest.java @@ -22,6 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.google.common.collect.Range; import org.junit.jupiter.api.Test; class EntityTest { @@ -32,4 +33,30 @@ void nullCharacters() { entity.setMemo("abc" + (char) 0); assertThat(entity.getMemo()).isEqualTo("abc�"); } + + @Test + void history() { + Entity entity = new Entity(); + assertThat(entity.getTimestampRange()).isNull(); + assertThat(entity.getTimestampLower()).isNull(); + assertThat(entity.getTimestampUpper()).isNull(); + + Range timestampRangeLower = Range.atLeast(1L); + entity.setTimestampRange(timestampRangeLower); + assertThat(entity.getTimestampRange()).isEqualTo(timestampRangeLower); + assertThat(entity.getTimestampLower()).isEqualTo(timestampRangeLower.lowerEndpoint()); + assertThat(entity.getTimestampUpper()).isNull(); + + entity.setTimestampUpper(2L); + assertThat(entity.getTimestampUpper()).isEqualTo(2L); + + Range timestampRangeUpper = Range.atMost(1L); + entity.setTimestampRange(timestampRangeUpper); + assertThat(entity.getTimestampRange()).isEqualTo(timestampRangeUpper); + assertThat(entity.getTimestampLower()).isNull(); + assertThat(entity.getTimestampUpper()).isEqualTo(timestampRangeUpper.upperEndpoint()); + + entity.setTimestampLower(0L); + assertThat(entity.getTimestampLower()).isZero(); + } } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/batch/BatchPersister.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/batch/BatchPersister.java index 060c1ecd156..bfb09810e8f 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/batch/BatchPersister.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/batch/BatchPersister.java @@ -25,8 +25,6 @@ /** * Performs bulk insertion of domain objects to the database. For some domain types it might be insert-only while others * may use upsert logic. - * - * @param the domain type to batch persist */ public interface BatchPersister { 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 3bb342331d6..9db805735c6 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 @@ -49,13 +49,8 @@ public class NonFeeTransferExtractionStrategyImpl implements NonFeeTransferExtra */ @Override public Iterable extractNonFeeTransfers(TransactionBody body, TransactionRecord transactionRecord) { - // Only these types of transactions have non-fee transfers. - if (!body.hasCryptoCreateAccount() && !body.hasContractCreateInstance() && !body.hasCryptoTransfer() - && !body.hasContractCall()) { - return Collections.emptyList(); - } - AccountID payerAccountId = body.getTransactionID().getAccountID(); + if (body.hasCryptoTransfer()) { return body.getCryptoTransfer().getTransfers().getAccountAmountsList(); } else if (body.hasCryptoCreateAccount()) { @@ -64,7 +59,8 @@ public Iterable extractNonFeeTransfers(TransactionBody body, Tran } else if (body.hasContractCreateInstance()) { return extractForCreateEntity(body.getContractCreateInstance().getInitialBalance(), payerAccountId, contractIdToAccountId(transactionRecord.getReceipt().getContractID()), transactionRecord); - } else { // contractCall + } else if (body.hasContractCall()) { + EntityId contractId = entityIdService.lookup(transactionRecord.getReceipt().getContractID(), body.getContractCall().getContractID()); LinkedList result = new LinkedList<>(); @@ -75,19 +71,39 @@ public Iterable extractNonFeeTransfers(TransactionBody body, Tran .setRealmNum(contractId.getRealmNum()) .setAccountNum(contractId.getEntityNum()) .build(); - result.add(AccountAmount.newBuilder().setAccountID(contractAccountId).setAmount(amount).build()); - result.add(AccountAmount.newBuilder().setAccountID(payerAccountId).setAmount(-amount).build()); + result.add(AccountAmount.newBuilder() + .setAccountID(contractAccountId) + .setAmount(amount) + .setIsApproval(false) + .build()); + result.add(AccountAmount.newBuilder() + .setAccountID(payerAccountId) + .setAmount(-amount) + .setIsApproval(false) + .build()); return result; + } else { + return Collections.emptyList(); } } private Iterable extractForCreateEntity( long initialBalance, AccountID payerAccountId, AccountID createdEntity, TransactionRecord txRecord) { LinkedList result = new LinkedList<>(); - result.add(AccountAmount.newBuilder().setAccountID(payerAccountId).setAmount(-initialBalance).build()); + result.add(AccountAmount.newBuilder() + .setAccountID(payerAccountId) + .setAmount(-initialBalance) + .setIsApproval(false) + .build()); + if (ResponseCodeEnum.SUCCESS == txRecord.getReceipt().getStatus()) { - result.add(AccountAmount.newBuilder().setAccountID(createdEntity).setAmount(initialBalance).build()); + result.add(AccountAmount.newBuilder() + .setAccountID(createdEntity) + .setAmount(initialBalance) + .setIsApproval(false) + .build()); } + return result; } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/CompositeEntityListener.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/CompositeEntityListener.java index 75e335136bd..f27fe34637f 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/CompositeEntityListener.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/CompositeEntityListener.java @@ -31,7 +31,10 @@ import com.hedera.mirror.common.domain.contract.ContractLog; import com.hedera.mirror.common.domain.contract.ContractResult; import com.hedera.mirror.common.domain.contract.ContractStateChange; +import com.hedera.mirror.common.domain.entity.CryptoAllowance; import com.hedera.mirror.common.domain.entity.Entity; +import com.hedera.mirror.common.domain.entity.NftAllowance; +import com.hedera.mirror.common.domain.entity.TokenAllowance; import com.hedera.mirror.common.domain.file.FileData; import com.hedera.mirror.common.domain.schedule.Schedule; import com.hedera.mirror.common.domain.token.Nft; @@ -90,6 +93,11 @@ public void onContractStateChange(ContractStateChange contractStateChange) throw onEach(EntityListener::onContractStateChange, contractStateChange); } + @Override + public void onCryptoAllowance(CryptoAllowance cryptoAllowance) throws ImporterException { + onEach(EntityListener::onCryptoAllowance, cryptoAllowance); + } + @Override public void onCryptoTransfer(CryptoTransfer cryptoTransfer) throws ImporterException { onEach(EntityListener::onCryptoTransfer, cryptoTransfer); @@ -125,6 +133,11 @@ public void onNft(Nft nft) throws ImporterException { onEach(EntityListener::onNft, nft); } + @Override + public void onNftAllowance(NftAllowance nftAllowance) throws ImporterException { + onEach(EntityListener::onNftAllowance, nftAllowance); + } + @Override public void onNftTransfer(NftTransfer nftTransfer) throws ImporterException { onEach(EntityListener::onNftTransfer, nftTransfer); @@ -145,6 +158,11 @@ public void onTokenAccount(TokenAccount tokenAccount) throws ImporterException { onEach(EntityListener::onTokenAccount, tokenAccount); } + @Override + public void onTokenAllowance(TokenAllowance tokenAllowance) throws ImporterException { + onEach(EntityListener::onTokenAllowance, tokenAllowance); + } + @Override public void onTokenTransfer(TokenTransfer tokenTransfer) throws ImporterException { onEach(EntityListener::onTokenTransfer, tokenTransfer); diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/EntityListener.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/EntityListener.java index 22b50aac3c4..3dc22e59154 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/EntityListener.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/EntityListener.java @@ -24,7 +24,10 @@ import com.hedera.mirror.common.domain.contract.ContractLog; import com.hedera.mirror.common.domain.contract.ContractResult; import com.hedera.mirror.common.domain.contract.ContractStateChange; +import com.hedera.mirror.common.domain.entity.CryptoAllowance; import com.hedera.mirror.common.domain.entity.Entity; +import com.hedera.mirror.common.domain.entity.NftAllowance; +import com.hedera.mirror.common.domain.entity.TokenAllowance; import com.hedera.mirror.common.domain.file.FileData; import com.hedera.mirror.common.domain.schedule.Schedule; import com.hedera.mirror.common.domain.token.Nft; @@ -66,6 +69,9 @@ default void onContractStateChange(ContractStateChange contractStateChange) { default void onContractResult(ContractResult contractResult) throws ImporterException { } + default void onCryptoAllowance(CryptoAllowance cryptoAllowance) { + } + default void onCustomFee(CustomFee customFee) throws ImporterException { } @@ -84,6 +90,9 @@ default void onLiveHash(LiveHash liveHash) throws ImporterException { default void onNft(Nft nft) throws ImporterException { } + default void onNftAllowance(NftAllowance nftAllowance) { + } + default void onNftTransfer(NftTransfer nftTransfer) throws ImporterException { } @@ -99,6 +108,9 @@ default void onToken(Token token) throws ImporterException { default void onTokenAccount(TokenAccount tokenAccount) throws ImporterException { } + default void onTokenAllowance(TokenAllowance tokenAllowance) { + } + default void onTokenTransfer(TokenTransfer tokenTransfer) throws ImporterException { } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListener.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListener.java index 505937bc7e0..a79c88b870b 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListener.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListener.java @@ -295,6 +295,7 @@ private void processNonFeeTransfers(long consensusTimestamp, RecordItem recordIt NonFeeTransfer nonFeeTransfer = new NonFeeTransfer(); nonFeeTransfer.setAmount(aa.getAmount()); nonFeeTransfer.setId(new NonFeeTransfer.Id(consensusTimestamp, entityId)); + nonFeeTransfer.setIsApproval(aa.getIsApproval()); nonFeeTransfer.setPayerAccountId(recordItem.getPayerAccountId()); entityListener.onNonFeeTransfer(nonFeeTransfer); } @@ -389,6 +390,7 @@ private void insertTransferList(long consensusTimestamp, TransferList transferLi var aa = transferList.getAccountAmounts(i); var account = EntityId.of(aa.getAccountID()); CryptoTransfer cryptoTransfer = new CryptoTransfer(consensusTimestamp, aa.getAmount(), account); + cryptoTransfer.setIsApproval(aa.getIsApproval()); cryptoTransfer.setPayerAccountId(payerAccountId); entityListener.onCryptoTransfer(cryptoTransfer); } @@ -406,6 +408,7 @@ var record = recordItem.getRecord(); var aa = transferList.getAccountAmounts(i); var account = EntityId.of(aa.getAccountID()); CryptoTransfer cryptoTransfer = new CryptoTransfer(consensusTimestamp, aa.getAmount(), account); + cryptoTransfer.setIsApproval(aa.getIsApproval()); cryptoTransfer.setPayerAccountId(recordItem.getPayerAccountId()); entityListener.onCryptoTransfer(cryptoTransfer); @@ -644,6 +647,7 @@ private void insertTokenTransfers(RecordItem recordItem) { TokenTransfer tokenTransfer = new TokenTransfer(); tokenTransfer.setAmount(amount); tokenTransfer.setId(new TokenTransfer.Id(consensusTimestamp, tokenId, accountId)); + tokenTransfer.setIsApproval(accountAmount.getIsApproval()); tokenTransfer.setPayerAccountId(recordItem.getPayerAccountId()); tokenTransfer.setTokenDissociate(isTokenDissociate); entityListener.onTokenTransfer(tokenTransfer); @@ -672,6 +676,7 @@ private void insertTokenTransfers(RecordItem recordItem) { NftTransfer nftTransferDomain = new NftTransfer(); nftTransferDomain.setId(new NftTransferId(consensusTimestamp, serialNumber, tokenId)); + nftTransferDomain.setIsApproval(nftTransfer.getIsApproval()); nftTransferDomain.setReceiverAccountId(receiverId); nftTransferDomain.setSenderAccountId(senderId); nftTransferDomain.setPayerAccountId(recordItem.getPayerAccountId()); 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 0536198a862..90f63e8ace5 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 @@ -39,8 +39,11 @@ import com.hedera.mirror.common.domain.contract.ContractResult; import com.hedera.mirror.common.domain.contract.ContractStateChange; import com.hedera.mirror.common.domain.entity.AbstractEntity; +import com.hedera.mirror.common.domain.entity.CryptoAllowance; import com.hedera.mirror.common.domain.entity.Entity; import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.entity.NftAllowance; +import com.hedera.mirror.common.domain.entity.TokenAllowance; import com.hedera.mirror.common.domain.file.FileData; import com.hedera.mirror.common.domain.schedule.Schedule; import com.hedera.mirror.common.domain.token.Nft; @@ -89,14 +92,17 @@ public class SqlEntityListener implements EntityListener, RecordStreamFileListen private final Collection contractLogs; private final Collection contractResults; private final Collection contractStateChanges; + private final Collection cryptoAllowances; private final Collection cryptoTransfers; private final Collection customFees; private final Collection entities; private final Collection fileData; private final Collection liveHashes; + private final Collection nftAllowances; private final Collection nftTransfers; private final Collection nonFeeTransfers; private final Collection tokenAccounts; + private final Collection tokenAllowances; private final Collection tokenDissociateTransfers; private final Collection tokenTransfers; private final Collection topicMessages; @@ -106,9 +112,12 @@ public class SqlEntityListener implements EntityListener, RecordStreamFileListen // maps of upgradable domains private final Map contractState; private final Map entityState; + private final Map cryptoAllowanceState; + private final Map nfts; + private final Map nftAllowanceState; private final Map schedules; private final Map tokens; - private final Map nfts; + private final Map tokenAllowanceState; // tracks the state of relationships in a batch, the initial state before the batch is in db. // for each update, merge the state and the update, save the merged state to the batch. @@ -134,26 +143,32 @@ public SqlEntityListener(BatchPersister batchPersister, contractLogs = new ArrayList<>(); contractResults = new ArrayList<>(); contractStateChanges = new ArrayList<>(); + cryptoAllowances = new ArrayList<>(); cryptoTransfers = new ArrayList<>(); customFees = new ArrayList<>(); entities = new ArrayList<>(); fileData = new ArrayList<>(); liveHashes = new ArrayList<>(); + nftAllowances = new ArrayList<>(); nftTransfers = new ArrayList<>(); nonFeeTransfers = new ArrayList<>(); + tokenAccounts = new ArrayList<>(); + tokenAllowances = new ArrayList<>(); tokenDissociateTransfers = new ArrayList<>(); tokenTransfers = new ArrayList<>(); - tokenAccounts = new ArrayList<>(); topicMessages = new ArrayList<>(); transactions = new ArrayList<>(); transactionSignatures = new ArrayList<>(); contractState = new HashMap<>(); + cryptoAllowanceState = new HashMap<>(); entityState = new HashMap<>(); nfts = new HashMap<>(); + nftAllowanceState = new HashMap<>(); schedules = new HashMap<>(); tokens = new HashMap<>(); tokenAccountState = new HashMap<>(); + tokenAllowanceState = new HashMap<>(); } @Override @@ -185,6 +200,8 @@ private void cleanup() { contractLogs.clear(); contractResults.clear(); contractStateChanges.clear(); + cryptoAllowances.clear(); + cryptoAllowanceState.clear(); cryptoTransfers.clear(); customFees.clear(); entities.clear(); @@ -193,11 +210,15 @@ private void cleanup() { liveHashes.clear(); nonFeeTransfers.clear(); nfts.clear(); + nftAllowances.clear(); + nftAllowanceState.clear(); nftTransfers.clear(); schedules.clear(); topicMessages.clear(); tokenAccounts.clear(); tokenAccountState.clear(); + tokenAllowances.clear(); + tokenAllowanceState.clear(); tokens.clear(); tokenDissociateTransfers.clear(); tokenTransfers.clear(); @@ -231,10 +252,13 @@ private void executeBatches() { // insert operations with conflict management batchPersister.persist(contracts); + batchPersister.persist(cryptoAllowances); batchPersister.persist(entities); + batchPersister.persist(nftAllowances); batchPersister.persist(tokens.values()); // ingest tokenAccounts after tokens since some fields of token accounts depends on the associated token batchPersister.persist(tokenAccounts); + batchPersister.persist(tokenAllowances); batchPersister.persist(nfts.values()); // persist nft after token entity batchPersister.persist(schedules.values()); @@ -283,6 +307,12 @@ public void onContractStateChange(ContractStateChange contractStateChange) { contractStateChanges.add(contractStateChange); } + @Override + public void onCryptoAllowance(CryptoAllowance cryptoAllowance) { + var merged = cryptoAllowanceState.merge(cryptoAllowance.getId(), cryptoAllowance, this::mergeCryptoAllowance); + cryptoAllowances.add(merged); + } + @Override public void onCryptoTransfer(CryptoTransfer cryptoTransfer) throws ImporterException { cryptoTransfers.add(cryptoTransfer); @@ -319,6 +349,12 @@ public void onNft(Nft nft) throws ImporterException { nfts.merge(nft.getId(), nft, this::mergeNft); } + @Override + public void onNftAllowance(NftAllowance nftAllowance) { + var merged = nftAllowanceState.merge(nftAllowance.getId(), nftAllowance, this::mergeNftAllowance); + nftAllowances.add(merged); + } + @Override public void onNftTransfer(NftTransfer nftTransfer) throws ImporterException { nftTransfers.add(nftTransfer); @@ -348,6 +384,13 @@ public void onTokenAccount(TokenAccount tokenAccount) throws ImporterException { tokenAccounts.add(merged); } + @Override + public void onTokenAllowance(TokenAllowance tokenAllowance) { + TokenAllowance merged = tokenAllowanceState.merge(tokenAllowance.getId(), tokenAllowance, + this::mergeTokenAllowance); + tokenAllowances.add(merged); + } + @Override public void onTokenTransfer(TokenTransfer tokenTransfer) throws ImporterException { if (tokenTransfer.isTokenDissociate()) { @@ -404,7 +447,7 @@ private T mergeAbstractEntity(T previous, T current) current.setProxyAccountId(previous.getProxyAccountId()); } - previous.setTimestampRangeUpper(current.getModifiedTimestamp()); + previous.setTimestampUpper(current.getTimestampLower()); return current; } @@ -420,6 +463,11 @@ private Contract mergeContract(Contract previous, Contract current) { return current; } + private CryptoAllowance mergeCryptoAllowance(CryptoAllowance previous, CryptoAllowance current) { + previous.setTimestampUpper(current.getTimestampLower()); + return current; + } + private Entity mergeEntity(Entity previous, Entity current) { mergeAbstractEntity(previous, current); @@ -463,6 +511,11 @@ private Nft mergeNft(Nft cachedNft, Nft newNft) { return cachedNft; } + private NftAllowance mergeNftAllowance(NftAllowance previous, NftAllowance current) { + previous.setTimestampUpper(current.getTimestampLower()); + return current; + } + private Schedule mergeSchedule(Schedule cachedSchedule, Schedule schedule) { cachedSchedule.setExecutedTimestamp(schedule.getExecutedTimestamp()); return cachedSchedule; @@ -548,4 +601,9 @@ private TokenAccount mergeTokenAccount(TokenAccount lastTokenAccount, TokenAccou return newTokenAccount; } + + private TokenAllowance mergeTokenAllowance(TokenAllowance previous, TokenAllowance current) { + previous.setTimestampUpper(current.getTimestampLower()); + return current; + } } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractAllowanceTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractAllowanceTransactionHandler.java new file mode 100644 index 00000000000..0cda7921052 --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractAllowanceTransactionHandler.java @@ -0,0 +1,89 @@ +package com.hedera.mirror.importer.parser.record.transactionhandler; + +/*- + * ‌ + * 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 java.util.List; +import lombok.RequiredArgsConstructor; + +import com.hedera.mirror.common.domain.entity.CryptoAllowance; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.entity.NftAllowance; +import com.hedera.mirror.common.domain.entity.TokenAllowance; +import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.common.domain.transaction.Transaction; +import com.hedera.mirror.importer.parser.record.entity.EntityListener; + +@RequiredArgsConstructor +abstract class AbstractAllowanceTransactionHandler implements TransactionHandler { + + private final EntityListener entityListener; + + @Override + public EntityId getEntity(RecordItem recordItem) { + return null; + } + + @Override + public void updateTransaction(Transaction transaction, RecordItem recordItem) { + long consensusTimestamp = transaction.getConsensusTimestamp(); + var payerAccountId = recordItem.getPayerAccountId(); + + for (var cryptoApproval : getCryptoAllowances(recordItem)) { + CryptoAllowance cryptoAllowance = new CryptoAllowance(); + cryptoAllowance.setAmount(cryptoApproval.getAmount()); + cryptoAllowance.setOwner(EntityId.of(cryptoApproval.getOwner()).getId()); + cryptoAllowance.setPayerAccountId(payerAccountId); + cryptoAllowance.setSpender(EntityId.of(cryptoApproval.getSpender()).getId()); + cryptoAllowance.setTimestampLower(consensusTimestamp); + entityListener.onCryptoAllowance(cryptoAllowance); + } + + for (var nftApproval : getNftAllowances(recordItem)) { + var approvedForAll = nftApproval.hasApprovedForAll() && nftApproval.getApprovedForAll().getValue(); + NftAllowance nftAllowance = new NftAllowance(); + nftAllowance.setApprovedForAll(approvedForAll); + nftAllowance.setOwner(EntityId.of(nftApproval.getOwner()).getId()); + nftAllowance.setPayerAccountId(payerAccountId); + nftAllowance.setSerialNumbers(nftApproval.getSerialNumbersList()); + nftAllowance.setSpender(EntityId.of(nftApproval.getSpender()).getId()); + nftAllowance.setTokenId(EntityId.of(nftApproval.getTokenId()).getId()); + nftAllowance.setTimestampLower(consensusTimestamp); + entityListener.onNftAllowance(nftAllowance); + } + + for (var tokenApproval : getTokenAllowances(recordItem)) { + TokenAllowance tokenAllowance = new TokenAllowance(); + tokenAllowance.setAmount(tokenApproval.getAmount()); + tokenAllowance.setOwner(EntityId.of(tokenApproval.getOwner()).getId()); + tokenAllowance.setPayerAccountId(payerAccountId); + tokenAllowance.setSpender(EntityId.of(tokenApproval.getSpender()).getId()); + tokenAllowance.setTokenId(EntityId.of(tokenApproval.getTokenId()).getId()); + tokenAllowance.setTimestampLower(consensusTimestamp); + entityListener.onTokenAllowance(tokenAllowance); + } + } + + protected abstract List getCryptoAllowances(RecordItem recordItem); + + protected abstract List getNftAllowances(RecordItem recordItem); + + protected abstract List getTokenAllowances(RecordItem recordItem); +} 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 26784910891..220063ae36d 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 @@ -133,7 +133,7 @@ protected Contract getContract(EntityId contractId, long consensusTimestamp) { Contract contract = contractId.toEntity(); contract.setCreatedTimestamp(consensusTimestamp); contract.setDeleted(false); - contract.setModifiedTimestamp(consensusTimestamp); + contract.setTimestampLower(consensusTimestamp); return contract; } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractEntityCrudTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractEntityCrudTransactionHandler.java index 2ed6db3a531..a67d3901373 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractEntityCrudTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractEntityCrudTransactionHandler.java @@ -68,7 +68,7 @@ protected final void updateEntity(EntityId entityId, RecordItem recordItem) { entity.setDeleted(true); } - entity.setModifiedTimestamp(consensusTimestamp); + entity.setTimestampLower(consensusTimestamp); doUpdateEntity(entity, recordItem); } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoAdjustAllowanceTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoAdjustAllowanceTransactionHandler.java new file mode 100644 index 00000000000..055563e8734 --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoAdjustAllowanceTransactionHandler.java @@ -0,0 +1,59 @@ +package com.hedera.mirror.importer.parser.record.transactionhandler; + +/*- + * ‌ + * 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.CryptoAllowance; +import com.hederahashgraph.api.proto.java.NftAllowance; +import com.hederahashgraph.api.proto.java.TokenAllowance; +import java.util.List; +import javax.inject.Named; + +import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.common.domain.transaction.TransactionType; +import com.hedera.mirror.importer.parser.record.entity.EntityListener; + +@Named +class CryptoAdjustAllowanceTransactionHandler extends AbstractAllowanceTransactionHandler { + + public CryptoAdjustAllowanceTransactionHandler(EntityListener entityListener) { + super(entityListener); + } + + @Override + public TransactionType getType() { + return TransactionType.CRYPTOADJUSTALLOWANCE; + } + + @Override + protected List getCryptoAllowances(RecordItem recordItem) { + return recordItem.getRecord().getCryptoAdjustmentsList(); + } + + @Override + protected List getNftAllowances(RecordItem recordItem) { + return recordItem.getRecord().getNftAdjustmentsList(); + } + + @Override + protected List getTokenAllowances(RecordItem recordItem) { + return recordItem.getRecord().getTokenAdjustmentsList(); + } +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandler.java new file mode 100644 index 00000000000..28fde6ab396 --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandler.java @@ -0,0 +1,67 @@ +package com.hedera.mirror.importer.parser.record.transactionhandler; + +/*- + * ‌ + * 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.CryptoAllowance; +import com.hederahashgraph.api.proto.java.NftAllowance; +import com.hederahashgraph.api.proto.java.TokenAllowance; +import java.util.List; +import javax.inject.Named; + +import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.common.domain.transaction.Transaction; +import com.hedera.mirror.common.domain.transaction.TransactionType; +import com.hedera.mirror.importer.parser.record.entity.EntityListener; + +@Named +class CryptoApproveAllowanceTransactionHandler extends AbstractAllowanceTransactionHandler { + + public CryptoApproveAllowanceTransactionHandler(EntityListener entityListener) { + super(entityListener); + } + + @Override + public TransactionType getType() { + return TransactionType.CRYPTOAPPROVEALLOWANCE; + } + + @Override + public void updateTransaction(Transaction transaction, RecordItem recordItem) { + if (recordItem.isSuccessful()) { + super.updateTransaction(transaction, recordItem); + } + } + + @Override + protected List getCryptoAllowances(RecordItem recordItem) { + return recordItem.getTransactionBody().getCryptoApproveAllowance().getCryptoAllowancesList(); + } + + @Override + protected List getNftAllowances(RecordItem recordItem) { + return recordItem.getTransactionBody().getCryptoApproveAllowance().getNftAllowancesList(); + } + + @Override + protected List getTokenAllowances(RecordItem recordItem) { + return recordItem.getTransactionBody().getCryptoApproveAllowance().getTokenAllowancesList(); + } +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenUpdateTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenUpdateTransactionHandler.java index d5ff05d6c83..50b375e8e0b 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenUpdateTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenUpdateTransactionHandler.java @@ -20,8 +20,6 @@ * ‍ */ -import com.hedera.mirror.common.util.DomainUtils; - import com.hederahashgraph.api.proto.java.NftTransfer; import com.hederahashgraph.api.proto.java.TokenTransferList; import javax.inject.Named; @@ -31,6 +29,7 @@ import com.hedera.mirror.common.domain.token.NftTransferId; 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.parser.record.entity.EntityListener; import com.hedera.mirror.importer.repository.NftRepository; @@ -88,7 +87,7 @@ private void updateTreasury(RecordItem recordItem) { EntityId tokenId = EntityId.of(tokenTransferList.getToken()); nftRepository.updateTreasury(tokenId.getId(), previousTreasury.getId(), newTreasury.getId(), - recordItem.getConsensusTimestamp(), payerAccountId); + recordItem.getConsensusTimestamp(), payerAccountId, nftTransfer.getIsApproval()); } } } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/CryptoAllowanceRepository.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/CryptoAllowanceRepository.java new file mode 100644 index 00000000000..ad2dff6b461 --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/CryptoAllowanceRepository.java @@ -0,0 +1,28 @@ +package com.hedera.mirror.importer.repository; + +/*- + * ‌ + * 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 org.springframework.data.repository.CrudRepository; + +import com.hedera.mirror.common.domain.entity.CryptoAllowance; + +public interface CryptoAllowanceRepository extends CrudRepository { +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/NftAllowanceRepository.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/NftAllowanceRepository.java new file mode 100644 index 00000000000..d075860b4cf --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/NftAllowanceRepository.java @@ -0,0 +1,28 @@ +package com.hedera.mirror.importer.repository; + +/*- + * ‌ + * 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 org.springframework.data.repository.CrudRepository; + +import com.hedera.mirror.common.domain.entity.NftAllowance; + +public interface NftAllowanceRepository extends CrudRepository { +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/NftRepository.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/NftRepository.java index 7973b7f0f87..9a5ac3f9fe8 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/NftRepository.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/NftRepository.java @@ -48,9 +48,10 @@ void transferNftOwnership(@Param("id") NftId nftId, @Param("accountId") EntityId " where token_id = ?1 and account_id = ?2" + " returning serial_number) " + "insert into nft_transfer " + - "(token_id, sender_account_id, receiver_account_id, consensus_timestamp, payer_account_id, serial_number)" + - " select ?1, ?2, ?3, ?4, ?5, nft_updated.serial_number " + + "(token_id, sender_account_id, receiver_account_id, consensus_timestamp, payer_account_id, serial_number," + + "is_approval)" + + " select ?1, ?2, ?3, ?4, ?5, nft_updated.serial_number, ?6 " + "from nft_updated", nativeQuery = true) void updateTreasury(long tokenId, long previousAccountId, long newAccountId, long consensusTimestamp, - long payerAccountId); + long payerAccountId, boolean isApproval); } diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/TokenAllowanceRepository.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/TokenAllowanceRepository.java new file mode 100644 index 00000000000..226e8c68b3a --- /dev/null +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/TokenAllowanceRepository.java @@ -0,0 +1,27 @@ +package com.hedera.mirror.importer.repository; + +/*- + * ‌ + * 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.TokenAllowance; +import org.springframework.data.repository.CrudRepository; + +public interface TokenAllowanceRepository extends CrudRepository { +} diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/upsert/TokenDissociateTransferUpsertQueryGenerator.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/upsert/TokenDissociateTransferUpsertQueryGenerator.java index 708ab4e5199..78feeb6d2c7 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/upsert/TokenDissociateTransferUpsertQueryGenerator.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/repository/upsert/TokenDissociateTransferUpsertQueryGenerator.java @@ -36,8 +36,8 @@ public class TokenDissociateTransferUpsertQueryGenerator implements UpsertQueryG ".payer_account_id" + "), updated_nft as (" + " insert into nft_transfer (consensus_timestamp, sender_account_id, serial_number, token_id," + - "payer_account_id)" + - " select modified_timestamp, account_id, serial_number, token_id, payer_account_id" + + "payer_account_id, is_approval)" + + " select modified_timestamp, account_id, serial_number, token_id, payer_account_id, false" + " from dissociated_nft" + " returning token_id" + ") " + diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.54.3__hip336_approval_allowance.sql b/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.54.3__hip336_approval_allowance.sql new file mode 100644 index 00000000000..c7c3ebbe93a --- /dev/null +++ b/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.54.3__hip336_approval_allowance.sql @@ -0,0 +1,83 @@ +-- HIP-336 Approval and Allowance API + +-- transfers +alter table if exists crypto_transfer + add column if not exists is_approval boolean null; + +alter table if exists nft_transfer + add column if not exists is_approval boolean null; + +alter table if exists non_fee_transfer + add column if not exists is_approval boolean null; + +alter table if exists token_transfer + add column if not exists is_approval boolean null; + + +-- crypto_allowance +create table if not exists crypto_allowance +( + amount bigint not null, + owner bigint not null, + payer_account_id bigint not null, + spender bigint not null, + timestamp_range int8range not null, + primary key (owner, spender) +); +comment on table crypto_allowance is 'Hbar allowances delegated by payer to spender'; + +create table if not exists crypto_allowance_history +( + like crypto_allowance including defaults, + primary key (owner, spender, timestamp_range) +); +comment on table crypto_allowance_history is 'History of hbar allowances delegated by payer to spender'; + +create index if not exists crypto_allowance_history__timestamp_range on crypto_allowance_history using gist (timestamp_range); + + +-- nft_allowance +create table if not exists nft_allowance +( + approved_for_all boolean not null, + owner bigint not null, + payer_account_id bigint not null, + serial_numbers bigint[] not null, + spender bigint not null, + timestamp_range int8range not null, + token_id bigint not null, + primary key (owner, spender, token_id) +); +comment on table nft_allowance is 'NFT allowances delegated by payer to spender'; + +create table if not exists nft_allowance_history +( + like nft_allowance including defaults, + primary key (owner, spender, token_id, timestamp_range) +); +comment on table nft_allowance_history is 'History of NFT allowances delegated by payer to spender'; + +create index if not exists nft_allowance_history__timestamp_range on nft_allowance_history using gist (timestamp_range); + + +-- token_allowance +create table if not exists token_allowance +( + amount bigint not null, + owner bigint not null, + payer_account_id bigint not null, + spender bigint not null, + timestamp_range int8range not null, + token_id bigint not null, + primary key (owner, spender, token_id) +); +comment on table token_allowance is 'Token allowances delegated by payer to spender'; + +create table if not exists token_allowance_history +( + like token_allowance including defaults, + primary key (owner, spender, token_id, timestamp_range) +); +comment on table token_allowance_history is 'History of token allowances delegated by payer to spender'; + +create index if not exists token_allowance_history__timestamp_range on token_allowance_history using gist (timestamp_range); 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 5755f3d9ad6..be6e8b34707 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 @@ -111,17 +111,17 @@ comment on table contract_history is 'Contract entity historical state'; -- contract_log create table if not exists contract_log ( - bloom bytea not null, - consensus_timestamp bigint not null, - contract_id bigint not null, - data bytea not null, - index int not null, - payer_account_id bigint not null, - root_contract_id bigint null, - topic0 bytea null, - topic1 bytea null, - topic2 bytea null, - topic3 bytea null + bloom bytea not null, + consensus_timestamp bigint not null, + contract_id bigint not null, + data bytea not null, + index int not null, + payer_account_id bigint not null, + root_contract_id bigint null, + topic0 bytea null, + topic1 bytea null, + topic2 bytea null, + topic3 bytea null ); comment on table contract_log is 'Contract execution result logs'; @@ -154,13 +154,30 @@ create table if not exists contract_state_change ); comment on table contract_result is 'Contract execution state changes'; +create table if not exists crypto_allowance +( + amount bigint not null, + owner bigint not null, + payer_account_id bigint not null, + spender bigint not null, + timestamp_range int8range not null +); +comment on table crypto_allowance is 'Hbar allowances delegated by payer to spender'; + +create table if not exists crypto_allowance_history +( + like crypto_allowance including defaults +); +comment on table crypto_allowance_history is 'History of hbar allowances delegated by payer to spender'; + -- crypto_transfer create table if not exists crypto_transfer ( - amount bigint not null, - consensus_timestamp bigint not null, - entity_id bigint not null, - payer_account_id bigint not null + amount bigint not null, + consensus_timestamp bigint not null, + is_approval boolean null, + entity_id bigint not null, + payer_account_id bigint not null ); comment on table crypto_transfer is 'Crypto account Hbar transfers'; @@ -260,25 +277,45 @@ create table if not exists nft ); comment on table nft is 'Non-Fungible Tokens (NFTs) minted on network'; +create table if not exists nft_allowance +( + approved_for_all boolean not null, + owner bigint not null, + payer_account_id bigint not null, + serial_numbers bigint[] not null, + spender bigint not null, + timestamp_range int8range not null, + token_id bigint not null +); +comment on table nft_allowance is 'NFT allowances delegated by payer to spender'; + +create table if not exists nft_allowance_history +( + like nft_allowance including defaults +); +comment on table nft_allowance_history is 'History of NFT allowances delegated by payer to spender'; + -- nft_transfer create table if not exists nft_transfer ( - consensus_timestamp bigint not null, - payer_account_id bigint not null, + consensus_timestamp bigint not null, + is_approval boolean null, + payer_account_id bigint not null, receiver_account_id bigint, sender_account_id bigint, - serial_number bigint not null, - token_id bigint not null + serial_number bigint not null, + token_id bigint not null ); comment on table nft_transfer is 'Crypto account nft transfers'; -- non_fee_transfer create table if not exists non_fee_transfer ( - amount bigint not null, - consensus_timestamp bigint not null, - entity_id bigint not null, - payer_account_id bigint not null + amount bigint not null, + consensus_timestamp bigint not null, + is_approval boolean null, + entity_id bigint not null, + payer_account_id bigint not null ); comment on table non_fee_transfer is 'Crypto account non fee Hbar transfers'; @@ -357,6 +394,23 @@ create table if not exists token_account ); comment on table token is 'Token account entity'; +create table if not exists token_allowance +( + amount bigint not null, + owner bigint not null, + payer_account_id bigint not null, + spender bigint not null, + timestamp_range int8range not null, + token_id bigint not null +); +comment on table token_allowance is 'Token allowances delegated by payer to spender'; + +create table if not exists token_allowance_history +( + like token_allowance including defaults +); +comment on table token_allowance_history is 'History of token allowances delegated by payer to spender'; + --- token_balance create table if not exists token_balance ( @@ -370,28 +424,29 @@ comment on table token_balance is 'Crypto account token balances'; --- token_transfer create table if not exists token_transfer ( - account_id bigint not null, - amount bigint not null, - consensus_timestamp bigint not null, - payer_account_id bigint not null, - token_id bigint not null + account_id bigint not null, + amount bigint not null, + consensus_timestamp bigint not null, + is_approval boolean null, + payer_account_id bigint not null, + token_id bigint not null ); comment on table token_transfer is 'Crypto account token transfers'; -- topic_message create table if not exists topic_message ( - chunk_num integer, - chunk_total integer, - consensus_timestamp bigint not null, - initial_transaction_id bytea, - message bytea not null, - payer_account_id bigint not null, - running_hash bytea not null, - running_hash_version smallint not null, - sequence_number bigint not null, - topic_id bigint not null, - valid_start_timestamp bigint + chunk_num integer, + chunk_total integer, + consensus_timestamp bigint not null, + initial_transaction_id bytea, + message bytea not null, + payer_account_id bigint not null, + running_hash bytea not null, + running_hash_version smallint not null, + sequence_number bigint not null, + topic_id bigint not null, + valid_start_timestamp bigint ); comment on table topic_message is 'Topic entity sequenced messages'; diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.1__distribution.sql b/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.1__distribution.sql index b422bb1ee50..4ab170ee834 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.1__distribution.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.0.1__distribution.sql @@ -21,6 +21,10 @@ select create_distributed_table('contract_result', 'payer_account_id', colocate_ select create_distributed_table('contract_state_change', 'payer_account_id', colocate_with => 'entity'); +select create_distributed_table('crypto_allowance', 'owner', colocate_with => 'entity'); + +select create_distributed_table('crypto_allowance_history', 'owner', colocate_with => 'crypto_allowance'); + select create_distributed_table('custom_fee', 'token_id', colocate_with => 'entity'); select create_distributed_table('entity_history', 'id', colocate_with => 'entity'); @@ -31,6 +35,10 @@ select create_distributed_table('file_data', 'entity_id', colocate_with => 'enti select create_distributed_table('nft', 'token_id', colocate_with => 'entity'); +select create_distributed_table('nft_allowance', 'owner', colocate_with => 'entity'); + +select create_distributed_table('nft_allowance_history', 'owner', colocate_with => 'nft_allowance'); + select create_distributed_table('record_file', 'node_account_id', colocate_with => 'entity'); select create_distributed_table('schedule', 'schedule_id', colocate_with => 'entity'); @@ -39,6 +47,10 @@ select create_distributed_table('token', 'token_id', colocate_with => 'entity'); select create_distributed_table('token_account', 'token_id', colocate_with => 'entity'); +select create_distributed_table('token_allowance', 'owner', colocate_with => 'entity'); + +select create_distributed_table('token_allowance_history', 'owner', colocate_with => 'token_allowance'); + select create_distributed_table('token_balance', 'account_id', colocate_with => 'entity'); select create_distributed_table('topic_message', 'topic_id', colocate_with => 'entity'); 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 9019bf92c4e..dc9a9a39d84 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 @@ -65,6 +65,13 @@ create index if not exists contract_result__id_payer_timestamp alter table if exists contract_state_change add constraint contract_state_change__pk primary key (consensus_timestamp, contract_id, slot, payer_account_id); +-- crypto_allowance +alter table if exists crypto_allowance + add constraint crypto_allowance__pk primary key (owner, spender); +alter table if exists crypto_allowance_history + add constraint crypto_allowance_history__pk primary key (owner, spender, timestamp_range); +create index if not exists crypto_allowance_history__timestamp_range on crypto_allowance_history using gist (timestamp_range); + -- crypto_transfer create index if not exists crypto_transfer__consensus_timestamp on crypto_transfer (consensus_timestamp); @@ -119,10 +126,17 @@ alter table nft add constraint nft__pk primary key (token_id, serial_number); create index if not exists nft__account_token on nft (account_id, token_id); +-- nft_allowance +alter table if exists nft_allowance + add constraint nft_allowance__pk primary key (owner, spender, token_id); +alter table if exists nft_allowance_history + add constraint nft_allowance_history__pk primary key (owner, spender, token_id, timestamp_range); +create index if not exists nft_allowance_history__timestamp_range on nft_allowance_history using gist (timestamp_range); + -- nft_transfer create index if not exists nft_transfer__timestamp on nft_transfer (consensus_timestamp desc); create unique index if not exists nft_transfer__token_id_serial_num_timestamp - on nft_transfer(token_id desc, serial_number desc, consensus_timestamp desc); + on nft_transfer (token_id desc, serial_number desc, consensus_timestamp desc); -- non_fee_transfer create index if not exists non_fee_transfer__consensus_timestamp @@ -152,6 +166,13 @@ alter table token alter table token_account add constraint token_account__pk primary key (account_id, token_id, modified_timestamp); +-- token_allowance +alter table if exists token_allowance + add constraint token_allowance__pk primary key (owner, spender, token_id); +alter table if exists token_allowance_history + add constraint token_allowance_history__pk primary key (owner, spender, token_id, timestamp_range); +create index if not exists token_allowance_history__timestamp_range on token_allowance_history using gist (timestamp_range); + -- token_balance alter table token_balance add constraint token_balance__pk primary key (consensus_timestamp, account_id, token_id); diff --git a/hedera-mirror-importer/src/main/resources/db/scripts/v2/csvBackupTables.sql b/hedera-mirror-importer/src/main/resources/db/scripts/v2/csvBackupTables.sql index 9212eac9ffe..a197a19d146 100644 --- a/hedera-mirror-importer/src/main/resources/db/scripts/v2/csvBackupTables.sql +++ b/hedera-mirror-importer/src/main/resources/db/scripts/v2/csvBackupTables.sql @@ -24,6 +24,10 @@ \copy contract_state_change to contract_state_change.csv delimiter ',' csv; +\copy crypto_allowance to crypto_allowance.csv delimiter ',' csv; + +\copy crypto_allowance_history to crypto_allowance_history.csv delimiter ',' csv; + \copy crypto_transfer to crypto_transfer.csv delimiter ',' csv; \copy custom_fee to custom_fee.csv delimiter ',' csv; @@ -40,6 +44,10 @@ \copy nft to nft.csv delimiter ',' csv; +\copy nft_allowance to nft_allowance.csv delimiter ',' csv; + +\copy nft_allowance_history to nft_allowance_history.csv delimiter ',' csv; + \copy nft_transfer to nft_transfer.csv delimiter ',' csv; \copy non_fee_transfer to non_fee_transfer.csv delimiter ',' csv; @@ -52,6 +60,10 @@ \copy token_account to token_account.csv delimiter ',' csv; +\copy token_allowance to token_allowance.csv delimiter ',' csv; + +\copy token_allowance_history to token_allowance_history.csv delimiter ',' csv; + \copy token_balance to token_balance.csv delimiter ',' csv; \copy token_transfer to token_transfer.csv delimiter ',' csv; 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 7eace117517..f671efd66cd 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 @@ -24,7 +24,11 @@ \copy contract_state_change (consensus_timestamp, contract_id, payer_account_id, slot, value_read, value_written) from contract_state_change.csv csv; -\copy crypto_transfer (entity_id, consensus_timestamp, amount) from crypto_transfer.csv csv; +\copy crypto_allowance (amount, payer_account_id, spender, timestamp_range) from crypto_allowance.csv csv; + +\copy crypto_allowance_history (amount, payer_account_id, spender, timestamp_range) from crypto_allowance_history.csv csv; + +\copy crypto_transfer (entity_id, consensus_timestamp, amount, is_approval) from crypto_transfer.csv csv; \copy custom_fee (amount, amount_denominator, collector_account_id, created_timestamp, denominating_token_id, maximum_amount, minimum_amount, token_id) from custom_fee.csv csv; @@ -40,9 +44,13 @@ \copy nft (account_id, created_timestamp, deleted, modified_timestamp, metadata, serial_number, token_id) from nft.csv csv; -\copy nft_transfer (consensus_timestamp, receiver_account_id, sender_account_id, serial_number, token_id) from nft_transfer.csv csv +\copy nft_allowance (approved_for_all, payer_account_id, serial_numbers, spender, timestamp_range, token_id) from nft_allowance.csv csv; + +\copy nft_allowance_history (approved_for_all, payer_account_id, serial_numbers, spender, timestamp_range, token_id) from nft_allowance_history.csv csv; -\copy non_fee_transfer (entity_id, consensus_timestamp, amount) from non_fee_transfer.csv csv; +\copy nft_transfer (consensus_timestamp, receiver_account_id, sender_account_id, serial_number, token_id, is_approval) from nft_transfer.csv csv + +\copy non_fee_transfer (entity_id, consensus_timestamp, amount, is_approval) from non_fee_transfer.csv csv; \copy record_file (name, load_start, load_end, hash, prev_hash, consensus_start, consensus_end, node_account_id, count, digest_algorithm, hapi_version_major, hapi_version_minor, hapi_version_patch, version, file_hash, bytes, index) from record_file.csv csv; @@ -52,9 +60,13 @@ \copy token_account (account_id, associated, created_timestamp, freeze_status, kyc_status, modified_timestamp, token_id, automatic_association) from token_account.csv csv; +\copy token_allowance (amount, payer_account_id, spender, timestamp_range, token_id) from token_allowance.csv csv; + +\copy token_allowance_history (amount, payer_account_id, spender, timestamp_range, token_id) from token_allowance_history.csv csv; + \copy token_balance (consensus_timestamp, account_id, balance, token_id) from token_balance.csv csv; -\copy token_transfer (token_id, account_id, consensus_timestamp, amount) from token_transfer.csv csv; +\copy token_transfer (token_id, account_id, consensus_timestamp, amount, is_approval) from token_transfer.csv csv; \copy topic_message (consensus_timestamp, topic_id, message, running_hash, sequence_number, running_hash_version, chunk_num, chunk_total, payer_account_id, valid_start_timestamp, initial_transaction_id) from topic_message.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 4e93d4584a3..41df09ba443 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 @@ -22,13 +22,17 @@ import com.google.common.collect.Range; import com.vladmihalcea.hibernate.type.range.guava.PostgreSQLGuavaRangeType; +import java.sql.SQLException; import java.time.Instant; +import java.util.Arrays; import java.util.Collection; +import java.util.List; import javax.annotation.Resource; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; +import org.postgresql.jdbc.PgArray; import org.postgresql.util.PGobject; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cache.CacheManager; @@ -68,6 +72,14 @@ protected static RowMapper rowMapper(Class entityClass) { source -> PostgreSQLGuavaRangeType.longRange(source.getValue())); defaultConversionService.addConverter(Long.class, EntityId.class, id -> EntityIdEndec.decode(id, EntityType.ACCOUNT)); + defaultConversionService.addConverter(PgArray.class, List.class, + array -> { + try { + return Arrays.asList((Object[]) array.getArray()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); DataClassRowMapper dataClassRowMapper = new DataClassRowMapper<>(entityClass); dataClassRowMapper.setConversionService(defaultConversionService); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/CleanupEntityMigrationTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/CleanupEntityMigrationTest.java index 4f7139b820f..615a3ea4b12 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/CleanupEntityMigrationTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/migration/CleanupEntityMigrationTest.java @@ -38,15 +38,15 @@ import org.springframework.jdbc.core.JdbcOperations; import org.springframework.test.context.TestPropertySource; +import com.hedera.mirror.common.domain.entity.Entity; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.EntityIdEndec; import com.hedera.mirror.common.domain.entity.EntityType; +import com.hedera.mirror.common.domain.transaction.Transaction; +import com.hedera.mirror.common.domain.transaction.TransactionType; import com.hedera.mirror.importer.EnabledIfV1; import com.hedera.mirror.importer.IntegrationTest; import com.hedera.mirror.importer.MirrorProperties; -import com.hedera.mirror.common.domain.entity.Entity; -import com.hedera.mirror.common.domain.transaction.Transaction; -import com.hedera.mirror.common.domain.transaction.TransactionType; import com.hedera.mirror.importer.repository.EntityRepository; import com.hedera.mirror.importer.repository.TransactionRepository; @@ -127,7 +127,7 @@ void verifyEntityMigrationCreationTransactions() throws Exception { assertAll( () -> assertThat(entity).isPresent().get() .returns(consensusTimestamp, Entity::getCreatedTimestamp) - .returns(consensusTimestamp, Entity::getModifiedTimestamp) + .returns(consensusTimestamp, Entity::getTimestampLower) .returns("", Entity::getMemo) ); } @@ -148,7 +148,7 @@ private Optional retrieveEntity(Long id) { entity.setId(rs.getLong("id")); entity.setKey(rs.getBytes("key")); entity.setMemo(rs.getString("memo")); - entity.setModifiedTimestamp(rs.getLong("modified_timestamp")); + entity.setTimestampLower(rs.getLong("modified_timestamp")); entity.setNum(rs.getLong("num")); entity.setPublicKey(rs.getString("public_key")); entity.setRealm(rs.getLong("realm")); @@ -211,7 +211,7 @@ void verifyEntityMigrationCreationTransactionsWithFailures() throws Exception { assertAll( () -> assertThat(entity).isPresent().get() .returns(consensusTimestamp, Entity::getCreatedTimestamp) - .returns(consensusTimestamp, Entity::getModifiedTimestamp) + .returns(consensusTimestamp, Entity::getTimestampLower) .returns("", Entity::getMemo) ); } @@ -265,7 +265,7 @@ void verifyEntityMigrationWithSingleUpdate() throws Exception { assertAll( () -> assertThat(entity).isPresent().get() .returns(createdTimestamp, Entity::getCreatedTimestamp) - .returns(modifiedTimestamp, Entity::getModifiedTimestamp) + .returns(modifiedTimestamp, Entity::getTimestampLower) ); } } @@ -330,7 +330,7 @@ void verifyEntityMigrationWithMultipleUpdates() throws Exception { assertAll( () -> assertThat(entity).isPresent().get() .returns(createdTimestamp, Entity::getCreatedTimestamp) - .returns(modifiedTimestamp, Entity::getModifiedTimestamp) + .returns(modifiedTimestamp, Entity::getTimestampLower) ); } } diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/batch/BatchUpserterTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/batch/BatchUpserterTest.java index a38dd0626d0..90e7dbb94ca 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/batch/BatchUpserterTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/batch/BatchUpserterTest.java @@ -41,9 +41,12 @@ import com.hedera.mirror.common.domain.DomainBuilder; import com.hedera.mirror.common.domain.contract.Contract; +import com.hedera.mirror.common.domain.entity.CryptoAllowance; import com.hedera.mirror.common.domain.entity.Entity; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.EntityType; +import com.hedera.mirror.common.domain.entity.NftAllowance; +import com.hedera.mirror.common.domain.entity.TokenAllowance; import com.hedera.mirror.common.domain.schedule.Schedule; import com.hedera.mirror.common.domain.token.Nft; import com.hedera.mirror.common.domain.token.NftId; @@ -60,11 +63,14 @@ import com.hedera.mirror.common.domain.token.TokenTypeEnum; import com.hedera.mirror.importer.IntegrationTest; import com.hedera.mirror.importer.repository.ContractRepository; +import com.hedera.mirror.importer.repository.CryptoAllowanceRepository; import com.hedera.mirror.importer.repository.EntityRepository; +import com.hedera.mirror.importer.repository.NftAllowanceRepository; import com.hedera.mirror.importer.repository.NftRepository; import com.hedera.mirror.importer.repository.NftTransferRepository; import com.hedera.mirror.importer.repository.ScheduleRepository; import com.hedera.mirror.importer.repository.TokenAccountRepository; +import com.hedera.mirror.importer.repository.TokenAllowanceRepository; import com.hedera.mirror.importer.repository.TokenRepository; import com.hedera.mirror.importer.repository.TokenTransferRepository; @@ -80,6 +86,9 @@ class BatchUpserterTest extends IntegrationTest { @Resource private ContractRepository contractRepository; + @Resource + private CryptoAllowanceRepository cryptoAllowanceRepository; + @Resource private DomainBuilder domainBuilder; @@ -92,6 +101,9 @@ class BatchUpserterTest extends IntegrationTest { @Resource private NftRepository nftRepository; + @Resource + private NftAllowanceRepository nftAllowanceRepository; + @Resource private NftTransferRepository nftTransferRepository; @@ -104,6 +116,9 @@ class BatchUpserterTest extends IntegrationTest { @Resource private TokenAccountRepository tokenAccountRepository; + @Resource + private TokenAllowanceRepository tokenAllowanceRepository; + @Resource(name = TOKEN_DISSOCIATE_BATCH_PERSISTER) private BatchPersister tokenDissociateTransferBatchUpserter; @@ -143,6 +158,16 @@ void contract() { .containsExactlyInAnyOrder(contract1.getId(), contract2.getId(), contract3.getId()); } + @Test + void cryptoAllowance() { + CryptoAllowance cryptoAllowance1 = domainBuilder.cryptoAllowance().get(); + CryptoAllowance cryptoAllowance2 = domainBuilder.cryptoAllowance().get(); + CryptoAllowance cryptoAllowance3 = domainBuilder.cryptoAllowance().get(); + var cryptoAllowances = List.of(cryptoAllowance1, cryptoAllowance2, cryptoAllowance3); + persist(batchPersister, cryptoAllowances); + assertThat(cryptoAllowanceRepository.findAll()).containsExactlyInAnyOrderElementsOf(cryptoAllowances); + } + @Test void entityInsertOnly() { var entities = new ArrayList(); @@ -644,6 +669,26 @@ void nftInsertTransferBurnWipe() { .containsExactlyInAnyOrder(false, true, false, true); } + @Test + void nftAllowance() { + NftAllowance nftAllowance1 = domainBuilder.nftAllowance().get(); + NftAllowance nftAllowance2 = domainBuilder.nftAllowance().get(); + NftAllowance nftAllowance3 = domainBuilder.nftAllowance().get(); + var nftAllowance = List.of(nftAllowance1, nftAllowance2, nftAllowance3); + persist(batchPersister, nftAllowance); + assertThat(nftAllowanceRepository.findAll()).containsExactlyInAnyOrderElementsOf(nftAllowance); + } + + @Test + void tokenAllowance() { + TokenAllowance tokenAllowance1 = domainBuilder.tokenAllowance().get(); + TokenAllowance tokenAllowance2 = domainBuilder.tokenAllowance().get(); + TokenAllowance tokenAllowance3 = domainBuilder.tokenAllowance().get(); + var tokenAllowance = List.of(tokenAllowance1, tokenAllowance2, tokenAllowance3); + persist(batchPersister, tokenAllowance); + assertThat(tokenAllowanceRepository.findAll()).containsExactlyInAnyOrderElementsOf(tokenAllowance); + } + @Test void tokenDissociateTransfer() { // given @@ -669,11 +714,13 @@ void tokenDissociateTransfer() { TokenTransfer fungibleTokenTransfer = domainBuilder.tokenTransfer().customize(t -> t .amount(-10) .id(new TokenTransfer.Id(consensusTimestamp, ftId, accountId)) + .isApproval(false) .payerAccountId(payerId) .tokenDissociate(true)).get(); TokenTransfer nonFungibleTokenTransfer = domainBuilder.tokenTransfer().customize(t -> t .amount(-2) .id(new TokenTransfer.Id(consensusTimestamp, tokenId1, accountId)) + .isApproval(false) .payerAccountId(payerId) .tokenDissociate(true)).get(); List tokenTransfers = List.of(fungibleTokenTransfer, nonFungibleTokenTransfer); @@ -719,7 +766,7 @@ private Entity getEntity(long id, Long createdTimestamp, long modifiedTimestamp, Entity entity = new Entity(); entity.setId(id); entity.setCreatedTimestamp(createdTimestamp); - entity.setModifiedTimestamp(modifiedTimestamp); + entity.setTimestampLower(modifiedTimestamp); entity.setNum(id); entity.setRealm(0L); entity.setShard(0L); @@ -811,6 +858,7 @@ private NftTransfer getNftTransfer(EntityId tokenId, EntityId senderAccountId, l long consensusTimestamp) { NftTransfer nftTransfer = new NftTransfer(); nftTransfer.setId(new NftTransferId(consensusTimestamp, serialNumber, tokenId)); + nftTransfer.setIsApproval(false); nftTransfer.setSenderAccountId(senderAccountId); return nftTransfer; } 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 f32ac892735..34d5a480804 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 @@ -24,6 +24,7 @@ import static com.hedera.mirror.common.domain.DomainBuilder.KEY_LENGTH_ED25519; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; +import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; import com.google.protobuf.BytesValue; import com.google.protobuf.GeneratedMessageV3; @@ -36,10 +37,15 @@ 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.ContractStateChange; import com.hederahashgraph.api.proto.java.ContractUpdateTransactionBody; +import com.hederahashgraph.api.proto.java.CryptoAdjustAllowanceTransactionBody; +import com.hederahashgraph.api.proto.java.CryptoAllowance; +import com.hederahashgraph.api.proto.java.CryptoApproveAllowanceTransactionBody; import com.hederahashgraph.api.proto.java.Duration; import com.hederahashgraph.api.proto.java.FileID; import com.hederahashgraph.api.proto.java.Key; +import com.hederahashgraph.api.proto.java.NftAllowance; import com.hederahashgraph.api.proto.java.RealmID; import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import com.hederahashgraph.api.proto.java.ShardID; @@ -48,6 +54,7 @@ import com.hederahashgraph.api.proto.java.SignedTransaction; import com.hederahashgraph.api.proto.java.StorageChange; import com.hederahashgraph.api.proto.java.Timestamp; +import com.hederahashgraph.api.proto.java.TokenAllowance; import com.hederahashgraph.api.proto.java.TokenID; import com.hederahashgraph.api.proto.java.TokenMintTransactionBody; import com.hederahashgraph.api.proto.java.TokenType; @@ -58,6 +65,7 @@ import com.hederahashgraph.api.proto.java.TransferList; import java.security.SecureRandom; import java.time.Instant; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import javax.inject.Named; @@ -160,33 +168,10 @@ public ContractFunctionResult.Builder contractFunctionResult(ContractID contract .addTopic(bytes(32)) .addTopic(bytes(32)) .build()) - .addStateChanges(com.hederahashgraph.api.proto.java.ContractStateChange.newBuilder() + .addStateChanges(ContractStateChange.newBuilder() .setContractID(contractId) - .addStorageChanges(StorageChange.newBuilder() - .setSlot(ByteString - .copyFromUtf8("0x000000000000000000")) - .setValueRead(ByteString - .copyFromUtf8( - "0xaf846d22986843e3d25981b94ce181adc556b334ccfdd8225762d7f709841df0")) - .build()) - .addStorageChanges(StorageChange.newBuilder() - .setSlot(ByteString - .copyFromUtf8("0x000000000000000001")) - .setValueRead(ByteString - .copyFromUtf8( - "0xaf846d22986843e3d25981b94ce181adc556b334ccfdd8225762d7f709841df0")) - .setValueWritten(BytesValue.of(ByteString - .copyFromUtf8( - "0x000000000000000000000000000000000000000000c2a8c408d0e29d623347c5"))) - .build()) - .addStorageChanges(StorageChange.newBuilder() - .setSlot(ByteString - .copyFromUtf8("0x00000000000000002")) - .setValueRead(ByteString - .copyFromUtf8( - "0xaf846d22986843e3d25981b94ce181adc556b334ccfdd8225762d7f709841df0")) - .setValueWritten(BytesValue.of(ByteString.copyFromUtf8("0"))) - .build()) + .addStorageChanges(storageChange()) + .addStorageChanges(storageChange().setValueWritten(BytesValue.of(ByteString.EMPTY))) .build()); } @@ -204,6 +189,71 @@ public Builder contractUpdate() { .receipt(r -> r.setContractID(contractId)); } + public Builder cryptoAdjustAllowance() { + var cryptoAllowance = CryptoAllowance.newBuilder() + .setAmount(-10L) + .setOwner(accountId()) + .setSpender(accountId()); + var nftAllowance1 = NftAllowance.newBuilder() + .setOwner(accountId()) + .setSpender(accountId()) + .setTokenId(tokenId()) + .addSerialNumbers(-1L) + .addSerialNumbers(2L); + var nftAllowance2 = NftAllowance.newBuilder() + .setApprovedForAll(BoolValue.of(true)) + .setOwner(accountId()) + .setSpender(accountId()) + .setTokenId(tokenId()); + var tokenAllowance = TokenAllowance.newBuilder() + .setAmount(-10L) + .setOwner(accountId()) + .setSpender(accountId()) + .setTokenId(tokenId()); + var builder = CryptoAdjustAllowanceTransactionBody.newBuilder() + .addCryptoAllowances(cryptoAllowance) + .addNftAllowances(nftAllowance1) + .addNftAllowances(nftAllowance2) + .addTokenAllowances(tokenAllowance); + return new Builder<>(TransactionType.CRYPTOADJUSTALLOWANCE, builder) + .record(r -> r.addCryptoAdjustments(cryptoAllowance.setAmount(5L)) + .addNftAdjustments(nftAllowance1.clearSerialNumbers().addAllSerialNumbers(List.of(2L, 3L))) + .addNftAdjustments(nftAllowance2) + .addTokenAdjustments(tokenAllowance.setAmount(5L))); + } + + public Builder cryptoApproveAllowance() { + var builder = CryptoApproveAllowanceTransactionBody.newBuilder() + .addCryptoAllowances(CryptoAllowance.newBuilder() + .setAmount(10L) + .setOwner(accountId()) + .setSpender(accountId())) + .addNftAllowances(NftAllowance.newBuilder() + .setOwner(accountId()) + .addSerialNumbers(1L) + .addSerialNumbers(2L) + .setSpender(accountId()) + .setTokenId(tokenId())) + .addNftAllowances(NftAllowance.newBuilder() + .setApprovedForAll(BoolValue.of(true)) + .setOwner(accountId()) + .setSpender(accountId()) + .setTokenId(tokenId())) + .addTokenAllowances(TokenAllowance.newBuilder() + .setAmount(10L) + .setOwner(accountId()) + .setSpender(accountId()) + .setTokenId(tokenId())); + return new Builder<>(TransactionType.CRYPTOAPPROVEALLOWANCE, builder); + } + + private StorageChange.Builder storageChange() { + return StorageChange.newBuilder() + .setSlot(bytes(32)) + .setValueRead(bytes(32)) + .setValueWritten(BytesValue.of(bytes(32))); + } + public Builder tokenMint(TokenType tokenType) { TokenMintTransactionBody.Builder transactionBody = TokenMintTransactionBody.newBuilder().setToken(tokenId()); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/AbstractEntityRecordItemListenerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/AbstractEntityRecordItemListenerTest.java index 3b691e133f9..5a698ec7672 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/AbstractEntityRecordItemListenerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/AbstractEntityRecordItemListenerTest.java @@ -63,6 +63,7 @@ import com.hedera.mirror.common.util.DomainUtils; import com.hedera.mirror.importer.IntegrationTest; import com.hedera.mirror.importer.domain.StreamFilename; +import com.hedera.mirror.importer.parser.domain.RecordItemBuilder; import com.hedera.mirror.importer.parser.record.RecordStreamFileListener; import com.hedera.mirror.importer.repository.ContractRepository; import com.hedera.mirror.importer.repository.ContractResultRepository; @@ -115,6 +116,9 @@ public abstract class AbstractEntityRecordItemListenerTest extends IntegrationTe @Resource protected EntityProperties entityProperties; + @Resource + protected RecordItemBuilder recordItemBuilder; + @Resource protected RecordStreamFileListener recordStreamFileListener; @@ -351,7 +355,7 @@ protected Entity createEntity(EntityId entityId, Key adminKey, EntityId autoRene entity.setDeleted(deleted); entity.setExpirationTimestamp(expiryTimeNs); entity.setMemo(memo); - entity.setModifiedTimestamp(modifiedTimestamp); + entity.setTimestampLower(modifiedTimestamp); entity.setKey(adminKeyBytes); entity.setSubmitKey(submitKeyBytes); 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 9ec2d26e85b..55b432faa6c 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 @@ -45,6 +45,7 @@ import com.hederahashgraph.api.proto.java.TransactionReceipt; import com.hederahashgraph.api.proto.java.TransactionRecord; import java.nio.ByteBuffer; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -70,7 +71,6 @@ 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; @@ -91,9 +91,6 @@ class EntityRecordItemListenerContractTest extends AbstractEntityRecordItemListe @Resource private ContractStateChangeRepository contractStateChangeRepository; - @Resource - private RecordItemBuilder recordItemBuilder; - @BeforeEach void before() { contractIds = new HashMap<>(); @@ -157,6 +154,7 @@ void contractCreateWithEvmAddressAndChildCreate() { var parentEvmAddress = domainBuilder.create2EvmAddress(); var parentRecordItem = recordItemBuilder.contractCreate() .record(r -> r.setContractCreateResult(r.getContractCreateResultBuilder() + .clearStateChanges() .setEvmAddress(BytesValue.of(DomainUtils.fromBytes(parentEvmAddress))) )) .hapiVersion(HAPI_VERSION_0_23_0) @@ -424,7 +422,7 @@ void contractDeleteToExisting(ContractIdType contractIdType) { () -> assertThat(dbContractEntity) .isNotNull() .returns(true, Contract::getDeleted) - .returns(recordItem.getConsensusTimestamp(), Contract::getModifiedTimestamp) + .returns(recordItem.getConsensusTimestamp(), Contract::getTimestampLower) .returns(EntityId.of(PAYER), Contract::getObtainerId) .usingRecursiveComparison() .ignoringFields("deleted", "obtainerId", "timestampRange") @@ -455,7 +453,7 @@ void contractDeleteToNew(ContractIdType contractIdType) { () -> assertThat(contract) .isNotNull() .returns(true, Contract::getDeleted) - .returns(recordItem.getConsensusTimestamp(), Contract::getModifiedTimestamp) + .returns(recordItem.getConsensusTimestamp(), Contract::getTimestampLower) .returns(null, Contract::getAutoRenewPeriod) .returns(null, Contract::getExpirationTimestamp) .returns(null, Contract::getKey) @@ -541,7 +539,8 @@ void contractCallToExistingWithChildContractCreate(ContractIdType contractIdType .receipt(r -> r.setContractID(CONTRACT_ID)) .transactionBody(b -> b.setContractID(setupResult.protoContractId)) .record(r -> r.clearContractCallResult() - .setContractCallResult(recordItemBuilder.contractFunctionResult(CONTRACT_ID))) + .setContractCallResult(recordItemBuilder.contractFunctionResult(CONTRACT_ID) + .clearStateChanges())) .hapiVersion(HAPI_VERSION_0_23_0) .build(); @@ -774,7 +773,7 @@ private void assertContractEntity(RecordItem recordItem) { .returns(EntityId.of(transactionBody.getFileID()), Contract::getFileId) .returns(adminKey, Contract::getKey) .returns(transactionBody.getMemo(), Contract::getMemo) - .returns(createdTimestamp, Contract::getModifiedTimestamp) + .returns(createdTimestamp, Contract::getTimestampLower) .returns(null, Contract::getObtainerId) .returns(EntityId.of(transactionBody.getProxyAccountID()), Contract::getProxyAccountId) .returns(DomainUtils.getPublicKey(adminKey), Contract::getPublicKey) @@ -796,7 +795,7 @@ private void assertCreatedContract(RecordItem recordItem) { .returns(false, Contract::getDeleted) .returns(evmAddress, Contract::getEvmAddress) .returns(createdId.getId(), Contract::getId) - .returns(recordItem.getConsensusTimestamp(), Contract::getModifiedTimestamp) + .returns(recordItem.getConsensusTimestamp(), Contract::getTimestampLower) .returns(createdId.getEntityNum(), Contract::getNum) .returns(createdId.getShardNum(), Contract::getShard) .returns(createdId.getType(), Contract::getType); @@ -815,7 +814,7 @@ private ObjectAssert assertContractEntity(ContractUpdateTransactionBod .returns(DomainUtils.timeStampInNanos(expected.getExpirationTime()), Contract::getExpirationTimestamp) .returns(adminKey, Contract::getKey) .returns(getMemoFromContractUpdateTransactionBody(expected), Contract::getMemo) - .returns(updatedTimestamp, Contract::getModifiedTimestamp) + .returns(updatedTimestamp, Contract::getTimestampLower) .returns(null, Contract::getObtainerId) .returns(EntityId.of(expected.getProxyAccountID()), Contract::getProxyAccountId) .returns(DomainUtils.getPublicKey(adminKey), Contract::getPublicKey) @@ -870,7 +869,7 @@ private void assertContractCallResult(ContractCallTransactionBody transactionBod private void assertContractResult(long consensusTimestamp, ContractFunctionResult result, List logInfoList, ObjectAssert contractResult, - List stageChangeList) { + List stateChanges) { List createdContractIds = result.getCreatedContractIDsList() .stream() .map(ContractID::getContractNum) @@ -904,29 +903,27 @@ private void assertContractResult(long consensusTimestamp, ContractFunctionResul .returns(Utility.getTopic(logInfo, 3), ContractLog::getTopic3); } - for (var contractStateChangeInfo : stageChangeList) { + int count = 0; + var contractStateChanges = assertThat(contractStateChangeRepository.findAll()); + + for (var contractStateChangeInfo : stateChanges) { EntityId contractId = EntityId.of(contractStateChangeInfo.getContractID()); - for (int j = 0; j < contractStateChangeInfo.getStorageChangesCount(); ++j) { - StorageChange storageChange = contractStateChangeInfo.getStorageChanges(j); + for (var storageChange : contractStateChangeInfo.getStorageChangesList()) { byte[] slot = DomainUtils.toBytes(storageChange.getSlot()); byte[] valueWritten = storageChange.hasValueWritten() ? storageChange.getValueWritten().getValue() .toByteArray() : null; - ContractStateChange.Id id = new ContractStateChange.Id(); - id.setConsensusTimestamp(consensusTimestamp); - id.setContractId(contractId); - id.setSlot(slot); - - assertThat(contractStateChangeRepository.findById(id)) - .isPresent() - .get() - .returns(consensusTimestamp, ContractStateChange::getConsensusTimestamp) - .returns(contractId, ContractStateChange::getContractId) - .returns(slot, ContractStateChange::getSlot) + contractStateChanges.filteredOn(c -> c.getConsensusTimestamp() == consensusTimestamp + && c.getContractId() == contractId.getId() && Arrays.equals(c.getSlot(), slot)) + .hasSize(1) + .first() .returns(storageChange.getValueRead().toByteArray(), ContractStateChange::getValueRead) .returns(valueWritten, ContractStateChange::getValueWritten); + ++count; } } + + contractStateChanges.hasSize(count); } private void assertPartialContractCreateResult(ContractCreateTransactionBody transactionBody, 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 e406963fe22..b760dc21d9a 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 @@ -47,6 +47,7 @@ import java.time.Instant; import java.util.List; import java.util.Optional; +import javax.annotation.Resource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -60,6 +61,9 @@ import com.hedera.mirror.common.domain.transaction.NonFeeTransfer; import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.importer.repository.CryptoAllowanceRepository; +import com.hedera.mirror.importer.repository.NftAllowanceRepository; +import com.hedera.mirror.importer.repository.TokenAllowanceRepository; import com.hedera.mirror.importer.util.Utility; class EntityRecordItemListenerCryptoTest extends AbstractEntityRecordItemListenerTest { @@ -70,6 +74,15 @@ class EntityRecordItemListenerCryptoTest extends AbstractEntityRecordItemListene private static final ByteString ALIAS_KEY = ByteString.copyFromUtf8( "0a2212200aa8e21064c61eab86e2a9c164565b4e7a9a4146106e0a6cd03a8c395a110fff"); + @Resource + private CryptoAllowanceRepository cryptoAllowanceRepository; + + @Resource + private NftAllowanceRepository nftAllowanceRepository; + + @Resource + private TokenAllowanceRepository tokenAllowanceRepository; + @BeforeEach void before() { entityProperties.getPersist().setClaims(true); @@ -77,6 +90,52 @@ void before() { entityProperties.getPersist().setTransactionBytes(false); } + @Test + void cryptoAdjustAllowance() { + RecordItem recordItem = recordItemBuilder.cryptoAdjustAllowance().build(); + parseRecordItemAndCommit(recordItem); + assertAllowances(recordItem); + } + + @Test + void cryptoApproveAllowance() { + RecordItem recordItem = recordItemBuilder.cryptoApproveAllowance().build(); + parseRecordItemAndCommit(recordItem); + assertAllowances(recordItem); + } + + private void assertAllowances(RecordItem recordItem) { + assertAll( + () -> assertEquals(1, cryptoAllowanceRepository.count()), + () -> assertEquals(3, cryptoTransferRepository.count()), + () -> assertEquals(0, entityRepository.count()), + () -> assertEquals(2, nftAllowanceRepository.count()), + () -> assertEquals(1, tokenAllowanceRepository.count()), + () -> assertEquals(1, transactionRepository.count()), + () -> assertTransactionAndRecord(recordItem.getTransactionBody(), recordItem.getRecord()), + () -> assertThat(cryptoAllowanceRepository.findAll()) + .allSatisfy(a -> assertThat(a.getAmount()).isPositive()) + .allSatisfy(a -> assertThat(a.getOwner()).isPositive()) + .allSatisfy(a -> assertThat(a.getSpender()).isPositive()) + .allMatch(a -> recordItem.getConsensusTimestamp() == a.getTimestampLower()) + .allMatch(a -> recordItem.getPayerAccountId().equals(a.getPayerAccountId())), + () -> assertThat(nftAllowanceRepository.findAll()) + .allSatisfy(a -> assertThat(a.getOwner()).isPositive()) + .allSatisfy(a -> assertThat(a.getSerialNumbers()).isNotNull()) + .allSatisfy(a -> assertThat(a.getSpender()).isPositive()) + .allSatisfy(a -> assertThat(a.getTokenId()).isPositive()) + .allMatch(a -> recordItem.getConsensusTimestamp() == a.getTimestampLower()) + .allMatch(a -> recordItem.getPayerAccountId().equals(a.getPayerAccountId())), + () -> assertThat(tokenAllowanceRepository.findAll()) + .allSatisfy(a -> assertThat(a.getAmount()).isPositive()) + .allSatisfy(a -> assertThat(a.getOwner()).isPositive()) + .allSatisfy(a -> assertThat(a.getSpender()).isPositive()) + .allSatisfy(a -> assertThat(a.getTokenId()).isPositive()) + .allMatch(a -> recordItem.getConsensusTimestamp() == a.getTimestampLower()) + .allMatch(a -> recordItem.getPayerAccountId().equals(a.getPayerAccountId())) + ); + } + @Test void cryptoCreate() { Transaction transaction = cryptoCreateTransaction(); @@ -222,7 +281,7 @@ void cryptoUpdateSuccessfulTransaction() { () -> assertEquals(DomainUtils.timeStampInNanos(cryptoUpdateTransactionBody.getExpirationTime()), dbAccountEntity.getExpirationTimestamp()), () -> assertEquals(DomainUtils.timestampInNanosMax(record.getConsensusTimestamp()), - dbAccountEntity.getModifiedTimestamp()), + dbAccountEntity.getTimestampLower()), () -> assertFalse(dbAccountEntity.getReceiverSigRequired()) ); } @@ -363,7 +422,7 @@ void cryptoDeleteSuccessfulTransaction() { .isNotNull() .returns(true, Entity::getDeleted) .returns(DomainUtils.timestampInNanosMax(record.getConsensusTimestamp()), - Entity::getModifiedTimestamp) + Entity::getTimestampLower) .usingRecursiveComparison() .ignoringFields("deleted", "timestampRange") .isEqualTo(dbAccountEntityBefore) @@ -596,7 +655,7 @@ private void assertCryptoEntity(CryptoCreateTransactionBody expected, Timestamp () -> assertArrayEquals(expected.getKey().toByteArray(), actualAccount.getKey()), () -> assertEquals(0, actualAccount.getMaxAutomaticTokenAssociations()), () -> assertEquals(expected.getMemo(), actualAccount.getMemo()), - () -> assertEquals(timestamp, actualAccount.getModifiedTimestamp()), + () -> assertEquals(timestamp, actualAccount.getTimestampLower()), () -> assertEquals(DomainUtils.getPublicKey(expected.getKey().toByteArray()), actualAccount.getPublicKey()), () -> assertEquals(EntityId.of(expected.getProxyAccountID()), diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerTokenTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerTokenTest.java index 33026669dee..2c110cbf1b1 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerTokenTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerTokenTest.java @@ -68,15 +68,10 @@ import org.junit.jupiter.params.provider.MethodSource; import org.springframework.jdbc.core.JdbcTemplate; +import com.hedera.mirror.common.domain.entity.Entity; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.EntityIdEndec; import com.hedera.mirror.common.domain.entity.EntityType; -import com.hedera.mirror.importer.TestUtils; -import com.hedera.mirror.common.domain.transaction.AssessedCustomFee; -import com.hedera.mirror.importer.domain.AssessedCustomFeeWrapper; -import com.hedera.mirror.common.domain.transaction.CustomFee; -import com.hedera.mirror.importer.domain.CustomFeeWrapper; -import com.hedera.mirror.common.domain.entity.Entity; import com.hedera.mirror.common.domain.token.Nft; import com.hedera.mirror.common.domain.token.NftId; import com.hedera.mirror.common.domain.token.NftTransferId; @@ -88,7 +83,12 @@ import com.hedera.mirror.common.domain.token.TokenKycStatusEnum; import com.hedera.mirror.common.domain.token.TokenPauseStatusEnum; import com.hedera.mirror.common.domain.token.TokenTransfer; +import com.hedera.mirror.common.domain.transaction.AssessedCustomFee; +import com.hedera.mirror.common.domain.transaction.CustomFee; import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.importer.TestUtils; +import com.hedera.mirror.importer.domain.AssessedCustomFeeWrapper; +import com.hedera.mirror.importer.domain.CustomFeeWrapper; import com.hedera.mirror.importer.repository.NftRepository; import com.hedera.mirror.importer.repository.NftTransferRepository; import com.hedera.mirror.importer.repository.TokenAccountRepository; @@ -507,6 +507,7 @@ void tokenDissociateDeletedFungibleToken() { var expected = domainBuilder.tokenTransfer().customize(t -> t .amount(-10) .id(new TokenTransfer.Id(dissociateTimeStamp, EntityId.of(TOKEN_ID), EntityId.of(PAYER2))) + .isApproval(false) .payerAccountId(EntityId.of(PAYER)) .tokenDissociate(false)).get(); assertThat(tokenTransferRepository.findById(expected.getId())).get().isEqualTo(expected); @@ -1333,6 +1334,7 @@ private com.hedera.mirror.common.domain.token.NftTransfer domainNftTransfer(long TokenID token, AccountID payer) { var nftTransfer = new com.hedera.mirror.common.domain.token.NftTransfer(); nftTransfer.setId(new NftTransferId(consensusTimestamp, serialNumber, EntityId.of(token))); + nftTransfer.setIsApproval(false); nftTransfer.setPayerAccountId(EntityId.of(payer)); if (!receiver.equals(DEFAULT_ACCOUNT_ID)) { nftTransfer.setReceiverAccountId(EntityId.of(receiver)); @@ -1606,6 +1608,7 @@ private void assertTokenTransferInRepository(TokenID tokenID, AccountID accountI var expected = domainBuilder.tokenTransfer().customize(t -> t .amount(amount) .id(new TokenTransfer.Id(consensusTimestamp, EntityId.of(tokenID), EntityId.of(accountID))) + .isApproval(false) .payerAccountId(PAYER_ACCOUNT_ID) .tokenDissociate(false)).get(); assertThat(tokenTransferRepository.findById(expected.getId())) @@ -1695,7 +1698,7 @@ private void updateTokenFeeSchedule(TokenID tokenID, long consensusTimestamp, Li private TokenTransferList tokenTransfer(TokenID tokenId, AccountID accountId, long amount) { return TokenTransferList.newBuilder() .setToken(tokenId) - .addTransfers(AccountAmount.newBuilder().setAccountID(accountId).setAmount(amount)) + .addTransfers(AccountAmount.newBuilder().setAccountID(accountId).setAmount(amount).setIsApproval(false)) .build(); } @@ -1711,6 +1714,7 @@ private TokenTransferList nftTransfer(TokenID tokenId, AccountID receiverAccount if (senderAccountId != null) { nftTransferBuilder.setSenderAccountID(senderAccountId); } + nftTransferBuilder.setIsApproval(false); builder.addNftTransfers(nftTransferBuilder); } return builder.build(); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerTopicTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerTopicTest.java index d8144b9106b..9468880e0a3 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerTopicTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/entity/EntityRecordItemListenerTopicTest.java @@ -92,7 +92,7 @@ void createTopicTest(@ConvertWith(TopicIdArgumentConverter.class) TopicID topicI autoRenewPeriod); expectedEntity.setCreatedTimestamp(consensusTimestamp); expectedEntity.setDeleted(false); - expectedEntity.setModifiedTimestamp(consensusTimestamp); + expectedEntity.setTimestampLower(consensusTimestamp); assertThat(entity).isEqualTo(expectedEntity); } @@ -196,7 +196,7 @@ void updateTopicTest(@ConvertWith(TopicIdArgumentConverter.class) TopicID topicI var expectedEntity = createTopicEntity(topicId, updatedExpirationTimeSeconds, updatedExpirationTimeNanos, updatedAdminKey, updatedSubmitKey, updatedMemo, autoRenewAccountId, autoRenewPeriod); expectedEntity.setDeleted(false); - expectedEntity.setModifiedTimestamp(consensusTimestamp); + expectedEntity.setTimestampLower(consensusTimestamp); parseRecordItemAndCommit(new RecordItem(transaction, transactionRecord)); @@ -253,7 +253,7 @@ void updateTopicTestTopicNotFound() { var expectedTopic = createTopicEntity(TOPIC_ID, 11L, 0, adminKey, submitKey, memo, autoRenewAccount.getId(), 30L); expectedTopic.setDeleted(false); - expectedTopic.setModifiedTimestamp(consensusTimestamp); + expectedTopic.setTimestampLower(consensusTimestamp); assertThat(entity).isEqualTo(expectedTopic); } @@ -314,7 +314,7 @@ void updateTopicTestPartialUpdates(@ConvertWith(TopicIdArgumentConverter.class) topic.setAutoRenewAccountId(EntityId.of(0L, 0L, updatedAutoRenewAccountNum, EntityType.ACCOUNT)); } topic.setDeleted(false); - topic.setModifiedTimestamp(consensusTimestamp); + topic.setTimestampLower(consensusTimestamp); var entity = getTopicEntity(topicId); assertTransactionInRepository(responseCode, consensusTimestamp, entity.getId()); @@ -333,7 +333,7 @@ void deleteTopicTest() { // Setup expected data topic.setDeleted(true); - topic.setModifiedTimestamp(consensusTimestamp); + topic.setTimestampLower(consensusTimestamp); var transaction = createDeleteTopicTransaction(TOPIC_ID); var transactionRecord = createTransactionRecord(TOPIC_ID, consensusTimestamp, responseCode); @@ -354,7 +354,7 @@ void deleteTopicTestTopicNotFound() { // Setup expected data var topic = createTopicEntity(TOPIC_ID, null, null, null, null, "", null, null); topic.setDeleted(true); - topic.setModifiedTimestamp(consensusTimestamp); + topic.setTimestampLower(consensusTimestamp); // Topic not saved to the repository. var transaction = createDeleteTopicTransaction(TOPIC_ID); 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 9595b90b5e0..8ff08ce4ffa 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 @@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +import com.google.common.base.CaseFormat; import com.google.protobuf.ByteString; import com.hederahashgraph.api.proto.java.Key; import java.nio.charset.StandardCharsets; @@ -48,9 +49,12 @@ import com.hedera.mirror.common.domain.contract.ContractLog; import com.hedera.mirror.common.domain.contract.ContractResult; import com.hedera.mirror.common.domain.contract.ContractStateChange; +import com.hedera.mirror.common.domain.entity.CryptoAllowance; import com.hedera.mirror.common.domain.entity.Entity; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.EntityType; +import com.hedera.mirror.common.domain.entity.NftAllowance; +import com.hedera.mirror.common.domain.entity.TokenAllowance; import com.hedera.mirror.common.domain.file.FileData; import com.hedera.mirror.common.domain.schedule.Schedule; import com.hedera.mirror.common.domain.token.Nft; @@ -80,16 +84,19 @@ import com.hedera.mirror.importer.repository.ContractRepository; import com.hedera.mirror.importer.repository.ContractResultRepository; import com.hedera.mirror.importer.repository.ContractStateChangeRepository; +import com.hedera.mirror.importer.repository.CryptoAllowanceRepository; import com.hedera.mirror.importer.repository.CryptoTransferRepository; import com.hedera.mirror.importer.repository.EntityRepository; import com.hedera.mirror.importer.repository.FileDataRepository; import com.hedera.mirror.importer.repository.LiveHashRepository; +import com.hedera.mirror.importer.repository.NftAllowanceRepository; import com.hedera.mirror.importer.repository.NftRepository; import com.hedera.mirror.importer.repository.NftTransferRepository; import com.hedera.mirror.importer.repository.NonFeeTransferRepository; import com.hedera.mirror.importer.repository.RecordFileRepository; import com.hedera.mirror.importer.repository.ScheduleRepository; import com.hedera.mirror.importer.repository.TokenAccountRepository; +import com.hedera.mirror.importer.repository.TokenAllowanceRepository; import com.hedera.mirror.importer.repository.TokenRepository; import com.hedera.mirror.importer.repository.TokenTransferRepository; import com.hedera.mirror.importer.repository.TopicMessageRepository; @@ -102,29 +109,32 @@ class SqlEntityListenerTest extends IntegrationTest { private static final String KEY2 = "0a3312200aa8e21064c61eab86e2a9c164565b4e7a9a4146106e0a6cd03a8c395a110e92"; private static final EntityId TRANSACTION_PAYER = EntityId.of("0.0.1000", ACCOUNT); - private final DomainBuilder domainBuilder; - private final TransactionRepository transactionRepository; - private final EntityRepository entityRepository; - private final CryptoTransferRepository cryptoTransferRepository; - private final NonFeeTransferRepository nonFeeTransferRepository; - private final ContractRepository contractRepository; private final ContractLogRepository contractLogRepository; + private final ContractRepository contractRepository; private final ContractResultRepository contractResultRepository; private final ContractStateChangeRepository contractStateChangeRepository; + private final CryptoAllowanceRepository cryptoAllowanceRepository; + private final CryptoTransferRepository cryptoTransferRepository; + private final DomainBuilder domainBuilder; + private final EntityRepository entityRepository; + private final FileDataRepository fileDataRepository; private final JdbcOperations jdbcOperations; private final LiveHashRepository liveHashRepository; private final NftRepository nftRepository; + private final NftAllowanceRepository nftAllowanceRepository; private final NftTransferRepository nftTransferRepository; - private final FileDataRepository fileDataRepository; - private final TopicMessageRepository topicMessageRepository; + private final NonFeeTransferRepository nonFeeTransferRepository; private final RecordFileRepository recordFileRepository; - private final TokenRepository tokenRepository; - private final TokenAccountRepository tokenAccountRepository; - private final TokenTransferRepository tokenTransferRepository; private final ScheduleRepository scheduleRepository; - private final TransactionSignatureRepository transactionSignatureRepository; private final SqlEntityListener sqlEntityListener; private final SqlProperties sqlProperties; + private final TokenAccountRepository tokenAccountRepository; + private final TokenAllowanceRepository tokenAllowanceRepository; + private final TokenRepository tokenRepository; + private final TokenTransferRepository tokenTransferRepository; + private final TopicMessageRepository topicMessageRepository; + private final TransactionRepository transactionRepository; + private final TransactionSignatureRepository transactionSignatureRepository; private final TransactionTemplate transactionTemplate; private static Key keyFromString(String key) { @@ -193,20 +203,20 @@ void onContractHistory(int commitIndex) { contractUpdate.setExpirationTimestamp(500L); contractUpdate.setKey(domainBuilder.key()); contractUpdate.setMemo("updated"); - contractUpdate.setModifiedTimestamp(contractCreate.getModifiedTimestamp() + 1); + contractUpdate.setTimestampLower(contractCreate.getTimestampLower() + 1); contractUpdate.setProxyAccountId(EntityId.of(100L, ACCOUNT)); Contract contractDelete = contractCreate.toEntityId().toEntity(); contractDelete.setDeleted(true); contractDelete.setEvmAddress(contractCreate.getEvmAddress()); - contractDelete.setModifiedTimestamp(contractCreate.getModifiedTimestamp() + 2); + contractDelete.setTimestampLower(contractCreate.getTimestampLower() + 2); contractDelete.setObtainerId(EntityId.of(999L, EntityType.CONTRACT)); // Expected merged objects Contract mergedCreate = TestUtils.clone(contractCreate); Contract mergedUpdate = TestUtils.merge(contractCreate, contractUpdate); Contract mergedDelete = TestUtils.merge(mergedUpdate, contractDelete); - mergedCreate.setTimestampRangeUpper(contractUpdate.getModifiedTimestamp()); + mergedCreate.setTimestampUpper(contractUpdate.getTimestampLower()); // when sqlEntityListener.onContract(contractCreate); @@ -227,7 +237,7 @@ void onContractHistory(int commitIndex) { completeFileAndCommit(); // then - mergedUpdate.setTimestampRangeUpper(contractDelete.getModifiedTimestamp()); + mergedUpdate.setTimestampUpper(contractDelete.getTimestampLower()); assertThat(contractRepository.findAll()).containsExactly(mergedDelete); assertThat(findHistory(Contract.class)).containsExactly(mergedCreate, mergedUpdate); assertThat(entityRepository.count()).isZero(); @@ -272,6 +282,67 @@ void onContractStateChange() { assertThat(contractStateChangeRepository.findAll()).containsExactlyInAnyOrder(contractStateChange); } + @Test + void onCryptoAllowance() { + // given + CryptoAllowance cryptoAllowance1 = domainBuilder.cryptoAllowance().get(); + CryptoAllowance cryptoAllowance2 = domainBuilder.cryptoAllowance().get(); + + // when + sqlEntityListener.onCryptoAllowance(cryptoAllowance1); + sqlEntityListener.onCryptoAllowance(cryptoAllowance2); + completeFileAndCommit(); + + // then + assertThat(entityRepository.count()).isZero(); + assertThat(cryptoAllowanceRepository.findAll()).containsExactlyInAnyOrder(cryptoAllowance1, cryptoAllowance2); + assertThat(findHistory(CryptoAllowance.class, "payer_account_id, spender")).isEmpty(); + } + + @ValueSource(ints = {1, 2, 3}) + @ParameterizedTest + void onCryptoAllowanceHistory(int commitIndex) { + // given + final String idColumns = "payer_account_id, spender"; + var builder = domainBuilder.cryptoAllowance(); + CryptoAllowance cryptoAllowanceCreate = builder.get(); + + CryptoAllowance cryptoAllowanceUpdate1 = builder.customize(c -> c.amount(999L)).get(); + cryptoAllowanceUpdate1.setTimestampLower(cryptoAllowanceCreate.getTimestampLower() + 1); + + CryptoAllowance cryptoAllowanceUpdate2 = builder.customize(c -> c.amount(0L)).get(); + cryptoAllowanceUpdate2.setTimestampLower(cryptoAllowanceCreate.getTimestampLower() + 2); + + // Expected merged objects + CryptoAllowance mergedCreate = TestUtils.clone(cryptoAllowanceCreate); + CryptoAllowance mergedUpdate1 = TestUtils.merge(cryptoAllowanceCreate, cryptoAllowanceUpdate1); + CryptoAllowance mergedUpdate2 = TestUtils.merge(mergedUpdate1, cryptoAllowanceUpdate2); + mergedCreate.setTimestampUpper(cryptoAllowanceUpdate1.getTimestampLower()); + + // when + sqlEntityListener.onCryptoAllowance(cryptoAllowanceCreate); + if (commitIndex > 1) { + completeFileAndCommit(); + assertThat(cryptoAllowanceRepository.findAll()).containsExactly(cryptoAllowanceCreate); + assertThat(findHistory(CryptoAllowance.class, idColumns)).isEmpty(); + } + + sqlEntityListener.onCryptoAllowance(cryptoAllowanceUpdate1); + if (commitIndex > 2) { + completeFileAndCommit(); + assertThat(cryptoAllowanceRepository.findAll()).containsExactly(mergedUpdate1); + assertThat(findHistory(CryptoAllowance.class, idColumns)).containsExactly(mergedCreate); + } + + sqlEntityListener.onCryptoAllowance(cryptoAllowanceUpdate2); + completeFileAndCommit(); + + // then + mergedUpdate1.setTimestampUpper(cryptoAllowanceUpdate2.getTimestampLower()); + assertThat(cryptoAllowanceRepository.findAll()).containsExactly(mergedUpdate2); + assertThat(findHistory(CryptoAllowance.class, idColumns)).containsExactly(mergedCreate, mergedUpdate1); + } + @Test void onCryptoTransferList() { // given @@ -320,7 +391,7 @@ void onEntityHistory(int commitIndex) { entityUpdate.setKey(domainBuilder.key()); entityUpdate.setMaxAutomaticTokenAssociations(40); entityUpdate.setMemo("updated"); - entityUpdate.setModifiedTimestamp(entityCreate.getModifiedTimestamp() + 1); + entityUpdate.setTimestampLower(entityCreate.getTimestampLower() + 1); entityUpdate.setProxyAccountId(EntityId.of(100L, ACCOUNT)); entityUpdate.setReceiverSigRequired(true); entityUpdate.setSubmitKey(domainBuilder.key()); @@ -328,13 +399,13 @@ void onEntityHistory(int commitIndex) { Entity entityDelete = entityCreate.toEntityId().toEntity(); entityDelete.setAlias(entityCreate.getAlias()); entityDelete.setDeleted(true); - entityDelete.setModifiedTimestamp(entityCreate.getModifiedTimestamp() + 2); + entityDelete.setTimestampLower(entityCreate.getTimestampLower() + 2); // Expected merged objects Entity mergedCreate = TestUtils.clone(entityCreate); Entity mergedUpdate = TestUtils.merge(entityCreate, entityUpdate); Entity mergedDelete = TestUtils.merge(mergedUpdate, entityDelete); - mergedCreate.setTimestampRangeUpper(entityUpdate.getModifiedTimestamp()); + mergedCreate.setTimestampUpper(entityUpdate.getTimestampLower()); // when sqlEntityListener.onEntity(entityCreate); @@ -355,7 +426,7 @@ void onEntityHistory(int commitIndex) { completeFileAndCommit(); // then - mergedUpdate.setTimestampRangeUpper(entityDelete.getModifiedTimestamp()); + mergedUpdate.setTimestampUpper(entityDelete.getTimestampLower()); assertThat(entityRepository.findAll()).containsExactly(mergedDelete); assertThat(findHistory(Entity.class)).containsExactly(mergedCreate, mergedUpdate); } @@ -509,6 +580,68 @@ void onNft() { assertThat(nftRepository.findAll()).containsExactlyInAnyOrder(nft1, nft2); } + @Test + void onNftAllowance() { + // given + NftAllowance nftAllowance1 = domainBuilder.nftAllowance().get(); + NftAllowance nftAllowance2 = domainBuilder.nftAllowance().get(); + + // when + sqlEntityListener.onNftAllowance(nftAllowance1); + sqlEntityListener.onNftAllowance(nftAllowance2); + completeFileAndCommit(); + + // then + assertThat(entityRepository.count()).isZero(); + assertThat(nftAllowanceRepository.findAll()).containsExactlyInAnyOrder(nftAllowance1, nftAllowance2); + assertThat(findHistory(NftAllowance.class, "payer_account_id, spender, token_id")).isEmpty(); + } + + @ValueSource(ints = {1, 2, 3}) + @ParameterizedTest + void onNftAllowanceHistory(int commitIndex) { + // given + final String idColumns = "payer_account_id, spender, token_id"; + var builder = domainBuilder.nftAllowance(); + NftAllowance nftAllowanceCreate = builder.get(); + + NftAllowance nftAllowanceUpdate1 = builder.customize(c -> c.serialNumbers(List.of(4L, 5L, 6L))).get(); + nftAllowanceUpdate1.setTimestampLower(nftAllowanceCreate.getTimestampLower() + 1); + + NftAllowance nftAllowanceUpdate2 = builder.customize(c -> c.approvedForAll(true).serialNumbers(List.of())) + .get(); + nftAllowanceUpdate2.setTimestampLower(nftAllowanceCreate.getTimestampLower() + 2); + + // Expected merged objects + NftAllowance mergedCreate = TestUtils.clone(nftAllowanceCreate); + NftAllowance mergedUpdate1 = TestUtils.merge(nftAllowanceCreate, nftAllowanceUpdate1); + NftAllowance mergedUpdate2 = TestUtils.merge(mergedUpdate1, nftAllowanceUpdate2); + mergedCreate.setTimestampUpper(nftAllowanceUpdate1.getTimestampLower()); + + // when + sqlEntityListener.onNftAllowance(nftAllowanceCreate); + if (commitIndex > 1) { + completeFileAndCommit(); + assertThat(nftAllowanceRepository.findAll()).containsExactly(nftAllowanceCreate); + assertThat(findHistory(NftAllowance.class, idColumns)).isEmpty(); + } + + sqlEntityListener.onNftAllowance(nftAllowanceUpdate1); + if (commitIndex > 2) { + completeFileAndCommit(); + assertThat(nftAllowanceRepository.findAll()).containsExactly(mergedUpdate1); + assertThat(findHistory(NftAllowance.class, idColumns)).containsExactly(mergedCreate); + } + + sqlEntityListener.onNftAllowance(nftAllowanceUpdate2); + completeFileAndCommit(); + + // then + mergedUpdate1.setTimestampUpper(nftAllowanceUpdate2.getTimestampLower()); + assertThat(nftAllowanceRepository.findAll()).containsExactly(mergedUpdate2); + assertThat(findHistory(NftAllowance.class, idColumns)).containsExactly(mergedCreate, mergedUpdate1); + } + @Test void onNftMintOutOfOrder() { // create token first @@ -997,6 +1130,67 @@ void onTokenAccountSpanningRecordFiles() { assertThat(tokenAccountRepository.findAll()).containsExactlyInAnyOrderElementsOf(expected); } + @Test + void onTokenAllowance() { + // given + TokenAllowance tokenAllowance1 = domainBuilder.tokenAllowance().get(); + TokenAllowance tokenAllowance2 = domainBuilder.tokenAllowance().get(); + + // when + sqlEntityListener.onTokenAllowance(tokenAllowance1); + sqlEntityListener.onTokenAllowance(tokenAllowance2); + completeFileAndCommit(); + + // then + assertThat(entityRepository.count()).isZero(); + assertThat(tokenAllowanceRepository.findAll()).containsExactlyInAnyOrder(tokenAllowance1, tokenAllowance2); + assertThat(findHistory(TokenAllowance.class, "payer_account_id, spender, token_id")).isEmpty(); + } + + @ValueSource(ints = {1, 2, 3}) + @ParameterizedTest + void onTokenAllowanceHistory(int commitIndex) { + // given + final String idColumns = "payer_account_id, spender, token_id"; + var builder = domainBuilder.tokenAllowance(); + TokenAllowance tokenAllowanceCreate = builder.get(); + + TokenAllowance tokenAllowanceUpdate1 = builder.customize(c -> c.amount(999L)).get(); + tokenAllowanceUpdate1.setTimestampLower(tokenAllowanceCreate.getTimestampLower() + 1); + + TokenAllowance tokenAllowanceUpdate2 = builder.customize(c -> c.amount(0)).get(); + tokenAllowanceUpdate2.setTimestampLower(tokenAllowanceCreate.getTimestampLower() + 2); + + // Expected merged objects + TokenAllowance mergedCreate = TestUtils.clone(tokenAllowanceCreate); + TokenAllowance mergedUpdate1 = TestUtils.merge(tokenAllowanceCreate, tokenAllowanceUpdate1); + TokenAllowance mergedUpdate2 = TestUtils.merge(mergedUpdate1, tokenAllowanceUpdate2); + mergedCreate.setTimestampUpper(tokenAllowanceUpdate1.getTimestampLower()); + + // when + sqlEntityListener.onTokenAllowance(tokenAllowanceCreate); + if (commitIndex > 1) { + completeFileAndCommit(); + assertThat(tokenAllowanceRepository.findAll()).containsExactly(tokenAllowanceCreate); + assertThat(findHistory(TokenAllowance.class, idColumns)).isEmpty(); + } + + sqlEntityListener.onTokenAllowance(tokenAllowanceUpdate1); + if (commitIndex > 2) { + completeFileAndCommit(); + assertThat(tokenAllowanceRepository.findAll()).containsExactly(mergedUpdate1); + assertThat(findHistory(TokenAllowance.class, idColumns)).containsExactly(mergedCreate); + } + + sqlEntityListener.onTokenAllowance(tokenAllowanceUpdate2); + completeFileAndCommit(); + + // then + mergedUpdate1.setTimestampUpper(tokenAllowanceUpdate2.getTimestampLower()); + assertThat(tokenAllowanceRepository.findAll()).containsExactly(mergedUpdate2); + assertThat(findHistory(TokenAllowance.class, idColumns)).containsExactly(mergedCreate, mergedUpdate1); + } + @Test void onTokenTransfer() { EntityId tokenId1 = EntityId.of("0.0.3", TOKEN); @@ -1078,8 +1272,13 @@ private void completeFileAndCommit() { } private Collection findHistory(Class historyClass) { - String table = historyClass.getSimpleName().toLowerCase(); - String sql = String.format("select * from %s_history order by id asc, timestamp_range asc", table); + return findHistory(historyClass, "id"); + } + + private Collection findHistory(Class historyClass, String ids) { + String table = historyClass.getSimpleName(); + table = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, table); + String sql = String.format("select * from %s_history order by %s, timestamp_range asc", table, ids); return jdbcOperations.query(sql, rowMapper(historyClass)); } @@ -1106,7 +1305,7 @@ private Entity getEntity(long id, Long createdTimestamp, long modifiedTimestamp, entity.setExpirationTimestamp(expiryTimeNs); entity.setKey(adminKey != null ? adminKey.toByteArray() : null); entity.setMaxAutomaticTokenAssociations(maxAutomaticTokenAssociations); - entity.setModifiedTimestamp(modifiedTimestamp); + entity.setTimestampLower(modifiedTimestamp); entity.setNum(id); entity.setRealm(0L); entity.setReceiverSigRequired(receiverSigRequired); 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 f92edea755c..fc9d5388c24 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 @@ -21,6 +21,7 @@ */ import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.verify; import com.google.protobuf.BoolValue; @@ -29,9 +30,6 @@ 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; @@ -48,6 +46,7 @@ import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.Builder; @@ -66,6 +65,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import com.hedera.mirror.common.domain.DomainBuilder; import com.hedera.mirror.common.domain.contract.Contract; import com.hedera.mirror.common.domain.entity.AbstractEntity; import com.hedera.mirror.common.domain.entity.Entity; @@ -75,6 +75,7 @@ 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.domain.RecordItemBuilder; import com.hedera.mirror.importer.parser.record.entity.EntityListener; import com.hedera.mirror.importer.repository.EntityRepository; import com.hedera.mirror.importer.util.Utility; @@ -83,39 +84,28 @@ abstract class AbstractTransactionHandlerTest { protected static final Duration DEFAULT_AUTO_RENEW_PERIOD = Duration.newBuilder().setSeconds(1).build(); - protected static final Long DEFAULT_ENTITY_NUM = 100L; - protected static final Timestamp DEFAULT_EXPIRATION_TIME = Utility.instantToTimestamp(Instant.now()); - protected static final Key DEFAULT_KEY = getKey("4a5ad514f0957fa170a676210c9bdbddf3bc9519702cf915fa6767a40463b96f"); - protected static final String DEFAULT_MEMO = "default entity memo"; - protected static final boolean DEFAULT_RECEIVER_SIG_REQUIRED = false; - protected static final Key DEFAULT_SUBMIT_KEY = getKey( "5a5ad514f0957fa170a676210c9bdbddf3bc9519702cf915fa6767a40463b96G"); - protected static final KeyList DEFAULT_KEY_LIST = KeyList.newBuilder().addAllKeys( - Arrays.asList(DEFAULT_KEY, DEFAULT_SUBMIT_KEY)) - .build(); - + Arrays.asList(DEFAULT_KEY, DEFAULT_SUBMIT_KEY)).build(); protected static final String UPDATED_MEMO = "update memo"; - protected static final BoolValue UPDATED_RECEIVER_SIG_REQUIRED = BoolValue.of(true); protected static final Timestamp MODIFIED_TIMESTAMP = Timestamp.newBuilder().setSeconds(200).setNanos(2).build(); private static final Timestamp CREATED_TIMESTAMP = Timestamp.newBuilder().setSeconds(100).setNanos(1).build(); private static final Long CREATED_TIMESTAMP_NS = DomainUtils.timestampInNanosMax(CREATED_TIMESTAMP); - private static final Long MODIFIED_TIMESTAMP_NS = DomainUtils.timestampInNanosMax(MODIFIED_TIMESTAMP); + protected final DomainBuilder domainBuilder = new DomainBuilder(); + protected final RecordItemBuilder recordItemBuilder = new RecordItemBuilder(); 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) @@ -134,6 +124,13 @@ protected static Key getKey(String keyString) { return Key.newBuilder().setEd25519(ByteString.copyFromUtf8(keyString)).build(); } + protected final T assertArg(Consumer asserter) { + return argThat(t -> { + asserter.accept(t); + return true; + }); + } + protected abstract TransactionHandler getTransactionHandler(); // All sub-classes need to implement this function and return a TransactionBody.Builder with valid 'oneof data' set. @@ -265,12 +262,12 @@ protected AbstractEntity getExpectedEntityWithTimestamp() { if (entityOperation == EntityOperation.CREATE) { entity.setCreatedTimestamp(CREATED_TIMESTAMP_NS); entity.setDeleted(false); - entity.setModifiedTimestamp(CREATED_TIMESTAMP_NS); + entity.setTimestampLower(CREATED_TIMESTAMP_NS); } else if (entityOperation == EntityOperation.UPDATE) { entity.setDeleted(false); - entity.setModifiedTimestamp(MODIFIED_TIMESTAMP_NS); + entity.setTimestampLower(MODIFIED_TIMESTAMP_NS); } else { - entity.setModifiedTimestamp(MODIFIED_TIMESTAMP_NS); + entity.setTimestampLower(MODIFIED_TIMESTAMP_NS); } return entity; diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoAdjustAllowanceTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoAdjustAllowanceTransactionHandlerTest.java new file mode 100644 index 00000000000..b786b28652d --- /dev/null +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoAdjustAllowanceTransactionHandlerTest.java @@ -0,0 +1,87 @@ +package com.hedera.mirror.importer.parser.record.transactionhandler; + +/*- + * ‌ + * 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 static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.hederahashgraph.api.proto.java.CryptoAdjustAllowanceTransactionBody; +import com.hederahashgraph.api.proto.java.TransactionBody; +import org.junit.jupiter.api.Test; + +import com.hedera.mirror.common.domain.entity.CryptoAllowance; +import com.hedera.mirror.common.domain.entity.EntityType; +import com.hedera.mirror.common.domain.entity.NftAllowance; +import com.hedera.mirror.common.domain.entity.TokenAllowance; + +class CryptoAdjustAllowanceTransactionHandlerTest extends AbstractTransactionHandlerTest { + + @Override + protected TransactionHandler getTransactionHandler() { + return new CryptoAdjustAllowanceTransactionHandler(entityListener); + } + + @Override + protected TransactionBody.Builder getDefaultTransactionBody() { + return TransactionBody.newBuilder() + .setCryptoAdjustAllowance(CryptoAdjustAllowanceTransactionBody.newBuilder().build()); + } + + @Override + protected EntityType getExpectedEntityIdType() { + return null; + } + + @Test + void updateTransaction() { + var recordItem = recordItemBuilder.cryptoAdjustAllowance().build(); + var timestamp = recordItem.getConsensusTimestamp(); + var transaction = domainBuilder.transaction().customize(t -> t.consensusTimestamp(timestamp)).get(); + transactionHandler.updateTransaction(transaction, recordItem); + + verify(entityListener).onCryptoAllowance(assertArg(t -> assertThat(t) + .isNotNull() + .satisfies(a -> assertThat(a.getAmount()).isPositive()) + .satisfies(a -> assertThat(a.getOwner()).isPositive()) + .satisfies(a -> assertThat(a.getSpender()).isPositive()) + .returns(recordItem.getPayerAccountId(), CryptoAllowance::getPayerAccountId) + .returns(timestamp, CryptoAllowance::getTimestampLower))); + + verify(entityListener, times(2)).onNftAllowance(assertArg(t -> assertThat(t) + .isNotNull() + .satisfies(a -> assertThat(a.isApprovedForAll()).isNotNull()) + .satisfies(a -> assertThat(a.getOwner()).isPositive()) + .satisfies(a -> assertThat(a.getSpender()).isPositive()) + .satisfies(a -> assertThat(a.getTokenId()).isPositive()) + .returns(recordItem.getPayerAccountId(), NftAllowance::getPayerAccountId) + .returns(timestamp, NftAllowance::getTimestampLower))); + + verify(entityListener).onTokenAllowance(assertArg(t -> assertThat(t) + .isNotNull() + .satisfies(a -> assertThat(a.getAmount()).isPositive()) + .satisfies(a -> assertThat(a.getOwner()).isPositive()) + .satisfies(a -> assertThat(a.getSpender()).isNotNull()) + .satisfies(a -> assertThat(a.getTokenId()).isPositive()) + .returns(recordItem.getPayerAccountId(), TokenAllowance::getPayerAccountId) + .returns(timestamp, TokenAllowance::getTimestampLower))); + } +} diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandlerTest.java new file mode 100644 index 00000000000..fb0fa004869 --- /dev/null +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/CryptoApproveAllowanceTransactionHandlerTest.java @@ -0,0 +1,101 @@ +package com.hedera.mirror.importer.parser.record.transactionhandler; + +/*- + * ‌ + * 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 static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.hederahashgraph.api.proto.java.CryptoApproveAllowanceTransactionBody; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.api.proto.java.TransactionBody; +import org.junit.jupiter.api.Test; + +import com.hedera.mirror.common.domain.entity.CryptoAllowance; +import com.hedera.mirror.common.domain.entity.EntityType; +import com.hedera.mirror.common.domain.entity.NftAllowance; +import com.hedera.mirror.common.domain.entity.TokenAllowance; +import com.hedera.mirror.common.domain.transaction.RecordItem; +import com.hedera.mirror.common.domain.transaction.Transaction; + +class CryptoApproveAllowanceTransactionHandlerTest extends AbstractTransactionHandlerTest { + + @Override + protected TransactionHandler getTransactionHandler() { + return new CryptoApproveAllowanceTransactionHandler(entityListener); + } + + @Override + protected TransactionBody.Builder getDefaultTransactionBody() { + return TransactionBody.newBuilder() + .setCryptoApproveAllowance(CryptoApproveAllowanceTransactionBody.newBuilder().build()); + } + + @Override + protected EntityType getExpectedEntityIdType() { + return null; + } + + @Test + void updateTransactionUnsuccessful() { + var transaction = new Transaction(); + RecordItem recordItem = recordItemBuilder.cryptoApproveAllowance() + .receipt(r -> r.setStatus(ResponseCodeEnum.ACCOUNT_DELETED)) + .build(); + transactionHandler.updateTransaction(transaction, recordItem); + verifyNoInteractions(entityListener); + } + + @Test + void updateTransactionSuccessful() { + var recordItem = recordItemBuilder.cryptoApproveAllowance().build(); + var timestamp = recordItem.getConsensusTimestamp(); + var transaction = domainBuilder.transaction().customize(t -> t.consensusTimestamp(timestamp)).get(); + transactionHandler.updateTransaction(transaction, recordItem); + + verify(entityListener).onCryptoAllowance(assertArg(t -> assertThat(t) + .isNotNull() + .satisfies(a -> assertThat(a.getAmount()).isPositive()) + .satisfies(a -> assertThat(a.getOwner()).isPositive()) + .satisfies(a -> assertThat(a.getSpender()).isPositive()) + .returns(recordItem.getPayerAccountId(), CryptoAllowance::getPayerAccountId) + .returns(timestamp, CryptoAllowance::getTimestampLower))); + + verify(entityListener, times(2)).onNftAllowance(assertArg(t -> assertThat(t) + .isNotNull() + .satisfies(a -> assertThat(a.isApprovedForAll()).isNotNull()) + .satisfies(a -> assertThat(a.getOwner()).isPositive()) + .satisfies(a -> assertThat(a.getSpender()).isPositive()) + .satisfies(a -> assertThat(a.getTokenId()).isPositive()) + .returns(recordItem.getPayerAccountId(), NftAllowance::getPayerAccountId) + .returns(timestamp, NftAllowance::getTimestampLower))); + + verify(entityListener).onTokenAllowance(assertArg(t -> assertThat(t) + .isNotNull() + .satisfies(a -> assertThat(a.getAmount()).isPositive()) + .satisfies(a -> assertThat(a.getOwner()).isPositive()) + .satisfies(a -> assertThat(a.getSpender()).isNotNull()) + .satisfies(a -> assertThat(a.getTokenId()).isPositive()) + .returns(recordItem.getPayerAccountId(), TokenAllowance::getPayerAccountId) + .returns(timestamp, TokenAllowance::getTimestampLower))); + } +} diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenUpdateTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenUpdateTransactionHandlerTest.java index 5c7191d17e4..1270d990c4c 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenUpdateTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenUpdateTransactionHandlerTest.java @@ -20,8 +20,6 @@ * ‍ */ -import com.hedera.mirror.common.util.DomainUtils; - import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.Duration; import com.hederahashgraph.api.proto.java.NftTransfer; @@ -43,6 +41,7 @@ import com.hedera.mirror.common.domain.token.NftTransferId; import com.hedera.mirror.common.domain.transaction.RecordItem; import com.hedera.mirror.common.domain.transaction.Transaction; +import com.hedera.mirror.common.util.DomainUtils; import com.hedera.mirror.importer.repository.NftRepository; @ExtendWith(MockitoExtension.class) @@ -105,7 +104,7 @@ void updateTreasury() { TransactionBody body = recordItem.getTransactionBody(); var payerAccount = EntityId.of(body.getTransactionID().getAccountID()).toEntity().getId(); Mockito.verify(nftRepository).updateTreasury(tokenID.getTokenNum(), previousAccountId.getAccountNum(), - newAccountId.getAccountNum(), consensusTimestamp, payerAccount); + newAccountId.getAccountNum(), consensusTimestamp, payerAccount, false); } @Test diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/CryptoAllowanceRepositoryTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/CryptoAllowanceRepositoryTest.java new file mode 100644 index 00000000000..acf4785ff45 --- /dev/null +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/CryptoAllowanceRepositoryTest.java @@ -0,0 +1,59 @@ +package com.hedera.mirror.importer.repository; + +/*- + * ‌ + * 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 java.util.List; +import javax.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.RowMapper; + +import com.hedera.mirror.common.domain.entity.CryptoAllowance; + +class CryptoAllowanceRepositoryTest extends AbstractRepositoryTest { + + private static final RowMapper ROW_MAPPER = rowMapper(CryptoAllowance.class); + + @Resource + private CryptoAllowanceRepository cryptoAllowanceRepository; + + @Test + void save() { + CryptoAllowance cryptoAllowance = domainBuilder.cryptoAllowance().persist(); + assertThat(cryptoAllowanceRepository.findById(cryptoAllowance.getId())).get().isEqualTo(cryptoAllowance); + } + + /** + * This test verifies that the domain object and table definition are in sync with the history table. + */ + @Test + void history() { + CryptoAllowance cryptoAllowance = domainBuilder.cryptoAllowance().persist(); + + jdbcOperations.update("insert into crypto_allowance_history select * from crypto_allowance"); + List cryptoAllowanceHistory = jdbcOperations.query("select * from crypto_allowance_history", + ROW_MAPPER); + + assertThat(cryptoAllowanceRepository.findAll()).containsExactly(cryptoAllowance); + assertThat(cryptoAllowanceHistory).containsExactly(cryptoAllowance); + } +} diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/NftAllowanceRepositoryTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/NftAllowanceRepositoryTest.java new file mode 100644 index 00000000000..b68d82f7dda --- /dev/null +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/NftAllowanceRepositoryTest.java @@ -0,0 +1,59 @@ +package com.hedera.mirror.importer.repository; + +/*- + * ‌ + * 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 java.util.List; +import javax.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.RowMapper; + +import com.hedera.mirror.common.domain.entity.NftAllowance; + +class NftAllowanceRepositoryTest extends AbstractRepositoryTest { + + private static final RowMapper ROW_MAPPER = rowMapper(NftAllowance.class); + + @Resource + private NftAllowanceRepository nftAllowanceRepository; + + @Test + void save() { + NftAllowance nftAllowance = domainBuilder.nftAllowance().persist(); + assertThat(nftAllowanceRepository.findById(nftAllowance.getId())).get().isEqualTo(nftAllowance); + } + + /** + * This test verifies that the domain object and table definition are in sync with the history table. + */ + @Test + void history() { + NftAllowance nftAllowance = domainBuilder.nftAllowance().persist(); + + jdbcOperations.update("insert into nft_allowance_history select * from nft_allowance"); + List nftAllowanceHistory = jdbcOperations.query("select * from nft_allowance_history", + ROW_MAPPER); + + assertThat(nftAllowanceRepository.findAll()).containsExactly(nftAllowance); + assertThat(nftAllowanceHistory).containsExactly(nftAllowance); + } +} diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/NftRepositoryTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/NftRepositoryTest.java index e6d0d251c35..f4aaa4005fd 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/NftRepositoryTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/NftRepositoryTest.java @@ -96,9 +96,8 @@ void updateTreasury() { EntityId tokenId = nft1.getId().getTokenId(); EntityId previousAccountId = nft1.getAccountId(); - nftRepository - .updateTreasury(tokenId.getId(), previousAccountId.getId(), newAccountId.getId(), consensusTimestamp, - EntityId.of("0.0.200", EntityType.ACCOUNT).getId()); + nftRepository.updateTreasury(tokenId.getId(), previousAccountId.getId(), newAccountId.getId(), + consensusTimestamp, EntityId.of("0.0.200", EntityType.ACCOUNT).getId(), false); assertAccountUpdated(nft1, newAccountId); assertAccountUpdated(nft2, newAccountId); @@ -112,6 +111,7 @@ void updateTreasury() { nftTransfers.extracting(n -> n.getId().getTokenId()).containsOnly(tokenId); nftTransfers.extracting(n -> n.getId().getConsensusTimestamp()).containsOnly(consensusTimestamp); nftTransfers.extracting(n -> n.getId().getSerialNumber()).containsExactlyInAnyOrder(1L, 2L, 3L); + nftTransfers.extracting(NftTransfer::getIsApproval).containsExactlyInAnyOrder(false, false, false); } private void assertAccountUpdated(Nft nft, EntityId accountId) { diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/TokenAllowanceRepositoryTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/TokenAllowanceRepositoryTest.java new file mode 100644 index 00000000000..f5f85fcefd2 --- /dev/null +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/repository/TokenAllowanceRepositoryTest.java @@ -0,0 +1,59 @@ +package com.hedera.mirror.importer.repository; + +/*- + * ‌ + * 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 java.util.List; +import javax.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.RowMapper; + +import com.hedera.mirror.common.domain.entity.TokenAllowance; + +class TokenAllowanceRepositoryTest extends AbstractRepositoryTest { + + private static final RowMapper ROW_MAPPER = rowMapper(TokenAllowance.class); + + @Resource + private TokenAllowanceRepository tokenAllowanceRepository; + + @Test + void save() { + TokenAllowance tokenAllowance = domainBuilder.tokenAllowance().persist(); + assertThat(tokenAllowanceRepository.findById(tokenAllowance.getId())).get().isEqualTo(tokenAllowance); + } + + /** + * This test verifies that the domain object and table definition are in sync with the history table. + */ + @Test + void history() { + TokenAllowance tokenAllowance = domainBuilder.tokenAllowance().persist(); + + jdbcOperations.update("insert into token_allowance_history select * from token_allowance"); + List tokenAllowanceHistory = jdbcOperations.query("select * from token_allowance_history", + ROW_MAPPER); + + assertThat(tokenAllowanceRepository.findAll()).containsExactly(tokenAllowance); + assertThat(tokenAllowanceHistory).containsExactly(tokenAllowance); + } +} 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 5b4b6fc14a5..53a80c3272d 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 @@ -28,7 +28,10 @@ import org.junit.jupiter.api.Test; import com.hedera.mirror.common.domain.contract.Contract; +import com.hedera.mirror.common.domain.entity.CryptoAllowance; import com.hedera.mirror.common.domain.entity.Entity; +import com.hedera.mirror.common.domain.entity.NftAllowance; +import com.hedera.mirror.common.domain.entity.TokenAllowance; import com.hedera.mirror.common.domain.schedule.Schedule; import com.hedera.mirror.importer.IntegrationTest; @@ -65,8 +68,7 @@ void contract() { String updatableColumns = "auto_renew_period,deleted,expiration_timestamp,key,memo,obtainer_id," + "proxy_account_id,public_key,timestamp_range"; - UpsertEntity upsertEntity = factory.createEntity(Contract.class); - assertThat(upsertEntity) + assertThat(factory.createEntity(Contract.class)) .isNotNull() .returns("contract", UpsertEntity::getTableName) .returns(true, e -> e.getUpsertable().history()) @@ -88,8 +90,7 @@ void entity() { "max_automatic_token_associations,memo,proxy_account_id,public_key,receiver_sig_required,submit_key," + "timestamp_range"; - UpsertEntity upsertEntity = factory.createEntity(Entity.class); - assertThat(upsertEntity) + assertThat(factory.createEntity(Entity.class)) .isNotNull() .returns("entity", UpsertEntity::getTableName) .returns(true, e -> e.getUpsertable().history()) @@ -100,4 +101,55 @@ void entity() { .extracting(UpsertEntity::getColumns, InstanceOfAssertFactories.ITERABLE) .hasSize(19); } + + @Test + void cryptoAllowance() { + String allColumns = "amount,owner,payer_account_id,spender,timestamp_range"; + String updatableColumns = "amount,payer_account_id,timestamp_range"; + + assertThat(factory.createEntity(CryptoAllowance.class)) + .isNotNull() + .returns("crypto_allowance", UpsertEntity::getTableName) + .returns(true, e -> e.getUpsertable().history()) + .returns("owner,spender", e -> e.columns(UpsertColumn::isId, "{0}")) + .returns("timestamp_range", e -> e.columns(UpsertColumn::isHistory, "{0}")) + .returns(allColumns, e -> e.columns("{0}")) + .returns(updatableColumns, e -> e.columns(UpsertColumn::isUpdatable, "{0}")) + .extracting(UpsertEntity::getColumns, InstanceOfAssertFactories.ITERABLE) + .hasSize(allColumns.split(",").length); + } + + @Test + void nftAllowance() { + String allColumns = "approved_for_all,owner,payer_account_id,serial_numbers,spender,timestamp_range,token_id"; + String updatableColumns = "approved_for_all,payer_account_id,serial_numbers,timestamp_range"; + + assertThat(factory.createEntity(NftAllowance.class)) + .isNotNull() + .returns("nft_allowance", UpsertEntity::getTableName) + .returns(true, e -> e.getUpsertable().history()) + .returns("owner,spender,token_id", e -> e.columns(UpsertColumn::isId, "{0}")) + .returns("timestamp_range", e -> e.columns(UpsertColumn::isHistory, "{0}")) + .returns(allColumns, e -> e.columns("{0}")) + .returns(updatableColumns, e -> e.columns(UpsertColumn::isUpdatable, "{0}")) + .extracting(UpsertEntity::getColumns, InstanceOfAssertFactories.ITERABLE) + .hasSize(allColumns.split(",").length); + } + + @Test + void tokenAllowance() { + String allColumns = "amount,owner,payer_account_id,spender,timestamp_range,token_id"; + String updatableColumns = "amount,payer_account_id,timestamp_range"; + + assertThat(factory.createEntity(TokenAllowance.class)) + .isNotNull() + .returns("token_allowance", UpsertEntity::getTableName) + .returns(true, e -> e.getUpsertable().history()) + .returns("owner,spender,token_id", e -> e.columns(UpsertColumn::isId, "{0}")) + .returns("timestamp_range", e -> e.columns(UpsertColumn::isHistory, "{0}")) + .returns(allColumns, e -> e.columns("{0}")) + .returns(updatableColumns, e -> e.columns(UpsertColumn::isUpdatable, "{0}")) + .extracting(UpsertEntity::getColumns, InstanceOfAssertFactories.ITERABLE) + .hasSize(allColumns.split(",").length); + } } diff --git a/hedera-mirror-rest/api/v1/openapi.yml b/hedera-mirror-rest/api/v1/openapi.yml index 34b58dd3257..83e64bd1a74 100644 --- a/hedera-mirror-rest/api/v1/openapi.yml +++ b/hedera-mirror-rest/api/v1/openapi.yml @@ -729,7 +729,7 @@ servers: network: default: testnet description: The Hedera network in use - enum: [ mainnet, previewnet, testnet ] + enum: [ mainnet-public, mainnet, previewnet, testnet ] components: schemas: AccountAlias: @@ -1661,6 +1661,8 @@ components: - CONTRACTDELETEINSTANCE - CONTRACTUPDATEINSTANCE - CRYPTOADDLIVEHASH + - CRYPTOADJUSTALLOWANCE + - CRYPTOAPPROVEALLOWANCE - CRYPTOCREATEACCOUNT - CRYPTODELETE - CRYPTODELETELIVEHASH diff --git a/hedera-mirror-rest/model/transactionResult.js b/hedera-mirror-rest/model/transactionResult.js index e39dc830caf..71dba34a577 100644 --- a/hedera-mirror-rest/model/transactionResult.js +++ b/hedera-mirror-rest/model/transactionResult.js @@ -265,6 +265,23 @@ const protoToName = { 281: 'MAX_STORAGE_IN_PRICE_REGIME_HAS_BEEN_USED', 282: 'INVALID_ALIAS_KEY', 283: 'UNEXPECTED_TOKEN_DECIMALS', + 284: 'INVALID_PROXY_ACCOUNT_ID', + 285: 'INVALID_TRANSFER_ACCOUNT_ID', + 286: 'INVALID_FEE_COLLECTOR_ACCOUNT_ID', + 287: 'ALIAS_IS_IMMUTABLE', + 288: 'SPENDER_ACCOUNT_SAME_AS_OWNER', + 289: 'AMOUNT_EXCEEDS_TOKEN_MAX_SUPPLY', + 290: 'NEGATIVE_ALLOWANCE_AMOUNT', + 291: 'CANNOT_APPROVE_FOR_ALL_FUNGIBLE_COMMON', + 292: 'SPENDER_DOES_NOT_HAVE_ALLOWANCE', + 293: 'AMOUNT_EXCEEDS_ALLOWANCE', + 294: 'MAX_ALLOWANCES_EXCEEDED', + 295: 'EMPTY_ALLOWANCES', + 296: 'SPENDER_ACCOUNT_REPEATED_IN_ALLOWANCES', + 297: 'REPEATED_SERIAL_NUMS_IN_NFT_ALLOWANCES', + 298: 'FUNGIBLE_TOKEN_IN_NFT_ALLOWANCES', + 299: 'NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES', + 300: 'PAYER_AND_OWNER_NOT_EQUAL', }; const nameToProto = _.invert(protoToName); diff --git a/hedera-mirror-rest/model/transactionType.js b/hedera-mirror-rest/model/transactionType.js index 28ad09dec2a..e3818d61a68 100644 --- a/hedera-mirror-rest/model/transactionType.js +++ b/hedera-mirror-rest/model/transactionType.js @@ -64,6 +64,8 @@ const protoToName = { 45: 'TOKENFEESCHEDULEUPDATE', 46: 'TOKENPAUSE', 47: 'TOKENUNPAUSE', + 48: 'CRYPTOADJUSTALLOWANCE', + 49: 'CRYPTOAPPROVEALLOWANCE', }; const UNKNOWN = 'UNKNOWN'; diff --git a/hedera-mirror-rosetta/app/domain/types/constants.go b/hedera-mirror-rosetta/app/domain/types/constants.go index e960893ded6..601e49441d0 100644 --- a/hedera-mirror-rosetta/app/domain/types/constants.go +++ b/hedera-mirror-rosetta/app/domain/types/constants.go @@ -289,6 +289,23 @@ var TransactionResults = map[int32]string{ 281: "MAX_STORAGE_IN_PRICE_REGIME_HAS_BEEN_USED", 282: "INVALID_ALIAS_KEY", 283: "UNEXPECTED_TOKEN_DECIMALS", + 284: "INVALID_PROXY_ACCOUNT_ID", + 285: "INVALID_TRANSFER_ACCOUNT_ID", + 286: "INVALID_FEE_COLLECTOR_ACCOUNT_ID", + 287: "ALIAS_IS_IMMUTABLE", + 288: "SPENDER_ACCOUNT_SAME_AS_OWNER", + 289: "AMOUNT_EXCEEDS_TOKEN_MAX_SUPPLY", + 290: "NEGATIVE_ALLOWANCE_AMOUNT", + 291: "CANNOT_APPROVE_FOR_ALL_FUNGIBLE_COMMON", + 292: "SPENDER_DOES_NOT_HAVE_ALLOWANCE", + 293: "AMOUNT_EXCEEDS_ALLOWANCE", + 294: "MAX_ALLOWANCES_EXCEEDED", + 295: "EMPTY_ALLOWANCES", + 296: "SPENDER_ACCOUNT_REPEATED_IN_ALLOWANCES", + 297: "REPEATED_SERIAL_NUMS_IN_NFT_ALLOWANCES", + 298: "FUNGIBLE_TOKEN_IN_NFT_ALLOWANCES", + 299: "NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES", + 300: "PAYER_AND_OWNER_NOT_EQUAL", } var TransactionTypes = map[int32]string{ @@ -332,6 +349,8 @@ var TransactionTypes = map[int32]string{ 45: "TOKENFEESCHEDULEUPDATE", 46: "TOKENPAUSE", 47: "TOKENUNPAUSE", + 48: "CRYPTOADJUSTALLOWANCE", + 49: "CRYPTOAPPROVEALLOWANCE", } var ( diff --git a/pom.xml b/pom.xml index 88ac41ae5e6..0859a423157 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ 2.11.0.RELEASE 1.44.0 31.0.1-jre - 0.23.0-SNAPSHOT + 0.23.0-alpha.2 2.14.0 0.8.7 11 @@ -140,6 +140,14 @@ true + + + false + + sdk-staging + SDK Staging + https://oss.sonatype.org/content/repositories/comhederahashgraph-1397 +