From 6a317325cf5e1b4da07bb26ddc1f2ffe04177dee Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Fri, 6 Sep 2024 16:28:42 -0400 Subject: [PATCH 01/10] Add outstanding token airdrops to rest api Signed-off-by: Edwin Greene --- .../{ParameterNames.java => Constants.java} | 7 +- .../common/EntityIdRangeParameter.java | 9 - .../common/IntegerRangeParameter.java | 38 +++ .../restjava/common/RangeParameter.java | 9 + .../controller/AllowancesController.java | 8 +- .../controller/TokenAirdropsController.java | 85 ++++++ .../dto/OutstandingTokenAirdropRequest.java | 43 +++ .../restjava/mapper/TokenAirdropsMapper.java | 57 ++++ .../repository/AbstractCustomRepository.java | 48 ++++ .../NftAllowanceRepositoryCustomImpl.java | 15 +- .../repository/TokenAirdropRepository.java | 23 ++ .../TokenAirdropRepositoryCustom.java | 29 ++ .../TokenAirdropRepositoryCustomImpl.java | 84 ++++++ .../service/NftAllowanceServiceImpl.java | 4 +- .../restjava/service/TokenAirdropService.java | 26 ++ .../service/TokenAirdropServiceImpl.java | 37 +++ .../common/IntegerRangeParameterTest.java | 71 +++++ .../restjava/common/LinkFactoryTest.java | 4 +- .../TokenAirdropsControllerTest.java | 238 +++++++++++++++ .../mapper/TokenAirdropsMapperTest.java | 78 +++++ .../restjava/model/TokenAirdropModelTest.java | 78 +++++ .../NftAllowanceRepositoryTest.java | 121 ++++---- .../TokenAirdropRepositoryTest.java | 272 ++++++++++++++++++ .../service/NftAllowanceServiceTest.java | 74 ++--- .../service/TokenAirdropServiceTest.java | 73 +++++ hedera-mirror-rest/api/v1/openapi.yml | 90 ++++++ 26 files changed, 1479 insertions(+), 142 deletions(-) rename hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/{ParameterNames.java => Constants.java} (76%) create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/IntegerRangeParameter.java create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/OutstandingTokenAirdropRequest.java create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/AbstractCustomRepository.java create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepository.java create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropService.java create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java create mode 100644 hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/IntegerRangeParameterTest.java create mode 100644 hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java create mode 100644 hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java create mode 100644 hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/model/TokenAirdropModelTest.java create mode 100644 hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java create mode 100644 hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/ParameterNames.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/Constants.java similarity index 76% rename from hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/ParameterNames.java rename to hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/Constants.java index bf51cf484ed..91f0d0c092a 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/ParameterNames.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/Constants.java @@ -19,8 +19,13 @@ import lombok.experimental.UtilityClass; @UtilityClass -public class ParameterNames { +public class Constants { public static final String ACCOUNT_ID = "account.id"; + public static final String RECEIVER_ID = "receiver.id"; + public static final String SERIAL_NUMBER = "serialnumber"; public static final String TOKEN_ID = "token.id"; + + public static final int MAX_LIMIT = 100; + public static final String DEFAULT_LIMIT = "25"; } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/EntityIdRangeParameter.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/EntityIdRangeParameter.java index f46ee93e558..b263f74c674 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/EntityIdRangeParameter.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/EntityIdRangeParameter.java @@ -61,13 +61,4 @@ private static EntityId getEntityId(String entityId) { default -> throw new IllegalArgumentException("Invalid entity ID: " + entityId); }; } - - // Considering EQ in the same category as GT,GTE as an assumption - public boolean hasLowerBound() { - return operator == RangeOperator.GT || operator == RangeOperator.GTE || operator == RangeOperator.EQ; - } - - public boolean hasUpperBound() { - return operator == RangeOperator.LT || operator == RangeOperator.LTE; - } } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/IntegerRangeParameter.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/IntegerRangeParameter.java new file mode 100644 index 00000000000..9eb5d278858 --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/IntegerRangeParameter.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.common; + +import org.apache.commons.lang3.StringUtils; + +public record IntegerRangeParameter(RangeOperator operator, Integer value) implements RangeParameter { + + public static final IntegerRangeParameter EMPTY = new IntegerRangeParameter(null, null); + + public static IntegerRangeParameter valueOf(String valueRangeParam) { + if (StringUtils.isBlank(valueRangeParam)) { + return EMPTY; + } + + var splitVal = valueRangeParam.split(":"); + return switch (splitVal.length) { + case 1 -> new IntegerRangeParameter(RangeOperator.EQ, Integer.valueOf(splitVal[0])); + case 2 -> new IntegerRangeParameter(RangeOperator.of(splitVal[0]), Integer.valueOf(splitVal[1])); + default -> throw new IllegalArgumentException( + "Invalid range operator %s. Should have format rangeOperator:Integer".formatted(valueRangeParam)); + }; + } +} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/RangeParameter.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/RangeParameter.java index 24b0466fa96..d2d2e971ea8 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/RangeParameter.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/RangeParameter.java @@ -21,4 +21,13 @@ public interface RangeParameter { RangeOperator operator(); T value(); + + // Considering EQ in the same category as GT,GTE as an assumption + default boolean hasLowerBound() { + return operator() == RangeOperator.GT || operator() == RangeOperator.GTE || operator() == RangeOperator.EQ; + } + + default boolean hasUpperBound() { + return operator() == RangeOperator.LT || operator() == RangeOperator.LTE; + } } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/AllowancesController.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/AllowancesController.java index 47e29e93346..70f4b8950c1 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/AllowancesController.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/AllowancesController.java @@ -16,8 +16,10 @@ package com.hedera.mirror.restjava.controller; -import static com.hedera.mirror.restjava.common.ParameterNames.ACCOUNT_ID; -import static com.hedera.mirror.restjava.common.ParameterNames.TOKEN_ID; +import static com.hedera.mirror.restjava.common.Constants.ACCOUNT_ID; +import static com.hedera.mirror.restjava.common.Constants.DEFAULT_LIMIT; +import static com.hedera.mirror.restjava.common.Constants.MAX_LIMIT; +import static com.hedera.mirror.restjava.common.Constants.TOKEN_ID; import com.google.common.collect.ImmutableSortedMap; import com.hedera.mirror.rest.model.NftAllowance; @@ -51,7 +53,6 @@ @RestController public class AllowancesController { - private static final String DEFAULT_LIMIT = "25"; private static final Map>> EXTRACTORS = Map.of( true, nftAllowance -> ImmutableSortedMap.of( @@ -61,7 +62,6 @@ public class AllowancesController { nftAllowance -> ImmutableSortedMap.of( ACCOUNT_ID, nftAllowance.getOwner(), TOKEN_ID, nftAllowance.getTokenId())); - private static final int MAX_LIMIT = 100; private final LinkFactory linkFactory; private final NftAllowanceService service; diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java new file mode 100644 index 00000000000..4f20556887f --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.controller; + +import static com.hedera.mirror.restjava.common.Constants.DEFAULT_LIMIT; +import static com.hedera.mirror.restjava.common.Constants.MAX_LIMIT; +import static com.hedera.mirror.restjava.common.Constants.RECEIVER_ID; +import static com.hedera.mirror.restjava.common.Constants.SERIAL_NUMBER; +import static com.hedera.mirror.restjava.common.Constants.TOKEN_ID; + +import com.google.common.collect.ImmutableSortedMap; +import com.hedera.mirror.rest.model.TokenAirdrop; +import com.hedera.mirror.rest.model.TokenAirdropsResponse; +import com.hedera.mirror.restjava.common.EntityIdParameter; +import com.hedera.mirror.restjava.common.EntityIdRangeParameter; +import com.hedera.mirror.restjava.common.IntegerRangeParameter; +import com.hedera.mirror.restjava.common.LinkFactory; +import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import com.hedera.mirror.restjava.mapper.TokenAirdropsMapper; +import com.hedera.mirror.restjava.service.TokenAirdropService; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Positive; +import java.util.Map; +import java.util.function.Function; +import lombok.CustomLog; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@CustomLog +@RequestMapping("/api/v1/accounts/{id}/airdrops") +@RequiredArgsConstructor +@RestController +public class TokenAirdropsController { + private static final Function> EXTRACTOR = tokenAirdrop -> ImmutableSortedMap.of( + RECEIVER_ID, tokenAirdrop.getReceiverId(), + TOKEN_ID, tokenAirdrop.getTokenId()); + + private final LinkFactory linkFactory; + private final TokenAirdropsMapper tokenMapper; + private final TokenAirdropService service; + + @GetMapping(value = "/outstanding") + TokenAirdropsResponse getOutstandingAirdrops( + @PathVariable EntityIdParameter id, + @RequestParam(defaultValue = DEFAULT_LIMIT) @Positive @Max(MAX_LIMIT) int limit, + @RequestParam(defaultValue = "asc") Sort.Direction order, + @RequestParam(name = RECEIVER_ID, required = false) EntityIdRangeParameter receiverId, + @RequestParam(name = SERIAL_NUMBER, required = false) IntegerRangeParameter serialNumber, + @RequestParam(name = TOKEN_ID, required = false) EntityIdRangeParameter tokenId) { + var request = OutstandingTokenAirdropRequest.builder() + .senderId(id) + .limit(limit) + .order(order) + .receiverId(receiverId) + .serialNumber(serialNumber) + .tokenId(tokenId) + .build(); + var response = service.getOutstandingTokenAirdrops(request); + var airdrops = tokenMapper.map(response); + var sort = Sort.by(order, RECEIVER_ID, TOKEN_ID); + var pageable = PageRequest.of(0, limit, sort); + var links = linkFactory.create(airdrops, pageable, EXTRACTOR); + return new TokenAirdropsResponse().airdrops(airdrops).links(links); + } +} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/OutstandingTokenAirdropRequest.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/OutstandingTokenAirdropRequest.java new file mode 100644 index 00000000000..aa453b835f2 --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/OutstandingTokenAirdropRequest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.dto; + +import com.hedera.mirror.restjava.common.EntityIdParameter; +import com.hedera.mirror.restjava.common.EntityIdRangeParameter; +import com.hedera.mirror.restjava.common.IntegerRangeParameter; +import lombok.Builder; +import lombok.Data; +import org.springframework.data.domain.Sort; + +@Data +@Builder +public class OutstandingTokenAirdropRequest { + + private EntityIdParameter senderId; + + @Builder.Default + private int limit = 25; + + @Builder.Default + private Sort.Direction order = Sort.Direction.ASC; + + private EntityIdRangeParameter receiverId; + + private IntegerRangeParameter serialNumber; + + private EntityIdRangeParameter tokenId; +} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java new file mode 100644 index 00000000000..f6088c3c43f --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.mapper; + +import com.hedera.mirror.common.domain.token.TokenAirdrop; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +@Mapper(config = MapperConfiguration.class) +public interface TokenAirdropsMapper { + + @Mapping(source = "receiverAccountId", target = "receiverId") + @Mapping(source = "senderAccountId", target = "senderId") + @Mapping(source = "serialNumber", target = "serialNumber", qualifiedByName = "mapToNullIfZero") + @Mapping(source = "timestampRange", target = "timestamp") + com.hedera.mirror.rest.model.TokenAirdrop map(TokenAirdrop source); + + default List map(Collection source) { + if (source == null) { + return Collections.emptyList(); + } + + List list = new ArrayList<>(source.size()); + for (TokenAirdrop tokenAirdrop : source) { + list.add(map(tokenAirdrop)); + } + + return list; + } + + @Named("mapToNullIfZero") + default Long mapToNullIfZero(long serialNumber) { + if (serialNumber == 0L) { + return null; + } + return serialNumber; + } +} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/AbstractCustomRepository.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/AbstractCustomRepository.java new file mode 100644 index 00000000000..c4fa5dd626d --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/AbstractCustomRepository.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.repository; + +import static org.jooq.impl.DSL.noCondition; + +import com.hedera.mirror.restjava.common.EntityIdRangeParameter; +import com.hedera.mirror.restjava.common.IntegerRangeParameter; +import com.hedera.mirror.restjava.common.RangeOperator; +import org.jooq.Condition; +import org.jooq.Field; + +abstract class AbstractCustomRepository { + + protected static Condition getCondition(Field field, EntityIdRangeParameter param) { + if (param == null) { + return noCondition(); + } + + return getCondition(field, param.operator(), param.value().getId()); + } + + protected static Condition getCondition(Field field, IntegerRangeParameter param) { + if (param == null) { + return noCondition(); + } + + return getCondition(field, param.operator(), param.value().longValue()); + } + + protected static Condition getCondition(Field field, RangeOperator operator, Long value) { + return operator.getFunction().apply(field, value); + } +} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java index 92f24911b84..847b3ebb7be 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java @@ -25,7 +25,6 @@ import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.NftAllowance; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.RangeOperator; import com.hedera.mirror.restjava.dto.NftAllowanceRequest; import com.hedera.mirror.restjava.service.Bound; import jakarta.inject.Named; @@ -42,7 +41,7 @@ @Named @RequiredArgsConstructor -class NftAllowanceRepositoryCustomImpl implements NftAllowanceRepositoryCustom { +class NftAllowanceRepositoryCustomImpl extends AbstractCustomRepository implements NftAllowanceRepositoryCustom { private static final Condition APPROVAL_CONDITION = NFT_ALLOWANCE.APPROVED_FOR_ALL.isTrue(); private static final Map>> SORT_ORDERS = Map.of( @@ -138,17 +137,5 @@ private Condition getMiddleCondition( return getCondition(primaryField, primaryParam.operator(), value); } - private static Condition getCondition(Field field, RangeOperator operator, Long value) { - return operator.getFunction().apply(field, value); - } - - private static Condition getCondition(Field field, EntityIdRangeParameter param) { - if (param == null) { - return noCondition(); - } - - return getCondition(field, param.operator(), param.value().getId()); - } - private record OrderSpec(boolean byOwner, Direction direction) {} } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepository.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepository.java new file mode 100644 index 00000000000..810a626864c --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.repository; + +import com.hedera.mirror.common.domain.token.AbstractTokenAirdrop.Id; +import com.hedera.mirror.common.domain.token.TokenAirdrop; +import org.springframework.data.repository.CrudRepository; + +public interface TokenAirdropRepository extends CrudRepository, TokenAirdropRepositoryCustom {} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java new file mode 100644 index 00000000000..8da76483947 --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.repository; + +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.token.TokenAirdrop; +import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import jakarta.validation.constraints.NotNull; +import java.util.Collection; + +public interface TokenAirdropRepositoryCustom { + + @NotNull + Collection findAllOutstanding(OutstandingTokenAirdropRequest request, EntityId accountId); +} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java new file mode 100644 index 00000000000..fa77f278e91 --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.repository; + +import static com.hedera.mirror.restjava.jooq.domain.Tables.TOKEN_AIRDROP; +import static org.jooq.impl.DSL.noCondition; + +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.token.TokenAirdrop; +import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import com.hedera.mirror.restjava.jooq.domain.enums.AirdropState; +import jakarta.inject.Named; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.jooq.DSLContext; +import org.jooq.SortField; +import org.springframework.data.domain.Sort.Direction; + +@Named +@RequiredArgsConstructor +class TokenAirdropRepositoryCustomImpl extends AbstractCustomRepository implements TokenAirdropRepositoryCustom { + + private final DSLContext dslContext; + private static final Map>> SORT_ORDERS = Map.of( + new OrderSpec(true, Direction.ASC), + List.of( + TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID.asc(), + TOKEN_AIRDROP.TOKEN_ID.asc(), + TOKEN_AIRDROP.SERIAL_NUMBER.asc()), + new OrderSpec(true, Direction.DESC), + List.of( + TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID.desc(), + TOKEN_AIRDROP.TOKEN_ID.desc(), + TOKEN_AIRDROP.SERIAL_NUMBER.desc()), + new OrderSpec(false, Direction.ASC), + List.of(TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID.asc(), TOKEN_AIRDROP.TOKEN_ID.asc()), + new OrderSpec(false, Direction.DESC), + List.of(TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID.desc(), TOKEN_AIRDROP.TOKEN_ID.desc())); + + @Override + public Collection findAllOutstanding(OutstandingTokenAirdropRequest request, EntityId accountId) { + var serialNumberCondition = getCondition(TOKEN_AIRDROP.SERIAL_NUMBER, request.getSerialNumber()); + var includeSerialNumber = !serialNumberCondition.equals(noCondition()); + if (includeSerialNumber) { + // If the query includes a serial number, explicitly remove fungible tokens from the result as they have a + // serial number of 0 + serialNumberCondition = serialNumberCondition.and(TOKEN_AIRDROP.SERIAL_NUMBER.ne(0L)); + } + + var condition = TOKEN_AIRDROP + .SENDER_ACCOUNT_ID + .eq(accountId.getId()) + .and(TOKEN_AIRDROP.STATE.eq(AirdropState.PENDING)) + .and(getCondition(TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID, request.getReceiverId())) + .and(getCondition(TOKEN_AIRDROP.TOKEN_ID, request.getTokenId())) + .and(serialNumberCondition); + + var order = SORT_ORDERS.get(new OrderSpec(includeSerialNumber, request.getOrder())); + return dslContext + .selectFrom(TOKEN_AIRDROP) + .where(condition) + .orderBy(order) + .limit(request.getLimit()) + .fetchInto(TokenAirdrop.class); + } + + private record OrderSpec(boolean includeSerialNumber, Direction direction) {} +} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/NftAllowanceServiceImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/NftAllowanceServiceImpl.java index 60536bce1cc..751493d33cf 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/NftAllowanceServiceImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/NftAllowanceServiceImpl.java @@ -17,7 +17,7 @@ package com.hedera.mirror.restjava.service; import com.hedera.mirror.common.domain.entity.NftAllowance; -import com.hedera.mirror.restjava.common.ParameterNames; +import com.hedera.mirror.restjava.common.Constants; import com.hedera.mirror.restjava.common.RangeOperator; import com.hedera.mirror.restjava.dto.NftAllowanceRequest; import com.hedera.mirror.restjava.repository.NftAllowanceRepository; @@ -55,7 +55,7 @@ private static void checkOwnerSpenderParamValidity(Bound ownerOrSpenderParams, B if (!ownerOrSpenderParams.hasLowerAndUpper() && tokenParams.adjustLowerBound() > tokenParams.adjustUpperBound()) { - throw new IllegalArgumentException("Invalid range provided for %s".formatted(ParameterNames.TOKEN_ID)); + throw new IllegalArgumentException("Invalid range provided for %s".formatted(Constants.TOKEN_ID)); } if (tokenParams.getCardinality(RangeOperator.LT, RangeOperator.LTE) > 0 diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropService.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropService.java new file mode 100644 index 00000000000..01713b6c1d8 --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropService.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.service; + +import com.hedera.mirror.common.domain.token.TokenAirdrop; +import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import java.util.Collection; + +public interface TokenAirdropService { + + Collection getOutstandingTokenAirdrops(OutstandingTokenAirdropRequest request); +} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java new file mode 100644 index 00000000000..f5972501946 --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.service; + +import com.hedera.mirror.common.domain.token.TokenAirdrop; +import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import com.hedera.mirror.restjava.repository.TokenAirdropRepository; +import jakarta.inject.Named; +import java.util.Collection; +import lombok.RequiredArgsConstructor; + +@Named +@RequiredArgsConstructor +public class TokenAirdropServiceImpl implements TokenAirdropService { + + private final EntityService entityService; + private final TokenAirdropRepository repository; + + public Collection getOutstandingTokenAirdrops(OutstandingTokenAirdropRequest request) { + var id = entityService.lookup(request.getSenderId()); + return repository.findAllOutstanding(request, id); + } +} diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/IntegerRangeParameterTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/IntegerRangeParameterTest.java new file mode 100644 index 00000000000..36aeff212ca --- /dev/null +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/IntegerRangeParameterTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import com.hedera.mirror.restjava.RestJavaProperties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class IntegerRangeParameterTest { + + @Mock + private RestJavaProperties properties; + + private MockedStatic context; + + @BeforeEach + void setUp() { + context = Mockito.mockStatic(SpringApplicationContext.class); + when(SpringApplicationContext.getBean(RestJavaProperties.class)).thenReturn(properties); + } + + @AfterEach + void closeMocks() { + context.close(); + } + + @Test + void testConversion() { + assertThat(new IntegerRangeParameter(RangeOperator.GTE, 2000)) + .isEqualTo(IntegerRangeParameter.valueOf("gte:2000")); + assertThat(new IntegerRangeParameter(RangeOperator.EQ, 2000)).isEqualTo(IntegerRangeParameter.valueOf("2000")); + assertThat(IntegerRangeParameter.EMPTY) + .isEqualTo(IntegerRangeParameter.valueOf("")) + .isEqualTo(IntegerRangeParameter.valueOf(null)); + } + + @ParameterizedTest + @ValueSource(strings = {"a", ".1", "someinvalidstring"}) + @DisplayName("IntegerRangeParameter parse from string tests, negative cases") + void testInvalidParam(String input) { + assertThrows(IllegalArgumentException.class, () -> IntegerRangeParameter.valueOf(input)); + } +} diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/LinkFactoryTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/LinkFactoryTest.java index 61d1ed43a38..8c983ea38ea 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/LinkFactoryTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/LinkFactoryTest.java @@ -16,8 +16,8 @@ package com.hedera.mirror.restjava.common; -import static com.hedera.mirror.restjava.common.ParameterNames.ACCOUNT_ID; -import static com.hedera.mirror.restjava.common.ParameterNames.TOKEN_ID; +import static com.hedera.mirror.restjava.common.Constants.ACCOUNT_ID; +import static com.hedera.mirror.restjava.common.Constants.TOKEN_ID; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.Mockito.when; diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java new file mode 100644 index 00000000000..15f52063016 --- /dev/null +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.controller; + +import static com.hedera.mirror.common.domain.token.TokenTypeEnum.FUNGIBLE_COMMON; +import static com.hedera.mirror.common.domain.token.TokenTypeEnum.NON_FUNGIBLE_UNIQUE; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.io.BaseEncoding; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.token.TokenAirdrop; +import com.hedera.mirror.common.util.DomainUtils; +import com.hedera.mirror.rest.model.Links; +import com.hedera.mirror.rest.model.TokenAirdropsResponse; +import com.hedera.mirror.restjava.mapper.TokenAirdropsMapper; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.web.client.RestClient.RequestHeadersSpec; +import org.springframework.web.client.RestClient.RequestHeadersUriSpec; + +@RequiredArgsConstructor +class TokenAirdropsControllerTest extends ControllerTest { + + private final TokenAirdropsMapper mapper; + + @DisplayName("/api/v1/accounts/{id}/airdrops/outstanding") + @Nested + class OutstandingTokenAirdropsEndpointTest extends EndpointTest { + + @Override + protected String getUrl() { + return "accounts/{id}/airdrops/outstanding"; + } + + @Override + protected RequestHeadersSpec defaultRequest(RequestHeadersUriSpec uriSpec) { + var tokenAirdrop = domainBuilder.tokenAirdrop(FUNGIBLE_COMMON).persist(); + return uriSpec.uri("", tokenAirdrop.getSenderAccountId()); + } + + @ValueSource(strings = {"1000", "0.1000", "0.0.1000"}) + @ParameterizedTest + void outstandingAirdropByEntityId(String id) { + // Given + var tokenAirdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(1000L)) + .persist(); + + // When + var response = restClient.get().uri("", id).retrieve().toEntity(TokenAirdropsResponse.class); + + // Then + assertThat(response.getBody().getAirdrops().getFirst()).isEqualTo(mapper.map(tokenAirdrop)); + // Based on application.yml response headers configuration + assertThat(response.getHeaders().getAccessControlAllowOrigin()).isEqualTo("*"); + assertThat(response.getHeaders().getCacheControl()).isEqualTo("public, max-age=1"); + } + + @Test + void evmAddressOutstanding() { + // Given + var entity = domainBuilder.entity().persist(); + var tokenAirdrop = domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> a.senderAccountId(entity.getId())) + .persist(); + + // When + var response = restClient + .get() + .uri("", DomainUtils.bytesToHex(entity.getEvmAddress())) + .retrieve() + .toEntity(TokenAirdropsResponse.class); + + // Then + assertThat(response.getBody().getAirdrops().getFirst()).isEqualTo(mapper.map(tokenAirdrop)); + } + + @Test + void aliasOutstanding() { + // Given + var entity = domainBuilder.entity().persist(); + var tokenAirdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId())) + .persist(); + + // When + var response = restClient + .get() + .uri("", BaseEncoding.base32().omitPadding().encode(entity.getAlias())) + .retrieve() + .toEntity(TokenAirdropsResponse.class); + + // Then + assertThat(response.getBody().getAirdrops().getFirst()).isEqualTo(mapper.map(tokenAirdrop)); + } + + @Test + void followAscendingOrderLinkOutstanding() { + // Given + var entity = domainBuilder.entity().persist(); + var id = entity.getId(); + var tokenAirdrop1 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId())) + .persist(); + var tokenAirdrop2 = domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> a.senderAccountId(entity.getId())) + .persist(); + var baseLink = "/api/v1/accounts/%d/airdrops/outstanding".formatted(id); + + // When + var result = restClient.get().uri("?limit=1", id).retrieve().body(TokenAirdropsResponse.class); + var nextParams = "?limit=1&receiver.id=gte:%s&token.id=gt:%s" + .formatted( + EntityId.of(tokenAirdrop1.getReceiverAccountId()), EntityId.of(tokenAirdrop1.getTokenId())); + + // Then + assertThat(result).isEqualTo(getExpectedResponse(List.of(tokenAirdrop1), baseLink + nextParams)); + + // When follow link + result = restClient.get().uri(nextParams, id).retrieve().body(TokenAirdropsResponse.class); + + // Then + nextParams = "?limit=1&receiver.id=gte:%s&token.id=gt:%s" + .formatted( + EntityId.of(tokenAirdrop2.getReceiverAccountId()), EntityId.of(tokenAirdrop2.getTokenId())); + assertThat(result).isEqualTo(getExpectedResponse(List.of(tokenAirdrop2), baseLink + nextParams)); + + // When follow link 2 + result = restClient + .get() + .uri(nextParams, tokenAirdrop1.getReceiverAccountId()) + .retrieve() + .body(TokenAirdropsResponse.class); + // Then + assertThat(result).isEqualTo(getExpectedResponse(List.of(), null)); + } + + @Test + void followDescendingOrderLinkOutstanding() { + // Given + long sender = 1000; + long receiver = 2000; + long fungibleTokenId = 100; + long token1 = 300; + long token2 = 301; + long serial1 = 100; + + var tokenAirdrop1 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(sender) + .receiverAccountId(receiver) + .tokenId(fungibleTokenId)) + .persist(); + + var nftAirdrop = domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> a.senderAccountId(sender) + .receiverAccountId(receiver) + .tokenId(token1) + .serialNumber(serial1)) + .persist(); + var nftAirdrop2 = domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> a.senderAccountId(sender) + .receiverAccountId(receiver) + .tokenId(token2) + .serialNumber(serial1)) + .persist(); + domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> a.receiverAccountId(receiver)) + .persist(); + + var uriParams = "?limit=1&receiver.id=gte:%s&order=desc".formatted(receiver); + var baseLink = "/api/v1/accounts/%d/airdrops/outstanding".formatted(sender); + + // When + var result = restClient.get().uri(uriParams, sender).retrieve().body(TokenAirdropsResponse.class); + // The first receiver id is '2000' instead of 0.0.2000 because the link creation does not alter the original + // value sent in the request + // The second receiver id is added by the link generator and has shard.realm.num format + var nextParams = "?limit=1&receiver.id=gte:2000&receiver.id=lte:0.0.2000&order=desc&token.id=lt:0.0.301"; + + // Then + assertThat(result).isEqualTo(getExpectedResponse(List.of(nftAirdrop2), baseLink + nextParams)); + + // When follow link + result = restClient.get().uri(nextParams, sender).retrieve().body(TokenAirdropsResponse.class); + + // Then + nextParams = "?limit=1&receiver.id=gte:2000&receiver.id=lte:0.0.2000&order=desc&token.id=lt:0.0.300"; + assertThat(result).isEqualTo(getExpectedResponse(List.of(nftAirdrop), baseLink + nextParams)); + + // When follow link + result = restClient.get().uri(nextParams, sender).retrieve().body(TokenAirdropsResponse.class); + + // Then + nextParams = "?limit=1&receiver.id=gte:2000&receiver.id=lte:0.0.2000&order=desc&token.id=lt:0.0.100"; + assertThat(result).isEqualTo(getExpectedResponse(List.of(tokenAirdrop1), baseLink + nextParams)); + + // When follow link + result = restClient.get().uri(nextParams, sender).retrieve().body(TokenAirdropsResponse.class); + + // Then + assertThat(result).isEqualTo(getExpectedResponse(List.of(), null)); + } + + private TokenAirdropsResponse getExpectedResponse(List tokenAirdrops, String next) { + return new TokenAirdropsResponse() + .airdrops(mapper.map(tokenAirdrops)) + .links(new Links().next(next)); + } + } +} diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java new file mode 100644 index 00000000000..4e9fbda42c1 --- /dev/null +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.mapper; + +import static com.hedera.mirror.common.domain.token.TokenTypeEnum.FUNGIBLE_COMMON; +import static com.hedera.mirror.common.domain.token.TokenTypeEnum.NON_FUNGIBLE_UNIQUE; +import static org.assertj.core.api.Assertions.assertThat; + +import com.hedera.mirror.common.domain.DomainBuilder; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.common.domain.token.TokenTypeEnum; +import com.hedera.mirror.rest.model.TimestampRange; +import com.hedera.mirror.rest.model.TokenAirdrop; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class TokenAirdropsMapperTest { + + private CommonMapper commonMapper; + private DomainBuilder domainBuilder; + private TokenAirdropsMapper mapper; + + @BeforeEach + void setup() { + commonMapper = new CommonMapperImpl(); + mapper = new TokenAirdropsMapperImpl(commonMapper); + domainBuilder = new DomainBuilder(); + } + + @ParameterizedTest + @EnumSource(TokenTypeEnum.class) + void map(TokenTypeEnum tokenType) { + var tokenAirdrop = domainBuilder.tokenAirdrop(tokenType).get(); + var to = commonMapper.mapTimestamp(tokenAirdrop.getTimestampLower()); + + assertThat(mapper.map(List.of(tokenAirdrop))) + .first() + .returns(tokenType == NON_FUNGIBLE_UNIQUE ? null : tokenAirdrop.getAmount(), TokenAirdrop::getAmount) + .returns(EntityId.of(tokenAirdrop.getReceiverAccountId()).toString(), TokenAirdrop::getReceiverId) + .returns(EntityId.of(tokenAirdrop.getSenderAccountId()).toString(), TokenAirdrop::getSenderId) + .returns( + tokenType == FUNGIBLE_COMMON ? null : tokenAirdrop.getSerialNumber(), + TokenAirdrop::getSerialNumber) + .returns(EntityId.of(tokenAirdrop.getTokenId()).toString(), TokenAirdrop::getTokenId) + .satisfies(a -> assertThat(a.getTimestamp()) + .returns(to, TimestampRange::getFrom) + .returns(null, TimestampRange::getTo)); + } + + @Test + void mapNulls() { + var tokenAirdrop = new com.hedera.mirror.common.domain.token.TokenAirdrop(); + assertThat(mapper.map(tokenAirdrop)) + .returns(null, TokenAirdrop::getAmount) + .returns(null, TokenAirdrop::getReceiverId) + .returns(null, TokenAirdrop::getSenderId) + .returns(null, TokenAirdrop::getSerialNumber) + .returns(null, TokenAirdrop::getTokenId) + .returns(null, TokenAirdrop::getTimestamp); + } +} diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/model/TokenAirdropModelTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/model/TokenAirdropModelTest.java new file mode 100644 index 00000000000..a9e4213ec51 --- /dev/null +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/model/TokenAirdropModelTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hedera.mirror.rest.model.TokenAirdropsResponse; +import org.junit.jupiter.api.Test; + +class TokenAirdropModelTest { + String airdropsResponse = + """ + { + "airdrops": [ + { + "amount": 333, + "receiver_id": "0.0.999", + "sender_id": "0.0.222", + "serial_number": null, + "timestamp": { + "from": "1111111111.111111111", + "to": null + }, + "token_id": "0.0.111" + }, + { + "amount": 555, + "receiver_id": "0.0.999", + "sender_id": "0.0.222", + "serial_number": null, + "timestamp": { + "from": "1111111111.111111112", + "to": null + }, + "token_id": "0.0.444" + }, + { + "amount": null, + "receiver_id": "0.0.999", + "sender_id": "0.0.222", + "serial_number": 888, + "timestamp": { + "from": "1111111111.111111113", + "to": null + }, + "token_id": "0.0.666" + } + ], + "links": { + "next": "/api/v1/accounts/0.0.1000/airdrops/outstanding?limit=3&order=asc&token.id=gt:0.0.667" + } + } + """; + + @Test + void verifyModelGeneration() throws JsonProcessingException { + var mapper = new ObjectMapper(); + var response = mapper.readValue(airdropsResponse, TokenAirdropsResponse.class); + var tokenAirdrop = mapper.writeValueAsString(response); + assertThat(tokenAirdrop).isEqualToIgnoringWhitespace(airdropsResponse); + } +} diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryTest.java index 6d3ef612317..08df0fea5cb 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryTest.java @@ -26,9 +26,9 @@ import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.NftAllowance; import com.hedera.mirror.restjava.RestJavaIntegrationTest; +import com.hedera.mirror.restjava.common.Constants; import com.hedera.mirror.restjava.common.EntityIdNumParameter; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.ParameterNames; import com.hedera.mirror.restjava.common.RangeOperator; import com.hedera.mirror.restjava.dto.NftAllowanceRequest; import com.hedera.mirror.restjava.service.Bound; @@ -74,8 +74,8 @@ void findAllNoMatch() { NftAllowanceRequest.builder() .isOwner(true) .accountId(new EntityIdNumParameter(EntityId.of(owners.get(2) + 1))) - .ownerOrSpenderIds(new Bound(List.of(), false, ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds(new Bound(List.of(), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .limit(10) .order(Direction.ASC) .build(), @@ -90,8 +90,8 @@ void findAllNoMatch() { .ownerOrSpenderIds(new Bound( List.of(new EntityIdRangeParameter(EQ, EntityId.of(spenders.get(2) + 1))), false, - ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .limit(10) .order(Direction.ASC) .build(), @@ -103,12 +103,12 @@ void findAllNoMatch() { NftAllowanceRequest.builder() .isOwner(true) .accountId(new EntityIdNumParameter(EntityId.of(owners.get(0)))) - .ownerOrSpenderIds(new Bound( - List.of(fromIndex(EQ, spenders, 0)), false, ParameterNames.ACCOUNT_ID)) + .ownerOrSpenderIds( + new Bound(List.of(fromIndex(EQ, spenders, 0)), false, Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(EQ, EntityId.of(tokenIds.get(2) + 1))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .limit(10) .order(Direction.ASC) .build(), @@ -123,11 +123,11 @@ void findAllNoMatch() { .ownerOrSpenderIds(new Bound( List.of(new EntityIdRangeParameter(GT, EntityId.of(spenders.get(2)))), false, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(GT, EntityId.of(tokenIds.get(0)))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .limit(10) .order(Direction.ASC) .build(), @@ -142,11 +142,11 @@ void findAllNoMatch() { .ownerOrSpenderIds(new Bound( List.of(new EntityIdRangeParameter(LT, EntityId.of(spenders.get(0)))), false, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(LT, EntityId.of(tokenIds.get(2)))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .limit(10) .order(Direction.ASC) .build(), @@ -190,8 +190,8 @@ private void populateTestSpecs() { NftAllowanceRequest.builder() .isOwner(true) .accountId(new EntityIdNumParameter(EntityId.of(owners.get(0)))) - .ownerOrSpenderIds(new Bound(List.of(), false, ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds(new Bound(List.of(), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -201,8 +201,8 @@ private void populateTestSpecs() { NftAllowanceRequest.builder() .isOwner(true) .accountId(new EntityIdNumParameter(EntityId.of(owners.get(0)))) - .ownerOrSpenderIds(new Bound(List.of(), false, ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds(new Bound(List.of(), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.DESC) .build(), @@ -212,8 +212,8 @@ private void populateTestSpecs() { NftAllowanceRequest.builder() .isOwner(false) .accountId(new EntityIdNumParameter(EntityId.of(spenders.get(1)))) - .ownerOrSpenderIds(new Bound(List.of(), false, ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds(new Bound(List.of(), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -223,8 +223,8 @@ private void populateTestSpecs() { NftAllowanceRequest.builder() .isOwner(false) .accountId(new EntityIdNumParameter(EntityId.of(spenders.get(1)))) - .ownerOrSpenderIds(new Bound(List.of(), false, ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds(new Bound(List.of(), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.DESC) .build(), @@ -234,9 +234,9 @@ private void populateTestSpecs() { NftAllowanceRequest.builder() .isOwner(true) .accountId(new EntityIdNumParameter(EntityId.of(owners.get(1)))) - .ownerOrSpenderIds(new Bound( - List.of(fromIndex(EQ, spenders, 0)), false, ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds( + new Bound(List.of(fromIndex(EQ, spenders, 0)), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -246,9 +246,9 @@ private void populateTestSpecs() { NftAllowanceRequest.builder() .isOwner(true) .accountId(new EntityIdNumParameter(EntityId.of(owners.get(1)))) - .ownerOrSpenderIds(new Bound( - List.of(fromIndex(GTE, spenders, 0)), false, ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds( + new Bound(List.of(fromIndex(GTE, spenders, 0)), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -258,10 +258,9 @@ private void populateTestSpecs() { NftAllowanceRequest.builder() .isOwner(true) .accountId(new EntityIdNumParameter(EntityId.of(owners.get(1)))) - .ownerOrSpenderIds(new Bound( - List.of(fromIndex(GTE, spenders, 0)), false, ParameterNames.ACCOUNT_ID)) - .tokenIds( - new Bound(List.of(fromIndex(GTE, tokenIds, 0)), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds( + new Bound(List.of(fromIndex(GTE, spenders, 0)), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(fromIndex(GTE, tokenIds, 0)), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -271,10 +270,9 @@ private void populateTestSpecs() { NftAllowanceRequest.builder() .isOwner(true) .accountId(new EntityIdNumParameter(EntityId.of(owners.get(1)))) - .ownerOrSpenderIds(new Bound( - List.of(fromIndex(GTE, spenders, 0)), false, ParameterNames.ACCOUNT_ID)) - .tokenIds( - new Bound(List.of(fromIndex(GT, tokenIds, 1)), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds( + new Bound(List.of(fromIndex(GTE, spenders, 0)), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(fromIndex(GT, tokenIds, 1)), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -284,10 +282,9 @@ private void populateTestSpecs() { NftAllowanceRequest.builder() .isOwner(true) .accountId(new EntityIdNumParameter(EntityId.of(owners.get(1)))) - .ownerOrSpenderIds(new Bound( - List.of(fromIndex(LTE, spenders, 2)), false, ParameterNames.ACCOUNT_ID)) - .tokenIds( - new Bound(List.of(fromIndex(LT, tokenIds, 2)), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds( + new Bound(List.of(fromIndex(LTE, spenders, 2)), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(fromIndex(LT, tokenIds, 2)), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.DESC) .build(), @@ -300,11 +297,11 @@ private void populateTestSpecs() { .ownerOrSpenderIds(new Bound( List.of(fromIndex(LTE, spenders, 2), fromIndex(GTE, spenders, 0)), false, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(fromIndex(LT, tokenIds, 2), fromIndex(GT, tokenIds, 0)), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -317,9 +314,8 @@ private void populateTestSpecs() { .ownerOrSpenderIds(new Bound( List.of(fromIndex(LTE, spenders, 2), fromIndex(GT, spenders, 0)), false, - ParameterNames.ACCOUNT_ID)) - .tokenIds( - new Bound(List.of(fromIndex(LTE, tokenIds, 0)), false, ParameterNames.TOKEN_ID)) + Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(fromIndex(LTE, tokenIds, 0)), false, Constants.TOKEN_ID)) .limit(6) .order(Direction.DESC) .build(), @@ -329,10 +325,9 @@ private void populateTestSpecs() { NftAllowanceRequest.builder() .isOwner(true) .accountId(new EntityIdNumParameter(EntityId.of(owners.get(1)))) - .ownerOrSpenderIds(new Bound( - List.of(fromIndex(EQ, spenders, 0)), false, ParameterNames.ACCOUNT_ID)) - .tokenIds( - new Bound(List.of(fromIndex(EQ, tokenIds, 1)), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds( + new Bound(List.of(fromIndex(EQ, spenders, 0)), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(fromIndex(EQ, tokenIds, 1)), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -345,9 +340,8 @@ private void populateTestSpecs() { .ownerOrSpenderIds(new Bound( List.of(fromIndex(GTE, spenders, 1), fromIndex(LTE, spenders, 1)), false, - ParameterNames.ACCOUNT_ID)) - .tokenIds( - new Bound(List.of(fromIndex(EQ, tokenIds, 0)), false, ParameterNames.TOKEN_ID)) + Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(fromIndex(EQ, tokenIds, 0)), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -360,9 +354,8 @@ private void populateTestSpecs() { .ownerOrSpenderIds(new Bound( List.of(fromIndex(GTE, spenders, 1), fromIndex(LTE, spenders, 1)), false, - ParameterNames.ACCOUNT_ID)) - .tokenIds( - new Bound(List.of(fromIndex(GTE, tokenIds, 0)), false, ParameterNames.TOKEN_ID)) + Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(fromIndex(GTE, tokenIds, 0)), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -375,9 +368,8 @@ private void populateTestSpecs() { .ownerOrSpenderIds(new Bound( List.of(fromIndex(GT, spenders, 0), fromIndex(LT, spenders, 2)), false, - ParameterNames.ACCOUNT_ID)) - .tokenIds( - new Bound(List.of(fromIndex(GTE, tokenIds, 0)), false, ParameterNames.TOKEN_ID)) + Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(fromIndex(GTE, tokenIds, 0)), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -392,8 +384,8 @@ private void populateTestSpecs() { new EntityIdRangeParameter(GT, EntityId.of(spenders.get(0) - 1)), new EntityIdRangeParameter(LT, EntityId.of(spenders.get(2) + 1))), false, - ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -406,9 +398,8 @@ private void populateTestSpecs() { .ownerOrSpenderIds(new Bound( List.of(fromIndex(GTE, spenders, 0), fromIndex(LTE, spenders, 2)), false, - ParameterNames.ACCOUNT_ID)) - .tokenIds( - new Bound(List.of(fromIndex(EQ, tokenIds, 0)), false, ParameterNames.TOKEN_ID)) + Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(fromIndex(EQ, tokenIds, 0)), false, Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -421,11 +412,11 @@ private void populateTestSpecs() { .ownerOrSpenderIds(new Bound( List.of(fromIndex(GTE, spenders, 0), fromIndex(LTE, spenders, 2)), false, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(fromIndex(GTE, tokenIds, 1), fromIndex(LTE, tokenIds, 1)), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), @@ -438,11 +429,11 @@ private void populateTestSpecs() { .ownerOrSpenderIds(new Bound( List.of(fromIndex(GTE, spenders, 0), fromIndex(LTE, spenders, 2)), false, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(fromIndex(GT, tokenIds, 0), fromIndex(LT, tokenIds, 2)), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .limit(4) .order(Direction.ASC) .build(), diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java new file mode 100644 index 00000000000..59161abcf87 --- /dev/null +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.repository; + +import static com.hedera.mirror.common.domain.token.TokenTypeEnum.FUNGIBLE_COMMON; +import static com.hedera.mirror.common.domain.token.TokenTypeEnum.NON_FUNGIBLE_UNIQUE; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.Range; +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.restjava.RestJavaIntegrationTest; +import com.hedera.mirror.restjava.common.EntityIdNumParameter; +import com.hedera.mirror.restjava.common.EntityIdRangeParameter; +import com.hedera.mirror.restjava.common.IntegerRangeParameter; +import com.hedera.mirror.restjava.common.RangeOperator; +import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.data.domain.Sort.Direction; + +@RequiredArgsConstructor +class TokenAirdropRepositoryTest extends RestJavaIntegrationTest { + + private final TokenAirdropRepository repository; + + @Test + void findById() { + var tokenAirdrop = domainBuilder.tokenAirdrop(FUNGIBLE_COMMON).persist(); + assertThat(repository.findById(tokenAirdrop.getId())).get().isEqualTo(tokenAirdrop); + } + + @Test + void findBySenderId() { + var tokenAirdrop = domainBuilder.tokenAirdrop(FUNGIBLE_COMMON).persist(); + var entityId = EntityId.of(tokenAirdrop.getSenderAccountId()); + var request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(entityId)) + .build(); + assertThat(repository.findAllOutstanding(request, entityId)).contains(tokenAirdrop); + } + + @ParameterizedTest + @EnumSource(Direction.class) + void findBySenderIdOrder(Direction order) { + var tokenAirdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.timestampRange(Range.atLeast(1000L))) + .persist(); + var tokenAirdrop2 = domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> + a.senderAccountId(tokenAirdrop.getSenderAccountId()).timestampRange(Range.atLeast(2000L))) + .persist(); + var entityId = EntityId.of(tokenAirdrop.getSenderAccountId()); + var outstandingTokenAirdropRequest = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(entityId)) + .order(order) + .build(); + + var expected = + order.isAscending() ? List.of(tokenAirdrop, tokenAirdrop2) : List.of(tokenAirdrop2, tokenAirdrop); + assertThat(repository.findAllOutstanding(outstandingTokenAirdropRequest, entityId)) + .containsExactlyElementsOf(expected); + } + + @Test + void noMatch() { + var tokenAirdrop = domainBuilder.tokenAirdrop(FUNGIBLE_COMMON).persist(); + var entityId = EntityId.of(tokenAirdrop.getSenderAccountId()); + var request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(entityId)) + .receiverId( + new EntityIdRangeParameter(RangeOperator.GT, EntityId.of(tokenAirdrop.getReceiverAccountId()))) + .build(); + assertThat(repository.findAllOutstanding(request, entityId)).isEmpty(); + } + + @ParameterizedTest + @EnumSource(Direction.class) + void conditionalClauses(Direction order) { + var sender = domainBuilder.entity().get(); + var receiver = domainBuilder.entity().get(); + var token = domainBuilder.token().get(); + var serialNumber = 5; + + var tokenAirdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(sender.getId()).receiverAccountId(receiver.getId())) + .persist(); + var tokenAirdrop2 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(sender.getId()).tokenId(token.getTokenId())) + .persist(); + var nftAirdrop = domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> a.senderAccountId(sender.getId()) + .receiverAccountId(receiver.getId()) + .serialNumber(serialNumber) + .tokenId(token.getTokenId())) + .persist(); + var nftAirdrop2 = domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> a.senderAccountId(sender.getId()).serialNumber(serialNumber)) + .persist(); + + // Default asc ordering by receiver, tokenId + var allAirdrops = List.of(nftAirdrop, tokenAirdrop, tokenAirdrop2, nftAirdrop2); + var receiverSpecifiedAirdrops = List.of(nftAirdrop, tokenAirdrop); + var tokenSpecifiedAirdrops = List.of(nftAirdrop, tokenAirdrop2); + var serialNumberAirdrops = List.of(nftAirdrop, nftAirdrop2); + + var orderedAirdrops = order.isAscending() ? allAirdrops : allAirdrops.reversed(); + var request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(sender.toEntityId())) + .order(order) + .build(); + assertThat(repository.findAllOutstanding(request, sender.toEntityId())) + .containsExactlyElementsOf(orderedAirdrops); + + // With token id condition + var tokenIdAirdrops = order.isAscending() ? tokenSpecifiedAirdrops : tokenSpecifiedAirdrops.reversed(); + request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(sender.toEntityId())) + .order(order) + .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(token.getTokenId()))) + .build(); + assertThat(repository.findAllOutstanding(request, sender.toEntityId())) + .containsExactlyElementsOf(tokenIdAirdrops); + + // With receiver id condition + var receiverIdAirdrops = order.isAscending() ? receiverSpecifiedAirdrops : receiverSpecifiedAirdrops.reversed(); + request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(sender.toEntityId())) + .order(order) + .receiverId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver.getId()))) + .build(); + assertThat(repository.findAllOutstanding(request, sender.toEntityId())) + .containsExactlyElementsOf(receiverIdAirdrops); + + // With serial number condition + var serialNumberAirdropsOrdered = order.isAscending() ? serialNumberAirdrops : serialNumberAirdrops.reversed(); + request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(sender.toEntityId())) + .order(order) + .serialNumber(new IntegerRangeParameter(RangeOperator.EQ, serialNumber)) + .build(); + assertThat(repository.findAllOutstanding(request, sender.toEntityId())) + .containsExactlyElementsOf(serialNumberAirdropsOrdered); + } + + @Test + void serialNumber() { + var sender = 1000; + var receiver = 3000; + var receiver2 = 4000; + var tokenId = 5000; + var nftTokenId = 6000; + var nftTokenId2 = 7000; + var serialNumber = 5; + var serialNumber2 = 10; + + var airdrop1 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> + a.senderAccountId(sender).receiverAccountId(receiver).tokenId(tokenId)) + .persist(); + var serialAirdrop1 = domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> a.senderAccountId(sender) + .receiverAccountId(receiver) + .serialNumber(serialNumber) + .tokenId(nftTokenId)) + .persist(); + var serialAirdrop2 = domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> a.senderAccountId(sender) + .receiverAccountId(receiver) + .serialNumber(serialNumber2) + .tokenId(nftTokenId)) + .persist(); + var serialAirdrop3 = domainBuilder + .tokenAirdrop(NON_FUNGIBLE_UNIQUE) + .customize(a -> a.senderAccountId(sender) + .receiverAccountId(receiver2) + .serialNumber(serialNumber) + .tokenId(nftTokenId2)) + .persist(); + + // serialNumber gt 4 -> serialAirdrop1, serialAirdrop2, serialAirdrop3 + var expectedAirdrops = List.of(serialAirdrop1, serialAirdrop2, serialAirdrop3); + var request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(EntityId.of(sender))) + .serialNumber(new IntegerRangeParameter(RangeOperator.GT, 4)) + .build(); + assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) + .containsExactlyElementsOf(expectedAirdrops); + + // serialNumber lt 10 -> serialAirdrop1, serialAirdrop3 + expectedAirdrops = List.of(serialAirdrop1, serialAirdrop3); + request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(EntityId.of(sender))) + .serialNumber(new IntegerRangeParameter(RangeOperator.LT, 10)) + .build(); + assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) + .containsExactlyElementsOf(expectedAirdrops); + + // tokenId eq nftTokenId -> serialAirdrop1, serialAirdrop2 + expectedAirdrops = List.of(serialAirdrop1, serialAirdrop2); + request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(EntityId.of(sender))) + .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(nftTokenId))) + .build(); + assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) + .containsExactlyElementsOf(expectedAirdrops); + + // tokenId eq nftTokenId && serialNumber lte 5 -> serialAirdrop1 + expectedAirdrops = List.of(serialAirdrop1); + request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(EntityId.of(sender))) + .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(nftTokenId))) + .serialNumber(new IntegerRangeParameter(RangeOperator.LTE, 5)) + .build(); + assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) + .containsExactlyElementsOf(expectedAirdrops); + + // tokenId eq nftTokenId && serialNumber gte 10 -> serialAirdrop2 + expectedAirdrops = List.of(serialAirdrop2); + request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(EntityId.of(sender))) + .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(nftTokenId))) + .serialNumber(new IntegerRangeParameter(RangeOperator.GTE, 10)) + .build(); + assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) + .containsExactlyElementsOf(expectedAirdrops); + + // receiver eq 3000 -> airdrop1, serialAirdrop1, serialAirdrop2 + expectedAirdrops = List.of(airdrop1, serialAirdrop1, serialAirdrop2); + request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(EntityId.of(sender))) + .receiverId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver))) + .build(); + assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) + .containsExactlyElementsOf(expectedAirdrops); + + // receiver eq 3000 && serialNumber lte 5 -> serialAirdrop1 + expectedAirdrops = List.of(serialAirdrop1); + request = OutstandingTokenAirdropRequest.builder() + .senderId(new EntityIdNumParameter(EntityId.of(sender))) + .receiverId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver))) + .serialNumber(new IntegerRangeParameter(RangeOperator.LTE, 5)) + .build(); + assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) + .containsExactlyElementsOf(expectedAirdrops); + } +} diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/NftAllowanceServiceTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/NftAllowanceServiceTest.java index da96a781e7f..a8ff4fca16d 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/NftAllowanceServiceTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/NftAllowanceServiceTest.java @@ -22,11 +22,11 @@ import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.NftAllowance; import com.hedera.mirror.restjava.RestJavaIntegrationTest; +import com.hedera.mirror.restjava.common.Constants; import com.hedera.mirror.restjava.common.EntityIdAliasParameter; import com.hedera.mirror.restjava.common.EntityIdEvmAddressParameter; import com.hedera.mirror.restjava.common.EntityIdNumParameter; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.ParameterNames; import com.hedera.mirror.restjava.common.RangeOperator; import com.hedera.mirror.restjava.dto.NftAllowanceRequest; import java.util.List; @@ -54,8 +54,8 @@ void getNftAllowancesForOrderAsc(boolean owner) { .isOwner(owner) .limit(2) .accountId(new EntityIdNumParameter(ACCOUNT_ID)) - .ownerOrSpenderIds(new Bound(List.of(), false, ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + .ownerOrSpenderIds(new Bound(List.of(), false, Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); var response = service.getNftAllowances(request); @@ -75,13 +75,9 @@ void getNftAllowancesWithAlias() { .limit(2) .accountId(new EntityIdAliasParameter(0, 0, entity.getAlias())) .ownerOrSpenderIds(new Bound( - List.of(new EntityIdRangeParameter(RangeOperator.GTE, accountId)), - true, - ParameterNames.ACCOUNT_ID)) + List.of(new EntityIdRangeParameter(RangeOperator.GTE, accountId)), true, Constants.ACCOUNT_ID)) .tokenIds(new Bound( - List.of(new EntityIdRangeParameter(RangeOperator.GT, accountId)), - false, - ParameterNames.TOKEN_ID)) + List.of(new EntityIdRangeParameter(RangeOperator.GT, accountId)), false, Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); var response = service.getNftAllowances(request); @@ -101,13 +97,9 @@ void getNftAllowancesWithEvmAddress() { .limit(2) .accountId(new EntityIdEvmAddressParameter(0, 0, entity.getEvmAddress())) .ownerOrSpenderIds(new Bound( - List.of(new EntityIdRangeParameter(RangeOperator.GTE, accountId)), - true, - ParameterNames.ACCOUNT_ID)) + List.of(new EntityIdRangeParameter(RangeOperator.GTE, accountId)), true, Constants.ACCOUNT_ID)) .tokenIds(new Bound( - List.of(new EntityIdRangeParameter(RangeOperator.GT, accountId)), - false, - ParameterNames.TOKEN_ID)) + List.of(new EntityIdRangeParameter(RangeOperator.GT, accountId)), false, Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); var response = service.getNftAllowances(request); @@ -133,13 +125,9 @@ void getNftAllowancesForOrderDescOwner() { .limit(2) .accountId(new EntityIdNumParameter(ACCOUNT_ID)) .ownerOrSpenderIds(new Bound( - List.of(new EntityIdRangeParameter(RangeOperator.GTE, ACCOUNT_ID)), - true, - ParameterNames.ACCOUNT_ID)) + List.of(new EntityIdRangeParameter(RangeOperator.GTE, ACCOUNT_ID)), true, Constants.ACCOUNT_ID)) .tokenIds(new Bound( - List.of(new EntityIdRangeParameter(RangeOperator.GT, ACCOUNT_ID)), - false, - ParameterNames.TOKEN_ID)) + List.of(new EntityIdRangeParameter(RangeOperator.GT, ACCOUNT_ID)), false, Constants.TOKEN_ID)) .order(Sort.Direction.DESC) .build(); @@ -167,13 +155,9 @@ void getNftAllowancesForOrderDescSpender() { .limit(2) .accountId(new EntityIdNumParameter(ACCOUNT_ID)) .ownerOrSpenderIds(new Bound( - List.of(new EntityIdRangeParameter(RangeOperator.GTE, ACCOUNT_ID)), - true, - ParameterNames.ACCOUNT_ID)) + List.of(new EntityIdRangeParameter(RangeOperator.GTE, ACCOUNT_ID)), true, Constants.ACCOUNT_ID)) .tokenIds(new Bound( - List.of(new EntityIdRangeParameter(RangeOperator.GTE, ACCOUNT_ID)), - false, - ParameterNames.TOKEN_ID)) + List.of(new EntityIdRangeParameter(RangeOperator.GTE, ACCOUNT_ID)), false, Constants.TOKEN_ID)) .order(Sort.Direction.DESC) .build(); @@ -204,11 +188,11 @@ void getNftAllowancesForGteOwner() { .ownerOrSpenderIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(nftAllowance1.getSpender()))), true, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(nftAllowance1.getTokenId()))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); var response = service.getNftAllowances(request); @@ -235,11 +219,11 @@ void getNftAllowancesForGteSpender() { .ownerOrSpenderIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(nftAllowance1.getOwner()))), true, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(nftAllowance1.getTokenId()))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); var response = service.getNftAllowances(request); @@ -268,11 +252,11 @@ void getNftAllowancesForOptimizedRange() { new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(nftAllowance1.getOwner())), new EntityIdRangeParameter(RangeOperator.LTE, EntityId.of(nftAllowance1.getOwner()))), true, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(nftAllowance1.getTokenId()))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); var response = service.getNftAllowances(request); @@ -297,11 +281,11 @@ void getNftAllowancesForTokenNeedsOwnerOrSpenderIdEq() { .ownerOrSpenderIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.LT, EntityId.of(nftAllowance1.getOwner()))), true, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.LT, EntityId.of(nftAllowance1.getTokenId()))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); assertThatThrownBy(() -> service.getNftAllowances(request)) @@ -315,11 +299,11 @@ void getNftAllowancesForTokenNeedsOwnerOrSpenderIdEq() { .ownerOrSpenderIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.GT, EntityId.of(nftAllowance1.getOwner()))), true, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.GT, EntityId.of(nftAllowance1.getTokenId()))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); assertThatThrownBy(() -> service.getNftAllowances(request1)) @@ -348,11 +332,11 @@ void getNftAllowancesForRepeatedParameter() { new EntityIdRangeParameter( RangeOperator.LT, EntityId.of(nftAllowance1.getOwner() + 10))), true, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.LT, EntityId.of(nftAllowance1.getTokenId()))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); assertThatThrownBy(() -> service.getNftAllowances(request)) @@ -381,11 +365,11 @@ void getNftAllowancesEqAndRangeParameter() { new EntityIdRangeParameter( RangeOperator.EQ, EntityId.of(nftAllowance1.getOwner() - 10))), true, - ParameterNames.ACCOUNT_ID)) + Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.LT, EntityId.of(nftAllowance1.getTokenId()))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); assertThatThrownBy(() -> service.getNftAllowances(request)) @@ -408,11 +392,11 @@ void getNftAllowancesForOwnerOrSpenderIdNotPresent() { .isOwner(false) .limit(2) .accountId(new EntityIdNumParameter(ACCOUNT_ID)) - .ownerOrSpenderIds(new Bound(List.of(), true, ParameterNames.ACCOUNT_ID)) + .ownerOrSpenderIds(new Bound(List.of(), true, Constants.ACCOUNT_ID)) .tokenIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(nftAllowance1.getTokenId()))), false, - ParameterNames.TOKEN_ID)) + Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); assertThatThrownBy(() -> service.getNftAllowances(request)) @@ -439,8 +423,8 @@ void getNftAllowancesForInvalidOperatorPresent() { .ownerOrSpenderIds(new Bound( List.of(new EntityIdRangeParameter(RangeOperator.NE, EntityId.of(nftAllowance1.getSpender()))), true, - ParameterNames.ACCOUNT_ID)) - .tokenIds(new Bound(List.of(), false, ParameterNames.TOKEN_ID)) + Constants.ACCOUNT_ID)) + .tokenIds(new Bound(List.of(), false, Constants.TOKEN_ID)) .order(Sort.Direction.ASC) .build(); assertThatThrownBy(() -> service.getNftAllowances(request)) diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java new file mode 100644 index 00000000000..93cab2a3024 --- /dev/null +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.service; + +import static com.hedera.mirror.common.domain.token.TokenTypeEnum.FUNGIBLE_COMMON; +import static org.assertj.core.api.Assertions.assertThat; + +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.restjava.RestJavaIntegrationTest; +import com.hedera.mirror.restjava.common.EntityIdAliasParameter; +import com.hedera.mirror.restjava.common.EntityIdNumParameter; +import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Sort.Direction; + +@RequiredArgsConstructor +class TokenAirdropServiceTest extends RestJavaIntegrationTest { + + private final TokenAirdropService service; + private static final EntityId RECEIVER = EntityId.of(1000L); + private static final EntityId SENDER = EntityId.of(1001L); + private static final EntityId TOKEN_ID = EntityId.of(5000L); + + @Test + void getOutstandingTokenAirdrops() { + var fungibleAirdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.amount(100L) + .receiverAccountId(RECEIVER.getId()) + .senderAccountId(SENDER.getId()) + .tokenId(TOKEN_ID.getId())) + .persist(); + + var request = OutstandingTokenAirdropRequest.builder() + .limit(2) + .senderId(new EntityIdNumParameter(SENDER)) + .order(Direction.ASC) + .build(); + var response = service.getOutstandingTokenAirdrops(request); + assertThat(response).containsExactly(fungibleAirdrop); + } + + @Test + void getOutstandingAirdropByAlias() { + var entity = domainBuilder.entity().persist(); + var tokenAirdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId())) + .persist(); + var request = OutstandingTokenAirdropRequest.builder() + .limit(2) + .senderId(new EntityIdAliasParameter(entity.getShard(), entity.getRealm(), entity.getAlias())) + .order(Direction.ASC) + .build(); + var response = service.getOutstandingTokenAirdrops(request); + assertThat(response).containsExactly(tokenAirdrop); + } +} diff --git a/hedera-mirror-rest/api/v1/openapi.yml b/hedera-mirror-rest/api/v1/openapi.yml index a8ebc5ab064..1c0600a8983 100644 --- a/hedera-mirror-rest/api/v1/openapi.yml +++ b/hedera-mirror-rest/api/v1/openapi.yml @@ -162,6 +162,32 @@ paths: $ref: "#/components/responses/NotFoundError" tags: - accounts + /api/v1/accounts/{senderIdOrAliasOrEvmAddress}/airdrops/outstanding: + get: + summary: Get get pending airdrops sent by an account + description: | + Returns pending airdrops that have been sent by an account. + operationId: getOutstandingTokenAirdrops + parameters: + - $ref: "#/components/parameters/accountIdOrAliasOrEvmAddressPathParam" + - $ref: "#/components/parameters/limitQueryParam" + - $ref: "#/components/parameters/orderQueryParam" + - $ref: "#/components/parameters/receiverIdOrAliasOrEvmAddressQueryParam" + - $ref: "#/components/parameters/serialNumberQueryParam" + - $ref: "#/components/parameters/tokenIdQueryParam" + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/TokenAirdropsResponse" + 400: + $ref: "#/components/responses/InvalidParameterError" + 404: + $ref: "#/components/responses/NotFoundError" + tags: + - accounts /api/v1/accounts/{idOrAliasOrEvmAddress}/allowances/crypto: get: summary: Get crypto allowances for an account info @@ -1654,6 +1680,13 @@ components: $ref: "#/components/schemas/StakingReward" links: $ref: "#/components/schemas/Links" + TokenAirdropsResponse: + type: object + properties: + airdrops: + $ref: "#/components/schemas/TokenAirdrops" + links: + $ref: "#/components/schemas/Links" TokenAllowancesResponse: type: object properties: @@ -3306,6 +3339,43 @@ components: symbol: FIRSTMOVERLPDJH token_id: 0.0.1 type: FUNGIBLE_COMMON + TokenAirdrop: + type: object + properties: + amount: + format: int64 + type: integer + receiver_id: + $ref: "#/components/schemas/EntityId" + sender_id: + $ref: "#/components/schemas/EntityId" + serial_number: + example: 1 + format: int64 + type: integer + timestamp: + $ref: "#/components/schemas/TimestampRange" + token_id: + $ref: "#/components/schemas/EntityId" + example: + amount: 10 + receiver_id: "0.0.15" + sender_id: "0.0.10" + serial_number: null + timestamp: + from: "1651560386.060890949" + to: "1651560386.661997287" + token_id: "0.0.99" + required: + - amount + - receiver_id + - sender_id + - timestamp + - token_id + TokenAirdrops: + type: array + items: + $ref: "#/components/schemas/TokenAirdrop" TokenAllowance: allOf: - $ref: "#/components/schemas/Allowance" @@ -4384,6 +4454,26 @@ components: example: 3c3d546321ff6f63d701d2ec5c277095874e19f4a235bee1e6bb19258bf362be schema: type: string + receiverIdOrAliasOrEvmAddressQueryParam: + name: receiver.id + in: query + description: Receiver account id or account alias with no shard realm or evm address with no shard realm + examples: + aliasOnly: + value: HIQQEXWKW53RKN4W6XXC4Q232SYNZ3SZANVZZSUME5B5PRGXL663UAQA + accountNumOnly: + value: 8 + realmAccountNum: + value: 0.8 + shardRealmAccountNum: + value: 0.0.8 + evmAddress: + value: ac384c53f03855fa1b3616052f8ba32c6c2a2fec + evmAddressWithPrefix: + value: 0xac384c53f03855fa1b3616052f8ba32c6c2a2fec + schema: + pattern: ^(\d{1,10}\.){0,2}(\d{1,10}|(0x)?[A-Fa-f0-9]{40}|(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}|[A-Z2-7]{4,5}|[A-Z2-7]{7,8}))$ + type: string scheduledQueryParam: name: scheduled in: query From 08121945f764babce71c286312594bd20c97ad28 Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Fri, 6 Sep 2024 16:56:52 -0400 Subject: [PATCH 02/10] Fix code smell Signed-off-by: Edwin Greene --- ...bstractCustomRepository.java => CustomRepository.java} | 8 ++++---- .../restjava/repository/NftAllowanceRepositoryCustom.java | 2 +- .../repository/NftAllowanceRepositoryCustomImpl.java | 2 +- .../restjava/repository/TokenAirdropRepositoryCustom.java | 2 +- .../repository/TokenAirdropRepositoryCustomImpl.java | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/{AbstractCustomRepository.java => CustomRepository.java} (80%) diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/AbstractCustomRepository.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java similarity index 80% rename from hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/AbstractCustomRepository.java rename to hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java index c4fa5dd626d..a341f517155 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/AbstractCustomRepository.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java @@ -24,9 +24,9 @@ import org.jooq.Condition; import org.jooq.Field; -abstract class AbstractCustomRepository { +interface CustomRepository { - protected static Condition getCondition(Field field, EntityIdRangeParameter param) { + default Condition getCondition(Field field, EntityIdRangeParameter param) { if (param == null) { return noCondition(); } @@ -34,7 +34,7 @@ protected static Condition getCondition(Field field, EntityIdRangeParamete return getCondition(field, param.operator(), param.value().getId()); } - protected static Condition getCondition(Field field, IntegerRangeParameter param) { + default Condition getCondition(Field field, IntegerRangeParameter param) { if (param == null) { return noCondition(); } @@ -42,7 +42,7 @@ protected static Condition getCondition(Field field, IntegerRangeParameter return getCondition(field, param.operator(), param.value().longValue()); } - protected static Condition getCondition(Field field, RangeOperator operator, Long value) { + default Condition getCondition(Field field, RangeOperator operator, Long value) { return operator.getFunction().apply(field, value); } } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustom.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustom.java index e9a09fcfaac..7eeba3390e2 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustom.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustom.java @@ -22,7 +22,7 @@ import jakarta.validation.constraints.NotNull; import java.util.Collection; -public interface NftAllowanceRepositoryCustom { +public interface NftAllowanceRepositoryCustom extends CustomRepository { /** * Find all NftAllowance matching the request parameters with the given limit, sort order, and byOwner flag diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java index 847b3ebb7be..b82ee8d0858 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java @@ -41,7 +41,7 @@ @Named @RequiredArgsConstructor -class NftAllowanceRepositoryCustomImpl extends AbstractCustomRepository implements NftAllowanceRepositoryCustom { +class NftAllowanceRepositoryCustomImpl implements NftAllowanceRepositoryCustom { private static final Condition APPROVAL_CONDITION = NFT_ALLOWANCE.APPROVED_FOR_ALL.isTrue(); private static final Map>> SORT_ORDERS = Map.of( diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java index 8da76483947..8836859cb7a 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java @@ -22,7 +22,7 @@ import jakarta.validation.constraints.NotNull; import java.util.Collection; -public interface TokenAirdropRepositoryCustom { +public interface TokenAirdropRepositoryCustom extends CustomRepository { @NotNull Collection findAllOutstanding(OutstandingTokenAirdropRequest request, EntityId accountId); diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java index fa77f278e91..b96d1f705b2 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java @@ -34,7 +34,7 @@ @Named @RequiredArgsConstructor -class TokenAirdropRepositoryCustomImpl extends AbstractCustomRepository implements TokenAirdropRepositoryCustom { +class TokenAirdropRepositoryCustomImpl implements TokenAirdropRepositoryCustom { private final DSLContext dslContext; private static final Map>> SORT_ORDERS = Map.of( From db31f8977eca3634fa20becc74581516448a537b Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Fri, 6 Sep 2024 17:21:15 -0400 Subject: [PATCH 03/10] Cleanup unneeded comments Signed-off-by: Edwin Greene --- .../restjava/repository/TokenAirdropRepositoryTest.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java index 59161abcf87..dd333d9f9b5 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java @@ -203,7 +203,6 @@ void serialNumber() { .tokenId(nftTokenId2)) .persist(); - // serialNumber gt 4 -> serialAirdrop1, serialAirdrop2, serialAirdrop3 var expectedAirdrops = List.of(serialAirdrop1, serialAirdrop2, serialAirdrop3); var request = OutstandingTokenAirdropRequest.builder() .senderId(new EntityIdNumParameter(EntityId.of(sender))) @@ -212,7 +211,6 @@ void serialNumber() { assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); - // serialNumber lt 10 -> serialAirdrop1, serialAirdrop3 expectedAirdrops = List.of(serialAirdrop1, serialAirdrop3); request = OutstandingTokenAirdropRequest.builder() .senderId(new EntityIdNumParameter(EntityId.of(sender))) @@ -221,7 +219,6 @@ void serialNumber() { assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); - // tokenId eq nftTokenId -> serialAirdrop1, serialAirdrop2 expectedAirdrops = List.of(serialAirdrop1, serialAirdrop2); request = OutstandingTokenAirdropRequest.builder() .senderId(new EntityIdNumParameter(EntityId.of(sender))) @@ -230,7 +227,6 @@ void serialNumber() { assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); - // tokenId eq nftTokenId && serialNumber lte 5 -> serialAirdrop1 expectedAirdrops = List.of(serialAirdrop1); request = OutstandingTokenAirdropRequest.builder() .senderId(new EntityIdNumParameter(EntityId.of(sender))) @@ -240,7 +236,6 @@ void serialNumber() { assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); - // tokenId eq nftTokenId && serialNumber gte 10 -> serialAirdrop2 expectedAirdrops = List.of(serialAirdrop2); request = OutstandingTokenAirdropRequest.builder() .senderId(new EntityIdNumParameter(EntityId.of(sender))) @@ -250,7 +245,6 @@ void serialNumber() { assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); - // receiver eq 3000 -> airdrop1, serialAirdrop1, serialAirdrop2 expectedAirdrops = List.of(airdrop1, serialAirdrop1, serialAirdrop2); request = OutstandingTokenAirdropRequest.builder() .senderId(new EntityIdNumParameter(EntityId.of(sender))) @@ -259,7 +253,6 @@ void serialNumber() { assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); - // receiver eq 3000 && serialNumber lte 5 -> serialAirdrop1 expectedAirdrops = List.of(serialAirdrop1); request = OutstandingTokenAirdropRequest.builder() .senderId(new EntityIdNumParameter(EntityId.of(sender))) From 154aacbbfefff6d0e314a4ff64d8b4760f54fc40 Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Wed, 11 Sep 2024 17:08:07 -0400 Subject: [PATCH 04/10] Updates per feedback Signed-off-by: Edwin Greene --- .../domain/token/AbstractTokenAirdrop.java | 12 +- .../mirror/common/domain/DomainBuilder.java | 8 +- ...tTokenUpdateAirdropTransactionHandler.java | 4 +- .../TokenAirdropTransactionHandler.java | 4 +- .../db/migration/common/R__01_temp_tables.sql | 2 +- .../v1/V1.100.0__add_token_airdrop.sql | 8 +- .../v2/R__02_temp_table_distribution.sql | 2 +- .../v2/V2.5.2__add_token_airdrop.sql | 12 +- .../EntityRecordItemListenerTokenTest.java | 24 +-- .../entity/sql/SqlEntityListenerTest.java | 20 +- .../TokenAirdropTransactionHandlerTest.java | 8 +- ...enCancelAirdropTransactionHandlerTest.java | 4 +- ...kenClaimAirdropTransactionHandlerTest.java | 4 +- .../mirror/restjava/common/Constants.java | 1 + ...rameter.java => NumberRangeParameter.java} | 21 ++- .../controller/TokenAirdropsController.java | 14 +- ...pRequest.java => TokenAirdropRequest.java} | 12 +- .../restjava/mapper/CollectionMapper.java | 40 ++++ .../restjava/mapper/NftAllowanceMapper.java | 19 +- .../restjava/mapper/TokenAirdropsMapper.java | 21 +-- .../restjava/repository/CustomRepository.java | 4 +- .../TokenAirdropRepositoryCustom.java | 4 +- .../TokenAirdropRepositoryCustomImpl.java | 21 +-- .../restjava/service/TokenAirdropService.java | 4 +- .../service/TokenAirdropServiceImpl.java | 12 +- ...est.java => NumberRangeParameterTest.java} | 30 ++- .../TokenAirdropsControllerTest.java | 173 +++++++++++++++--- .../mapper/TokenAirdropsMapperTest.java | 4 +- .../TokenAirdropRepositoryTest.java | 117 ++++++------ .../service/TokenAirdropServiceTest.java | 59 ++++-- hedera-mirror-rest/api/v1/openapi.yml | 58 +++--- 31 files changed, 461 insertions(+), 265 deletions(-) rename hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/{IntegerRangeParameter.java => NumberRangeParameter.java} (57%) rename hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/{OutstandingTokenAirdropRequest.java => TokenAirdropRequest.java} (73%) create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/CollectionMapper.java rename hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/{IntegerRangeParameterTest.java => NumberRangeParameterTest.java} (68%) diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/AbstractTokenAirdrop.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/AbstractTokenAirdrop.java index 6084281197c..66dc7ea7bf6 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/AbstractTokenAirdrop.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/AbstractTokenAirdrop.java @@ -43,10 +43,10 @@ public class AbstractTokenAirdrop implements History { private Long amount; @jakarta.persistence.Id - private long receiverAccountId; + private long receiverId; @jakarta.persistence.Id - private long senderAccountId; + private long senderId; @jakarta.persistence.Id private long serialNumber; @@ -63,8 +63,8 @@ public class AbstractTokenAirdrop implements History { @JsonIgnore public Id getId() { Id id = new Id(); - id.setReceiverAccountId(receiverAccountId); - id.setSenderAccountId(senderAccountId); + id.setReceiverId(receiverId); + id.setSenderId(senderId); id.setSerialNumber(serialNumber); id.setTokenId(tokenId); return id; @@ -75,8 +75,8 @@ public static class Id implements Serializable { @Serial private static final long serialVersionUID = -8165098238647325621L; - private long receiverAccountId; - private long senderAccountId; + private long receiverId; + private long senderId; private long serialNumber; private long tokenId; } 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 24bd8b2a6d4..02440687e43 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 @@ -932,8 +932,8 @@ public DomainWrapper sidecarFile() public DomainWrapper> tokenAirdrop(TokenTypeEnum type) { long timestamp = timestamp(); var builder = TokenAirdrop.builder() - .receiverAccountId(id()) - .senderAccountId(id()) + .receiverId(id()) + .senderId(id()) .state(TokenAirdropStateEnum.PENDING) .timestampRange(Range.atLeast(timestamp)) .tokenId(id()); @@ -951,8 +951,8 @@ public DomainWrapper sidecarFile() TokenTypeEnum type) { long timestamp = timestamp(); var builder = TokenAirdropHistory.builder() - .receiverAccountId(id()) - .senderAccountId(id()) + .receiverId(id()) + .senderId(id()) .state(TokenAirdropStateEnum.PENDING) .timestampRange(Range.closedOpen(timestamp, timestamp + 10)) .tokenId(id()); diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTokenUpdateAirdropTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTokenUpdateAirdropTransactionHandler.java index a15e56254ca..48d36158ea0 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTokenUpdateAirdropTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTokenUpdateAirdropTransactionHandler.java @@ -65,8 +65,8 @@ public void doUpdateTransaction(Transaction transaction, RecordItem recordItem) var tokenAirdrop = new TokenAirdrop(); tokenAirdrop.setState(state); - tokenAirdrop.setReceiverAccountId(receiver.getId()); - tokenAirdrop.setSenderAccountId(sender.getId()); + tokenAirdrop.setReceiverId(receiver.getId()); + tokenAirdrop.setSenderId(sender.getId()); tokenAirdrop.setTimestampRange(Range.atLeast(recordItem.getConsensusTimestamp())); TokenID tokenId; diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandler.java index 2949c1f0b35..a491cb33beb 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandler.java @@ -52,8 +52,8 @@ protected void doUpdateTransaction(Transaction transaction, RecordItem recordIte var tokenAirdrop = new TokenAirdrop(); tokenAirdrop.setState(TokenAirdropStateEnum.PENDING); - tokenAirdrop.setReceiverAccountId(receiver.getId()); - tokenAirdrop.setSenderAccountId(sender.getId()); + tokenAirdrop.setReceiverId(receiver.getId()); + tokenAirdrop.setSenderId(sender.getId()); tokenAirdrop.setTimestampRange(Range.atLeast(recordItem.getConsensusTimestamp())); TokenID tokenId; diff --git a/hedera-mirror-importer/src/main/resources/db/migration/common/R__01_temp_tables.sql b/hedera-mirror-importer/src/main/resources/db/migration/common/R__01_temp_tables.sql index 380a84d1d38..78912c64af6 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/common/R__01_temp_tables.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/common/R__01_temp_tables.sql @@ -30,7 +30,7 @@ call create_temp_table_safe('nft', 'token_id', 'serial_number'); call create_temp_table_safe('node', 'node_id'); call create_temp_table_safe('schedule', 'schedule_id'); call create_temp_table_safe('token_account', 'account_id', 'token_id'); -call create_temp_table_safe('token_airdrop', 'receiver_account_id', 'sender_account_id', 'serial_number', 'token_id'); +call create_temp_table_safe('token_airdrop', 'receiver_id', 'sender_id', 'serial_number', 'token_id'); call create_temp_table_safe('token_allowance', 'owner', 'spender', 'token_id'); call create_temp_table_safe('token', 'token_id'); call create_temp_table_safe('topic_message_lookup', 'topic_id', 'partition'); diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.100.0__add_token_airdrop.sql b/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.100.0__add_token_airdrop.sql index 2b0fefe0cd7..f0943252038 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.100.0__add_token_airdrop.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.100.0__add_token_airdrop.sql @@ -3,16 +3,16 @@ create type airdrop_state as enum ('CANCELLED', 'CLAIMED', 'PENDING'); create table if not exists token_airdrop ( amount bigint, - receiver_account_id bigint not null, - sender_account_id bigint not null, + receiver_id bigint not null, + sender_id bigint not null, serial_number bigint not null, state airdrop_state not null default 'PENDING', timestamp_range int8range not null, token_id bigint not null ); -create unique index if not exists token_airdrop__sender_id on token_airdrop (sender_account_id, receiver_account_id, token_id, serial_number); -create index if not exists token_airdrop__receiver_id on token_airdrop (receiver_account_id, sender_account_id, token_id, serial_number); +create unique index if not exists token_airdrop__sender_id on token_airdrop (sender_id, receiver_id, token_id, serial_number); +create index if not exists token_airdrop__receiver_id on token_airdrop (receiver_id, sender_id, token_id, serial_number); create table if not exists token_airdrop_history ( diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v2/R__02_temp_table_distribution.sql b/hedera-mirror-importer/src/main/resources/db/migration/v2/R__02_temp_table_distribution.sql index 4bbf3eb01e4..a554fe850f6 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/v2/R__02_temp_table_distribution.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/v2/R__02_temp_table_distribution.sql @@ -22,7 +22,7 @@ call create_distributed_table_safe('nft_allowance_temp', 'owner', 'nft_allowance call create_distributed_table_safe('nft_temp', 'token_id', 'nft'); call create_distributed_table_safe('schedule_temp', 'schedule_id', 'schedule'); call create_distributed_table_safe('token_account_temp', 'account_id', 'token_account'); -call create_distributed_table_safe('token_airdrop_temp', 'receiver_account_id', 'token_airdrop'); +call create_distributed_table_safe('token_airdrop_temp', 'receiver_id', 'token_airdrop'); call create_distributed_table_safe('token_allowance_temp', 'owner', 'token_allowance'); call create_distributed_table_safe('dissociate_token_transfer', 'token_id', 'nft'); call create_distributed_table_safe('token_temp', 'token_id', 'token'); diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.5.2__add_token_airdrop.sql b/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.5.2__add_token_airdrop.sql index 54216bb9dd1..0e2762299b7 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.5.2__add_token_airdrop.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.5.2__add_token_airdrop.sql @@ -3,8 +3,8 @@ create type airdrop_state as enum ('CANCELLED', 'CLAIMED', 'PENDING'); create table if not exists token_airdrop ( amount bigint, - receiver_account_id bigint not null, - sender_account_id bigint not null, + receiver_id bigint not null, + sender_id bigint not null, serial_number bigint not null, state airdrop_state not null default 'PENDING', timestamp_range int8range not null, @@ -18,9 +18,9 @@ create table if not exists token_airdrop_history ); comment on table token_airdrop_history is 'History of token airdrops'; -select create_distributed_table('token_airdrop', 'receiver_account_id', colocate_with => 'entity'); -select create_distributed_table('token_airdrop_history', 'receiver_account_id', colocate_with => 'token_airdrop'); +select create_distributed_table('token_airdrop', 'receiver_id', colocate_with => 'entity'); +select create_distributed_table('token_airdrop_history', 'receiver_id', colocate_with => 'token_airdrop'); -create unique index if not exists token_airdrop__sender_id on token_airdrop (sender_account_id, receiver_account_id, token_id, serial_number); -create index if not exists token_airdrop__receiver_id on token_airdrop (receiver_account_id, sender_account_id, token_id, serial_number); +create unique index if not exists token_airdrop__sender_id on token_airdrop (sender_id, receiver_id, token_id, serial_number); +create index if not exists token_airdrop__receiver_id on token_airdrop (receiver_id, sender_id, token_id, serial_number); create index if not exists token_airdrop_history__timestamp_range on token_airdrop_history using gist (timestamp_range); 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 4651322902f..6b346867ab9 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 @@ -3609,16 +3609,16 @@ void tokenAirdrop(TokenAirdropStateEnum airdropType) { var expectedPendingFungible = domainBuilder .tokenAirdrop(TokenTypeEnum.FUNGIBLE_COMMON) .customize(t -> t.amount(pendingAmount) - .receiverAccountId(RECEIVER.getAccountNum()) - .senderAccountId(PAYER.getAccountNum()) + .receiverId(RECEIVER.getAccountNum()) + .senderId(PAYER.getAccountNum()) .state(TokenAirdropStateEnum.PENDING) .timestampRange(Range.atLeast(airdropTimestamp)) .tokenId(TOKEN_ID.getTokenNum())) .get(); var expectedPendingNft = domainBuilder .tokenAirdrop(TokenTypeEnum.NON_FUNGIBLE_UNIQUE) - .customize(t -> t.receiverAccountId(RECEIVER.getAccountNum()) - .senderAccountId(PAYER.getAccountNum()) + .customize(t -> t.receiverId(RECEIVER.getAccountNum()) + .senderId(PAYER.getAccountNum()) .serialNumber(SERIAL_NUMBER_1) .state(TokenAirdropStateEnum.PENDING) .timestampRange(Range.atLeast(airdropTimestamp)) @@ -3738,15 +3738,15 @@ void tokenAirdropUpdateState(TokenAirdropStateEnum airdropType) { var expectedPendingFungible = domainBuilder .tokenAirdrop(TokenTypeEnum.FUNGIBLE_COMMON) .customize(t -> t.amount(pendingAmount) - .receiverAccountId(RECEIVER.getAccountNum()) - .senderAccountId(PAYER.getAccountNum()) + .receiverId(RECEIVER.getAccountNum()) + .senderId(PAYER.getAccountNum()) .timestampRange(Range.atLeast(airdropTimestamp)) .tokenId(TOKEN_ID.getTokenNum())) .get(); var expectedPendingNft = domainBuilder .tokenAirdrop(TokenTypeEnum.NON_FUNGIBLE_UNIQUE) - .customize(t -> t.receiverAccountId(RECEIVER.getAccountNum()) - .senderAccountId(PAYER.getAccountNum()) + .customize(t -> t.receiverId(RECEIVER.getAccountNum()) + .senderId(PAYER.getAccountNum()) .serialNumber(SERIAL_NUMBER_1) .timestampRange(Range.atLeast(airdropTimestamp)) .tokenId(nftTokenId.getTokenNum())) @@ -3843,15 +3843,15 @@ void tokenAirdropPartialData(TokenAirdropStateEnum airdropType) { .tokenAirdrop(TokenTypeEnum.FUNGIBLE_COMMON) // Amount will be null when there is no pending airdrop .customize(t -> t.amount(null) - .receiverAccountId(RECEIVER.getAccountNum()) - .senderAccountId(PAYER.getAccountNum()) + .receiverId(RECEIVER.getAccountNum()) + .senderId(PAYER.getAccountNum()) .timestampRange(Range.atLeast(updateTimestamp)) .tokenId(TOKEN_ID.getTokenNum())) .get(); var expectedPendingNft = domainBuilder .tokenAirdrop(TokenTypeEnum.NON_FUNGIBLE_UNIQUE) - .customize(t -> t.receiverAccountId(RECEIVER.getAccountNum()) - .senderAccountId(PAYER.getAccountNum()) + .customize(t -> t.receiverId(RECEIVER.getAccountNum()) + .senderId(PAYER.getAccountNum()) .serialNumber(SERIAL_NUMBER_1) .timestampRange(Range.atLeast(updateTimestamp)) .tokenId(nftTokenId.getTokenNum())) 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 4425f2c9d6c..59413f3a264 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 @@ -2722,8 +2722,8 @@ void onTokenAirdrop() { var updatedAmountAirdrop = domainBuilder .tokenAirdrop(TokenTypeEnum.FUNGIBLE_COMMON) .customize(a -> a.amount(newAmount) - .receiverAccountId(tokenAirdrop.getReceiverAccountId()) - .senderAccountId(tokenAirdrop.getSenderAccountId()) + .receiverId(tokenAirdrop.getReceiverId()) + .senderId(tokenAirdrop.getSenderId()) .tokenId(tokenAirdrop.getTokenId()) .timestampRange(Range.atLeast(domainBuilder.timestamp()))) .get(); @@ -2768,16 +2768,16 @@ void onTokenAirdropUpdate(TokenAirdropStateEnum state, int commitIndex) { .customize(a -> a.state(state) .amount(null) // Record files that change state do not include PendingAirdropValue so remove the // amount here - .receiverAccountId(tokenAirdrop.getReceiverAccountId()) - .senderAccountId(tokenAirdrop.getSenderAccountId()) + .receiverId(tokenAirdrop.getReceiverId()) + .senderId(tokenAirdrop.getSenderId()) .tokenId(tokenAirdrop.getTokenId()) .timestampRange(Range.atLeast(domainBuilder.timestamp()))) .get(); var nftAirdropUpdateState = domainBuilder .tokenAirdrop(TokenTypeEnum.NON_FUNGIBLE_UNIQUE) .customize(a -> a.state(state) - .receiverAccountId(nftAirdrop.getReceiverAccountId()) - .senderAccountId(nftAirdrop.getSenderAccountId()) + .receiverId(nftAirdrop.getReceiverId()) + .senderId(nftAirdrop.getSenderId()) .serialNumber(nftAirdrop.getSerialNumber()) .tokenId(nftAirdrop.getTokenId()) .timestampRange(Range.atLeast(domainBuilder.timestamp()))) @@ -2814,16 +2814,16 @@ void onTokenAirdropPartialUpdate(TokenAirdropStateEnum state) { .customize(a -> a.state(state) .amount(null) // Record files that change state do not include PendingAirdropValue so remove the // amount here - .receiverAccountId(tokenAirdrop.getReceiverAccountId()) - .senderAccountId(tokenAirdrop.getSenderAccountId()) + .receiverId(tokenAirdrop.getReceiverId()) + .senderId(tokenAirdrop.getSenderId()) .tokenId(tokenAirdrop.getTokenId()) .timestampRange(Range.atLeast(domainBuilder.timestamp()))) .get(); var nftAirdropUpdateState = domainBuilder .tokenAirdrop(TokenTypeEnum.NON_FUNGIBLE_UNIQUE) .customize(a -> a.state(state) - .receiverAccountId(nftAirdrop.getReceiverAccountId()) - .senderAccountId(nftAirdrop.getSenderAccountId()) + .receiverId(nftAirdrop.getReceiverId()) + .senderId(nftAirdrop.getSenderId()) .serialNumber(nftAirdrop.getSerialNumber()) .tokenId(nftAirdrop.getTokenId()) .timestampRange(Range.atLeast(domainBuilder.timestamp()))) diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandlerTest.java index cf93dcff1f3..cb7d701eb51 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandlerTest.java @@ -87,8 +87,8 @@ void updateTransactionSuccessfulFungiblePendingAirdrop() { verify(entityListener).onTokenAirdrop(tokenAirdrop.capture()); assertThat(tokenAirdrop.getValue()) .returns(amount, TokenAirdrop::getAmount) - .returns(receiver.getAccountNum(), TokenAirdrop::getReceiverAccountId) - .returns(sender.getAccountNum(), TokenAirdrop::getSenderAccountId) + .returns(receiver.getAccountNum(), TokenAirdrop::getReceiverId) + .returns(sender.getAccountNum(), TokenAirdrop::getSenderId) .returns(0L, TokenAirdrop::getSerialNumber) .returns(PENDING, TokenAirdrop::getState) .returns(Range.atLeast(timestamp), TokenAirdrop::getTimestampRange) @@ -130,8 +130,8 @@ void updateTransactionSuccessfulNftPendingAirdrop() { verify(entityListener).onTokenAirdrop(tokenAirdrop.capture()); assertThat(tokenAirdrop.getValue()) .returns(null, TokenAirdrop::getAmount) - .returns(receiver.getAccountNum(), TokenAirdrop::getReceiverAccountId) - .returns(sender.getAccountNum(), TokenAirdrop::getSenderAccountId) + .returns(receiver.getAccountNum(), TokenAirdrop::getReceiverId) + .returns(sender.getAccountNum(), TokenAirdrop::getSenderId) .returns(PENDING, TokenAirdrop::getState) .returns(Range.atLeast(timestamp), TokenAirdrop::getTimestampRange) .returns(token.getTokenNum(), TokenAirdrop::getTokenId); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenCancelAirdropTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenCancelAirdropTransactionHandlerTest.java index 3508d7e8733..4d74cfcaf4b 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenCancelAirdropTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenCancelAirdropTransactionHandlerTest.java @@ -101,8 +101,8 @@ void cancelAirdrop(TokenTypeEnum tokenType) { verify(entityListener).onTokenAirdrop(tokenAirdrop.capture()); assertThat(tokenAirdrop.getValue()) - .returns(receiver.getNum(), TokenAirdrop::getReceiverAccountId) - .returns(sender.getNum(), TokenAirdrop::getSenderAccountId) + .returns(receiver.getNum(), TokenAirdrop::getReceiverId) + .returns(sender.getNum(), TokenAirdrop::getSenderId) .returns(TokenAirdropStateEnum.CANCELLED, TokenAirdrop::getState) .returns(Range.atLeast(timestamp), TokenAirdrop::getTimestampRange) .returns(token.getTokenNum(), TokenAirdrop::getTokenId); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenClaimAirdropTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenClaimAirdropTransactionHandlerTest.java index e0ada6968ee..fdf1b823a5b 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenClaimAirdropTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenClaimAirdropTransactionHandlerTest.java @@ -101,8 +101,8 @@ void claimAirdrop(TokenTypeEnum tokenType) { verify(entityListener).onTokenAirdrop(tokenAirdrop.capture()); assertThat(tokenAirdrop.getValue()) - .returns(receiver.getNum(), TokenAirdrop::getReceiverAccountId) - .returns(sender.getNum(), TokenAirdrop::getSenderAccountId) + .returns(receiver.getNum(), TokenAirdrop::getReceiverId) + .returns(sender.getNum(), TokenAirdrop::getSenderId) .returns(TokenAirdropStateEnum.CLAIMED, TokenAirdrop::getState) .returns(Range.atLeast(timestamp), TokenAirdrop::getTimestampRange) .returns(token.getTokenNum(), TokenAirdrop::getTokenId); diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/Constants.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/Constants.java index 91f0d0c092a..54cadc86377 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/Constants.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/Constants.java @@ -23,6 +23,7 @@ public class Constants { public static final String ACCOUNT_ID = "account.id"; public static final String RECEIVER_ID = "receiver.id"; + public static final String SENDER_ID = "sender.id"; public static final String SERIAL_NUMBER = "serialnumber"; public static final String TOKEN_ID = "token.id"; diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/IntegerRangeParameter.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/NumberRangeParameter.java similarity index 57% rename from hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/IntegerRangeParameter.java rename to hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/NumberRangeParameter.java index 9eb5d278858..ce81947d904 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/IntegerRangeParameter.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/common/NumberRangeParameter.java @@ -18,21 +18,30 @@ import org.apache.commons.lang3.StringUtils; -public record IntegerRangeParameter(RangeOperator operator, Integer value) implements RangeParameter { +public record NumberRangeParameter(RangeOperator operator, Long value) implements RangeParameter { - public static final IntegerRangeParameter EMPTY = new IntegerRangeParameter(null, null); + public static final NumberRangeParameter EMPTY = new NumberRangeParameter(null, null); - public static IntegerRangeParameter valueOf(String valueRangeParam) { + public static NumberRangeParameter valueOf(String valueRangeParam) { if (StringUtils.isBlank(valueRangeParam)) { return EMPTY; } var splitVal = valueRangeParam.split(":"); return switch (splitVal.length) { - case 1 -> new IntegerRangeParameter(RangeOperator.EQ, Integer.valueOf(splitVal[0])); - case 2 -> new IntegerRangeParameter(RangeOperator.of(splitVal[0]), Integer.valueOf(splitVal[1])); + case 1 -> new NumberRangeParameter(RangeOperator.EQ, getNumberValue(splitVal[0])); + case 2 -> new NumberRangeParameter(RangeOperator.of(splitVal[0]), getNumberValue(splitVal[1])); default -> throw new IllegalArgumentException( - "Invalid range operator %s. Should have format rangeOperator:Integer".formatted(valueRangeParam)); + "Invalid range operator %s. Should have format rangeOperator:Number".formatted(valueRangeParam)); }; } + + private static long getNumberValue(String number) { + var value = Long.parseLong(number); + if (value < 0) { + throw new IllegalArgumentException("Invalid range value: " + number); + } + + return value; + } } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java index 4f20556887f..682bdb2fffc 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java @@ -27,9 +27,9 @@ import com.hedera.mirror.rest.model.TokenAirdropsResponse; import com.hedera.mirror.restjava.common.EntityIdParameter; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.IntegerRangeParameter; import com.hedera.mirror.restjava.common.LinkFactory; -import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import com.hedera.mirror.restjava.common.NumberRangeParameter; +import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import com.hedera.mirror.restjava.mapper.TokenAirdropsMapper; import com.hedera.mirror.restjava.service.TokenAirdropService; import jakarta.validation.constraints.Max; @@ -65,17 +65,17 @@ TokenAirdropsResponse getOutstandingAirdrops( @RequestParam(defaultValue = DEFAULT_LIMIT) @Positive @Max(MAX_LIMIT) int limit, @RequestParam(defaultValue = "asc") Sort.Direction order, @RequestParam(name = RECEIVER_ID, required = false) EntityIdRangeParameter receiverId, - @RequestParam(name = SERIAL_NUMBER, required = false) IntegerRangeParameter serialNumber, + @RequestParam(name = SERIAL_NUMBER, required = false) NumberRangeParameter serialNumber, @RequestParam(name = TOKEN_ID, required = false) EntityIdRangeParameter tokenId) { - var request = OutstandingTokenAirdropRequest.builder() - .senderId(id) + var request = TokenAirdropRequest.builder() + .accountId(id) + .entityId(receiverId) .limit(limit) .order(order) - .receiverId(receiverId) .serialNumber(serialNumber) .tokenId(tokenId) .build(); - var response = service.getOutstandingTokenAirdrops(request); + var response = service.getOutstandingAirdrops(request); var airdrops = tokenMapper.map(response); var sort = Sort.by(order, RECEIVER_ID, TOKEN_ID); var pageable = PageRequest.of(0, limit, sort); diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/OutstandingTokenAirdropRequest.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java similarity index 73% rename from hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/OutstandingTokenAirdropRequest.java rename to hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java index aa453b835f2..06450e91dee 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/OutstandingTokenAirdropRequest.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java @@ -18,16 +18,17 @@ import com.hedera.mirror.restjava.common.EntityIdParameter; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.IntegerRangeParameter; +import com.hedera.mirror.restjava.common.NumberRangeParameter; import lombok.Builder; import lombok.Data; import org.springframework.data.domain.Sort; @Data @Builder -public class OutstandingTokenAirdropRequest { +public class TokenAirdropRequest { - private EntityIdParameter senderId; + // Sender Id for Outstanding Airdrops, Receiver Id for Pending Airdrops + private EntityIdParameter accountId; @Builder.Default private int limit = 25; @@ -35,9 +36,10 @@ public class OutstandingTokenAirdropRequest { @Builder.Default private Sort.Direction order = Sort.Direction.ASC; - private EntityIdRangeParameter receiverId; + // Receiver Id for Outstanding Airdrops, Sender Id for Pending Airdrops + private EntityIdRangeParameter entityId; - private IntegerRangeParameter serialNumber; + private NumberRangeParameter serialNumber; private EntityIdRangeParameter tokenId; } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/CollectionMapper.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/CollectionMapper.java new file mode 100644 index 00000000000..af680f1cf98 --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/CollectionMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.mapper; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public interface CollectionMapper { + + T map(S source); + + default List map(Collection sources) { + if (sources == null) { + return Collections.emptyList(); + } + + List list = new ArrayList<>(sources.size()); + for (S source : sources) { + list.add(map(source)); + } + + return list; + } +} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/NftAllowanceMapper.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/NftAllowanceMapper.java index 097bbd7c102..bc052bacbb1 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/NftAllowanceMapper.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/NftAllowanceMapper.java @@ -17,29 +17,12 @@ package com.hedera.mirror.restjava.mapper; import com.hedera.mirror.common.domain.entity.NftAllowance; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @Mapper(config = MapperConfiguration.class) -public interface NftAllowanceMapper { +public interface NftAllowanceMapper extends CollectionMapper { @Mapping(source = "timestampRange", target = "timestamp") com.hedera.mirror.rest.model.NftAllowance map(NftAllowance source); - - default List map(Collection source) { - if (source == null) { - return Collections.emptyList(); - } - - List list = new ArrayList<>(source.size()); - for (NftAllowance allowance : source) { - list.add(map(allowance)); - } - - return list; - } } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java index f6088c3c43f..9c1fd9d648a 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java @@ -17,36 +17,17 @@ package com.hedera.mirror.restjava.mapper; import com.hedera.mirror.common.domain.token.TokenAirdrop; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Named; @Mapper(config = MapperConfiguration.class) -public interface TokenAirdropsMapper { +public interface TokenAirdropsMapper extends CollectionMapper { - @Mapping(source = "receiverAccountId", target = "receiverId") - @Mapping(source = "senderAccountId", target = "senderId") @Mapping(source = "serialNumber", target = "serialNumber", qualifiedByName = "mapToNullIfZero") @Mapping(source = "timestampRange", target = "timestamp") com.hedera.mirror.rest.model.TokenAirdrop map(TokenAirdrop source); - default List map(Collection source) { - if (source == null) { - return Collections.emptyList(); - } - - List list = new ArrayList<>(source.size()); - for (TokenAirdrop tokenAirdrop : source) { - list.add(map(tokenAirdrop)); - } - - return list; - } - @Named("mapToNullIfZero") default Long mapToNullIfZero(long serialNumber) { if (serialNumber == 0L) { diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java index a341f517155..946e6b7cef7 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java @@ -19,7 +19,7 @@ import static org.jooq.impl.DSL.noCondition; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.IntegerRangeParameter; +import com.hedera.mirror.restjava.common.NumberRangeParameter; import com.hedera.mirror.restjava.common.RangeOperator; import org.jooq.Condition; import org.jooq.Field; @@ -34,7 +34,7 @@ default Condition getCondition(Field field, EntityIdRangeParameter param) return getCondition(field, param.operator(), param.value().getId()); } - default Condition getCondition(Field field, IntegerRangeParameter param) { + default Condition getCondition(Field field, NumberRangeParameter param) { if (param == null) { return noCondition(); } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java index 8836859cb7a..f1919f96e7f 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java @@ -18,12 +18,12 @@ import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.token.TokenAirdrop; -import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import jakarta.validation.constraints.NotNull; import java.util.Collection; public interface TokenAirdropRepositoryCustom extends CustomRepository { @NotNull - Collection findAllOutstanding(OutstandingTokenAirdropRequest request, EntityId accountId); + Collection findAllOutstanding(TokenAirdropRequest request, EntityId accountId); } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java index b96d1f705b2..127f3cf5ee1 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java @@ -21,7 +21,7 @@ import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.token.TokenAirdrop; -import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import com.hedera.mirror.restjava.jooq.domain.enums.AirdropState; import jakarta.inject.Named; import java.util.Collection; @@ -37,24 +37,23 @@ class TokenAirdropRepositoryCustomImpl implements TokenAirdropRepositoryCustom { private final DSLContext dslContext; - private static final Map>> SORT_ORDERS = Map.of( + private static final Map>> OUTSTANDING_SORT_ORDERS = Map.of( new OrderSpec(true, Direction.ASC), List.of( - TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID.asc(), + TOKEN_AIRDROP.RECEIVER_ID.asc(), TOKEN_AIRDROP.TOKEN_ID.asc(), TOKEN_AIRDROP.SERIAL_NUMBER.asc()), new OrderSpec(true, Direction.DESC), List.of( - TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID.desc(), + TOKEN_AIRDROP.RECEIVER_ID.desc(), TOKEN_AIRDROP.TOKEN_ID.desc(), TOKEN_AIRDROP.SERIAL_NUMBER.desc()), - new OrderSpec(false, Direction.ASC), - List.of(TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID.asc(), TOKEN_AIRDROP.TOKEN_ID.asc()), + new OrderSpec(false, Direction.ASC), List.of(TOKEN_AIRDROP.RECEIVER_ID.asc(), TOKEN_AIRDROP.TOKEN_ID.asc()), new OrderSpec(false, Direction.DESC), - List.of(TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID.desc(), TOKEN_AIRDROP.TOKEN_ID.desc())); + List.of(TOKEN_AIRDROP.RECEIVER_ID.desc(), TOKEN_AIRDROP.TOKEN_ID.desc())); @Override - public Collection findAllOutstanding(OutstandingTokenAirdropRequest request, EntityId accountId) { + public Collection findAllOutstanding(TokenAirdropRequest request, EntityId accountId) { var serialNumberCondition = getCondition(TOKEN_AIRDROP.SERIAL_NUMBER, request.getSerialNumber()); var includeSerialNumber = !serialNumberCondition.equals(noCondition()); if (includeSerialNumber) { @@ -64,14 +63,14 @@ public Collection findAllOutstanding(OutstandingTokenAirdropReques } var condition = TOKEN_AIRDROP - .SENDER_ACCOUNT_ID + .SENDER_ID .eq(accountId.getId()) .and(TOKEN_AIRDROP.STATE.eq(AirdropState.PENDING)) - .and(getCondition(TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID, request.getReceiverId())) + .and(getCondition(TOKEN_AIRDROP.RECEIVER_ID, request.getEntityId())) .and(getCondition(TOKEN_AIRDROP.TOKEN_ID, request.getTokenId())) .and(serialNumberCondition); - var order = SORT_ORDERS.get(new OrderSpec(includeSerialNumber, request.getOrder())); + var order = OUTSTANDING_SORT_ORDERS.get(new OrderSpec(includeSerialNumber, request.getOrder())); return dslContext .selectFrom(TOKEN_AIRDROP) .where(condition) diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropService.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropService.java index 01713b6c1d8..7eff0d62585 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropService.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropService.java @@ -17,10 +17,10 @@ package com.hedera.mirror.restjava.service; import com.hedera.mirror.common.domain.token.TokenAirdrop; -import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import java.util.Collection; public interface TokenAirdropService { - Collection getOutstandingTokenAirdrops(OutstandingTokenAirdropRequest request); + Collection getOutstandingAirdrops(TokenAirdropRequest request); } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java index f5972501946..95ac76b97cd 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java @@ -16,8 +16,10 @@ package com.hedera.mirror.restjava.service; +import static com.hedera.mirror.restjava.common.Constants.SENDER_ID; + import com.hedera.mirror.common.domain.token.TokenAirdrop; -import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import com.hedera.mirror.restjava.repository.TokenAirdropRepository; import jakarta.inject.Named; import java.util.Collection; @@ -30,8 +32,12 @@ public class TokenAirdropServiceImpl implements TokenAirdropService { private final EntityService entityService; private final TokenAirdropRepository repository; - public Collection getOutstandingTokenAirdrops(OutstandingTokenAirdropRequest request) { - var id = entityService.lookup(request.getSenderId()); + public Collection getOutstandingAirdrops(TokenAirdropRequest request) { + var accountId = request.getAccountId(); + if (accountId == null) { + throw new IllegalArgumentException(SENDER_ID + " is required"); + } + var id = entityService.lookup(accountId); return repository.findAllOutstanding(request, id); } } diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/IntegerRangeParameterTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java similarity index 68% rename from hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/IntegerRangeParameterTest.java rename to hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java index 36aeff212ca..a44a378769c 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/IntegerRangeParameterTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -34,7 +35,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class IntegerRangeParameterTest { +class NumberRangeParameterTest { @Mock private RestJavaProperties properties; @@ -53,19 +54,28 @@ void closeMocks() { } @Test - void testConversion() { - assertThat(new IntegerRangeParameter(RangeOperator.GTE, 2000)) - .isEqualTo(IntegerRangeParameter.valueOf("gte:2000")); - assertThat(new IntegerRangeParameter(RangeOperator.EQ, 2000)).isEqualTo(IntegerRangeParameter.valueOf("2000")); - assertThat(IntegerRangeParameter.EMPTY) - .isEqualTo(IntegerRangeParameter.valueOf("")) - .isEqualTo(IntegerRangeParameter.valueOf(null)); + void testNoOperatorPresent() { + assertThat(new NumberRangeParameter(RangeOperator.EQ, 2000L)).isEqualTo(NumberRangeParameter.valueOf("2000")); } @ParameterizedTest - @ValueSource(strings = {"a", ".1", "someinvalidstring"}) + @EnumSource(RangeOperator.class) + void testGte(RangeOperator operator) { + assertThat(new NumberRangeParameter(operator, 2000L)) + .isEqualTo(NumberRangeParameter.valueOf(operator + ":2000")); + } + + @Test + void testEmpty() { + assertThat(NumberRangeParameter.EMPTY) + .isEqualTo(NumberRangeParameter.valueOf("")) + .isEqualTo(NumberRangeParameter.valueOf(null)); + } + + @ParameterizedTest + @ValueSource(strings = {"a", ".1", "someinvalidstring", "-1", "9223372036854775808", ":2000"}) @DisplayName("IntegerRangeParameter parse from string tests, negative cases") void testInvalidParam(String input) { - assertThrows(IllegalArgumentException.class, () -> IntegerRangeParameter.valueOf(input)); + assertThrows(IllegalArgumentException.class, () -> NumberRangeParameter.valueOf(input)); } } diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java index 15f52063016..11cee08eafa 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java @@ -29,11 +29,14 @@ import com.hedera.mirror.restjava.mapper.TokenAirdropsMapper; import java.util.List; import lombok.RequiredArgsConstructor; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestClient.RequestHeadersSpec; import org.springframework.web.client.RestClient.RequestHeadersUriSpec; @@ -54,7 +57,7 @@ protected String getUrl() { @Override protected RequestHeadersSpec defaultRequest(RequestHeadersUriSpec uriSpec) { var tokenAirdrop = domainBuilder.tokenAirdrop(FUNGIBLE_COMMON).persist(); - return uriSpec.uri("", tokenAirdrop.getSenderAccountId()); + return uriSpec.uri("", tokenAirdrop.getSenderId()); } @ValueSource(strings = {"1000", "0.1000", "0.0.1000"}) @@ -63,7 +66,7 @@ void outstandingAirdropByEntityId(String id) { // Given var tokenAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderAccountId(1000L)) + .customize(a -> a.senderId(1000L)) .persist(); // When @@ -82,7 +85,7 @@ void evmAddressOutstanding() { var entity = domainBuilder.entity().persist(); var tokenAirdrop = domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderAccountId(entity.getId())) + .customize(a -> a.senderId(entity.getId())) .persist(); // When @@ -102,7 +105,7 @@ void aliasOutstanding() { var entity = domainBuilder.entity().persist(); var tokenAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderAccountId(entity.getId())) + .customize(a -> a.senderId(entity.getId())) .persist(); // When @@ -123,19 +126,18 @@ void followAscendingOrderLinkOutstanding() { var id = entity.getId(); var tokenAirdrop1 = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderAccountId(entity.getId())) + .customize(a -> a.senderId(entity.getId())) .persist(); var tokenAirdrop2 = domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderAccountId(entity.getId())) + .customize(a -> a.senderId(entity.getId())) .persist(); var baseLink = "/api/v1/accounts/%d/airdrops/outstanding".formatted(id); // When var result = restClient.get().uri("?limit=1", id).retrieve().body(TokenAirdropsResponse.class); var nextParams = "?limit=1&receiver.id=gte:%s&token.id=gt:%s" - .formatted( - EntityId.of(tokenAirdrop1.getReceiverAccountId()), EntityId.of(tokenAirdrop1.getTokenId())); + .formatted(EntityId.of(tokenAirdrop1.getReceiverId()), EntityId.of(tokenAirdrop1.getTokenId())); // Then assertThat(result).isEqualTo(getExpectedResponse(List.of(tokenAirdrop1), baseLink + nextParams)); @@ -145,14 +147,13 @@ void followAscendingOrderLinkOutstanding() { // Then nextParams = "?limit=1&receiver.id=gte:%s&token.id=gt:%s" - .formatted( - EntityId.of(tokenAirdrop2.getReceiverAccountId()), EntityId.of(tokenAirdrop2.getTokenId())); + .formatted(EntityId.of(tokenAirdrop2.getReceiverId()), EntityId.of(tokenAirdrop2.getTokenId())); assertThat(result).isEqualTo(getExpectedResponse(List.of(tokenAirdrop2), baseLink + nextParams)); // When follow link 2 result = restClient .get() - .uri(nextParams, tokenAirdrop1.getReceiverAccountId()) + .uri(nextParams, tokenAirdrop1.getReceiverId()) .retrieve() .body(TokenAirdropsResponse.class); // Then @@ -171,28 +172,26 @@ void followDescendingOrderLinkOutstanding() { var tokenAirdrop1 = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderAccountId(sender) - .receiverAccountId(receiver) - .tokenId(fungibleTokenId)) + .customize(a -> a.senderId(sender).receiverId(receiver).tokenId(fungibleTokenId)) .persist(); var nftAirdrop = domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderAccountId(sender) - .receiverAccountId(receiver) + .customize(a -> a.senderId(sender) + .receiverId(receiver) .tokenId(token1) .serialNumber(serial1)) .persist(); var nftAirdrop2 = domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderAccountId(sender) - .receiverAccountId(receiver) + .customize(a -> a.senderId(sender) + .receiverId(receiver) .tokenId(token2) .serialNumber(serial1)) .persist(); domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.receiverAccountId(receiver)) + .customize(a -> a.receiverId(receiver)) .persist(); var uriParams = "?limit=1&receiver.id=gte:%s&order=desc".formatted(receiver); @@ -229,10 +228,138 @@ void followDescendingOrderLinkOutstanding() { assertThat(result).isEqualTo(getExpectedResponse(List.of(), null)); } - private TokenAirdropsResponse getExpectedResponse(List tokenAirdrops, String next) { - return new TokenAirdropsResponse() - .airdrops(mapper.map(tokenAirdrops)) - .links(new Links().next(next)); + @ParameterizedTest + @ValueSource( + strings = { + "0.0x000000000000000000000000000000000186Fb1b", + "0.0.0x000000000000000000000000000000000186Fb1b", + "0x000000000000000000000000000000000186Fb1b", + "0.0.AABBCC22", + "0.AABBCC22", + "AABBCC22" + }) + void notFound(String accountId) { + // When + ThrowingCallable callable = + () -> restClient.get().uri("", accountId).retrieve().body(TokenAirdropsResponse.class); + + // Then + validateError(callable, HttpClientErrorException.NotFound.class, "No account found for the given ID"); + } + + @ParameterizedTest + @ValueSource( + strings = { + "abc", + "a.b.c", + "0.0.", + "0.65537.1001", + "0.0.-1001", + "9223372036854775807", + "0x00000001000000000000000200000000000000034" + }) + void invalidId(String id) { + // When + ThrowingCallable callable = + () -> restClient.get().uri("", id).retrieve().body(TokenAirdropsResponse.class); + + // Then + validateError( + callable, + HttpClientErrorException.BadRequest.class, + "Failed to convert 'id' with value: '" + id + "'"); + } + + @ParameterizedTest + @ValueSource( + strings = { + "abc", + "a.b.c", + "0.0.", + "0.65537.1001", + "0.0.-1001", + "9223372036854775807", + "0x00000001000000000000000200000000000000034" + }) + void invalidAccountId(String accountId) { + // When + ThrowingCallable callable = () -> restClient + .get() + .uri("?receiver.id={accountId}", "0.0.1001", accountId) + .retrieve() + .body(TokenAirdropsResponse.class); + + // Then + validateError( + callable, + HttpClientErrorException.BadRequest.class, + "Failed to convert 'receiver.id' with value: '" + accountId + "'"); + } + + @ParameterizedTest + @CsvSource({ + "101, limit must be less than or equal to 100", + "-1, limit must be greater than 0", + "a, Failed to convert 'limit' with value: 'a'" + }) + void invalidLimit(String limit, String expected) { + // When + ThrowingCallable callable = () -> restClient + .get() + .uri("?limit={limit}", "0.0.1001", limit) + .retrieve() + .body(TokenAirdropsResponse.class); + + // Then + validateError(callable, HttpClientErrorException.BadRequest.class, expected); + } + + @ParameterizedTest + @CsvSource({ + "ascending, Failed to convert 'order' with value: 'ascending'", + "dsc, Failed to convert 'order' with value: 'dsc'", + "invalid, Failed to convert 'order' with value: 'invalid'" + }) + void invalidOrder(String order, String expected) { + // When + ThrowingCallable callable = () -> restClient + .get() + .uri("?order={order}", "0.0.1001", order) + .retrieve() + .body(TokenAirdropsResponse.class); + + // Then + validateError(callable, HttpClientErrorException.BadRequest.class, expected); + } + + @ParameterizedTest + @ValueSource( + strings = { + "abc", + "a.b.c", + "0.0.", + "0.65537.1001", + "0.0.-1001", + "9223372036854775807", + "0x00000001000000000000000200000000000000034" + }) + void invalidTokenId(String tokenId) { + // When + ThrowingCallable callable = () -> restClient + .get() + .uri("?token.id={tokenId}", "0.0.1001", tokenId) + .retrieve() + .body(TokenAirdropsResponse.class); + + // Then + validateError( + callable, + HttpClientErrorException.BadRequest.class, + "Failed to convert 'token.id' with value: '" + tokenId + "'"); } } + + private TokenAirdropsResponse getExpectedResponse(List tokenAirdrops, String next) { + return new TokenAirdropsResponse().airdrops(mapper.map(tokenAirdrops)).links(new Links().next(next)); + } } diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java index 4e9fbda42c1..2596ef1182b 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java @@ -53,8 +53,8 @@ void map(TokenTypeEnum tokenType) { assertThat(mapper.map(List.of(tokenAirdrop))) .first() .returns(tokenType == NON_FUNGIBLE_UNIQUE ? null : tokenAirdrop.getAmount(), TokenAirdrop::getAmount) - .returns(EntityId.of(tokenAirdrop.getReceiverAccountId()).toString(), TokenAirdrop::getReceiverId) - .returns(EntityId.of(tokenAirdrop.getSenderAccountId()).toString(), TokenAirdrop::getSenderId) + .returns(EntityId.of(tokenAirdrop.getReceiverId()).toString(), TokenAirdrop::getReceiverId) + .returns(EntityId.of(tokenAirdrop.getSenderId()).toString(), TokenAirdrop::getSenderId) .returns( tokenType == FUNGIBLE_COMMON ? null : tokenAirdrop.getSerialNumber(), TokenAirdrop::getSerialNumber) diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java index dd333d9f9b5..d10738e3d0f 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java @@ -25,9 +25,9 @@ import com.hedera.mirror.restjava.RestJavaIntegrationTest; import com.hedera.mirror.restjava.common.EntityIdNumParameter; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.IntegerRangeParameter; +import com.hedera.mirror.restjava.common.NumberRangeParameter; import com.hedera.mirror.restjava.common.RangeOperator; -import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import java.util.List; import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; @@ -49,9 +49,9 @@ void findById() { @Test void findBySenderId() { var tokenAirdrop = domainBuilder.tokenAirdrop(FUNGIBLE_COMMON).persist(); - var entityId = EntityId.of(tokenAirdrop.getSenderAccountId()); - var request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(entityId)) + var entityId = EntityId.of(tokenAirdrop.getSenderId()); + var request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(entityId)) .build(); assertThat(repository.findAllOutstanding(request, entityId)).contains(tokenAirdrop); } @@ -65,12 +65,11 @@ void findBySenderIdOrder(Direction order) { .persist(); var tokenAirdrop2 = domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> - a.senderAccountId(tokenAirdrop.getSenderAccountId()).timestampRange(Range.atLeast(2000L))) + .customize(a -> a.senderId(tokenAirdrop.getSenderId()).timestampRange(Range.atLeast(2000L))) .persist(); - var entityId = EntityId.of(tokenAirdrop.getSenderAccountId()); - var outstandingTokenAirdropRequest = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(entityId)) + var entityId = EntityId.of(tokenAirdrop.getSenderId()); + var outstandingTokenAirdropRequest = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(entityId)) .order(order) .build(); @@ -83,11 +82,10 @@ void findBySenderIdOrder(Direction order) { @Test void noMatch() { var tokenAirdrop = domainBuilder.tokenAirdrop(FUNGIBLE_COMMON).persist(); - var entityId = EntityId.of(tokenAirdrop.getSenderAccountId()); - var request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(entityId)) - .receiverId( - new EntityIdRangeParameter(RangeOperator.GT, EntityId.of(tokenAirdrop.getReceiverAccountId()))) + var entityId = EntityId.of(tokenAirdrop.getSenderId()); + var request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(entityId)) + .entityId(new EntityIdRangeParameter(RangeOperator.GT, EntityId.of(tokenAirdrop.getReceiverId()))) .build(); assertThat(repository.findAllOutstanding(request, entityId)).isEmpty(); } @@ -98,26 +96,26 @@ void conditionalClauses(Direction order) { var sender = domainBuilder.entity().get(); var receiver = domainBuilder.entity().get(); var token = domainBuilder.token().get(); - var serialNumber = 5; + var serialNumber = 5L; var tokenAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderAccountId(sender.getId()).receiverAccountId(receiver.getId())) + .customize(a -> a.senderId(sender.getId()).receiverId(receiver.getId())) .persist(); var tokenAirdrop2 = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderAccountId(sender.getId()).tokenId(token.getTokenId())) + .customize(a -> a.senderId(sender.getId()).tokenId(token.getTokenId())) .persist(); var nftAirdrop = domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderAccountId(sender.getId()) - .receiverAccountId(receiver.getId()) + .customize(a -> a.senderId(sender.getId()) + .receiverId(receiver.getId()) .serialNumber(serialNumber) .tokenId(token.getTokenId())) .persist(); var nftAirdrop2 = domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderAccountId(sender.getId()).serialNumber(serialNumber)) + .customize(a -> a.senderId(sender.getId()).serialNumber(serialNumber)) .persist(); // Default asc ordering by receiver, tokenId @@ -127,8 +125,8 @@ void conditionalClauses(Direction order) { var serialNumberAirdrops = List.of(nftAirdrop, nftAirdrop2); var orderedAirdrops = order.isAscending() ? allAirdrops : allAirdrops.reversed(); - var request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(sender.toEntityId())) + var request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(sender.toEntityId())) .order(order) .build(); assertThat(repository.findAllOutstanding(request, sender.toEntityId())) @@ -136,8 +134,8 @@ void conditionalClauses(Direction order) { // With token id condition var tokenIdAirdrops = order.isAscending() ? tokenSpecifiedAirdrops : tokenSpecifiedAirdrops.reversed(); - request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(sender.toEntityId())) + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(sender.toEntityId())) .order(order) .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(token.getTokenId()))) .build(); @@ -146,20 +144,20 @@ void conditionalClauses(Direction order) { // With receiver id condition var receiverIdAirdrops = order.isAscending() ? receiverSpecifiedAirdrops : receiverSpecifiedAirdrops.reversed(); - request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(sender.toEntityId())) + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(sender.toEntityId())) .order(order) - .receiverId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver.getId()))) + .entityId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver.getId()))) .build(); assertThat(repository.findAllOutstanding(request, sender.toEntityId())) .containsExactlyElementsOf(receiverIdAirdrops); // With serial number condition var serialNumberAirdropsOrdered = order.isAscending() ? serialNumberAirdrops : serialNumberAirdrops.reversed(); - request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(sender.toEntityId())) + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(sender.toEntityId())) .order(order) - .serialNumber(new IntegerRangeParameter(RangeOperator.EQ, serialNumber)) + .serialNumber(new NumberRangeParameter(RangeOperator.EQ, serialNumber)) .build(); assertThat(repository.findAllOutstanding(request, sender.toEntityId())) .containsExactlyElementsOf(serialNumberAirdropsOrdered); @@ -178,86 +176,85 @@ void serialNumber() { var airdrop1 = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> - a.senderAccountId(sender).receiverAccountId(receiver).tokenId(tokenId)) + .customize(a -> a.senderId(sender).receiverId(receiver).tokenId(tokenId)) .persist(); var serialAirdrop1 = domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderAccountId(sender) - .receiverAccountId(receiver) + .customize(a -> a.senderId(sender) + .receiverId(receiver) .serialNumber(serialNumber) .tokenId(nftTokenId)) .persist(); var serialAirdrop2 = domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderAccountId(sender) - .receiverAccountId(receiver) + .customize(a -> a.senderId(sender) + .receiverId(receiver) .serialNumber(serialNumber2) .tokenId(nftTokenId)) .persist(); var serialAirdrop3 = domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderAccountId(sender) - .receiverAccountId(receiver2) + .customize(a -> a.senderId(sender) + .receiverId(receiver2) .serialNumber(serialNumber) .tokenId(nftTokenId2)) .persist(); var expectedAirdrops = List.of(serialAirdrop1, serialAirdrop2, serialAirdrop3); - var request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(EntityId.of(sender))) - .serialNumber(new IntegerRangeParameter(RangeOperator.GT, 4)) + var request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(EntityId.of(sender))) + .serialNumber(new NumberRangeParameter(RangeOperator.GT, 4L)) .build(); assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); expectedAirdrops = List.of(serialAirdrop1, serialAirdrop3); - request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(EntityId.of(sender))) - .serialNumber(new IntegerRangeParameter(RangeOperator.LT, 10)) + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(EntityId.of(sender))) + .serialNumber(new NumberRangeParameter(RangeOperator.LT, 10L)) .build(); assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); expectedAirdrops = List.of(serialAirdrop1, serialAirdrop2); - request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(EntityId.of(sender))) + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(EntityId.of(sender))) .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(nftTokenId))) .build(); assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); expectedAirdrops = List.of(serialAirdrop1); - request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(EntityId.of(sender))) + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(EntityId.of(sender))) .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(nftTokenId))) - .serialNumber(new IntegerRangeParameter(RangeOperator.LTE, 5)) + .serialNumber(new NumberRangeParameter(RangeOperator.LTE, 5L)) .build(); assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); expectedAirdrops = List.of(serialAirdrop2); - request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(EntityId.of(sender))) + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(EntityId.of(sender))) .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(nftTokenId))) - .serialNumber(new IntegerRangeParameter(RangeOperator.GTE, 10)) + .serialNumber(new NumberRangeParameter(RangeOperator.GTE, 10L)) .build(); assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); expectedAirdrops = List.of(airdrop1, serialAirdrop1, serialAirdrop2); - request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(EntityId.of(sender))) - .receiverId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver))) + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(EntityId.of(sender))) + .entityId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver))) .build(); assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); expectedAirdrops = List.of(serialAirdrop1); - request = OutstandingTokenAirdropRequest.builder() - .senderId(new EntityIdNumParameter(EntityId.of(sender))) - .receiverId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver))) - .serialNumber(new IntegerRangeParameter(RangeOperator.LTE, 5)) + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(EntityId.of(sender))) + .entityId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver))) + .serialNumber(new NumberRangeParameter(RangeOperator.LTE, 5L)) .build(); assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) .containsExactlyElementsOf(expectedAirdrops); diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java index 93cab2a3024..ca1efaaf0ac 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java @@ -18,15 +18,16 @@ import static com.hedera.mirror.common.domain.token.TokenTypeEnum.FUNGIBLE_COMMON; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.restjava.RestJavaIntegrationTest; import com.hedera.mirror.restjava.common.EntityIdAliasParameter; +import com.hedera.mirror.restjava.common.EntityIdEvmAddressParameter; import com.hedera.mirror.restjava.common.EntityIdNumParameter; -import com.hedera.mirror.restjava.dto.OutstandingTokenAirdropRequest; +import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; -import org.springframework.data.domain.Sort.Direction; @RequiredArgsConstructor class TokenAirdropServiceTest extends RestJavaIntegrationTest { @@ -37,21 +38,19 @@ class TokenAirdropServiceTest extends RestJavaIntegrationTest { private static final EntityId TOKEN_ID = EntityId.of(5000L); @Test - void getOutstandingTokenAirdrops() { + void getOutstandingAirdrops() { var fungibleAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) .customize(a -> a.amount(100L) - .receiverAccountId(RECEIVER.getId()) - .senderAccountId(SENDER.getId()) + .receiverId(RECEIVER.getId()) + .senderId(SENDER.getId()) .tokenId(TOKEN_ID.getId())) .persist(); - var request = OutstandingTokenAirdropRequest.builder() - .limit(2) - .senderId(new EntityIdNumParameter(SENDER)) - .order(Direction.ASC) + var request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(SENDER)) .build(); - var response = service.getOutstandingTokenAirdrops(request); + var response = service.getOutstandingAirdrops(request); assertThat(response).containsExactly(fungibleAirdrop); } @@ -60,14 +59,42 @@ void getOutstandingAirdropByAlias() { var entity = domainBuilder.entity().persist(); var tokenAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderAccountId(entity.getId())) + .customize(a -> a.senderId(entity.getId())) .persist(); - var request = OutstandingTokenAirdropRequest.builder() - .limit(2) - .senderId(new EntityIdAliasParameter(entity.getShard(), entity.getRealm(), entity.getAlias())) - .order(Direction.ASC) + var request = TokenAirdropRequest.builder() + .accountId(new EntityIdAliasParameter(entity.getShard(), entity.getRealm(), entity.getAlias())) .build(); - var response = service.getOutstandingTokenAirdrops(request); + var response = service.getOutstandingAirdrops(request); assertThat(response).containsExactly(tokenAirdrop); } + + @Test + void getOutstandingAirdropByEvmAddress() { + var entity = domainBuilder.entity().persist(); + var tokenAirdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderId(entity.getId())) + .persist(); + var request = TokenAirdropRequest.builder() + .accountId( + new EntityIdEvmAddressParameter(entity.getShard(), entity.getRealm(), entity.getEvmAddress())) + .build(); + var response = service.getOutstandingAirdrops(request); + assertThat(response).containsExactly(tokenAirdrop); + } + + @Test + void getOutstandingAirdropNotFound() { + var request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(SENDER)) + .build(); + var response = service.getOutstandingAirdrops(request); + assertThat(response).isEmpty(); + } + + @Test + void getOutstandingNoSender() { + var request = TokenAirdropRequest.builder().build(); + assertThrows(IllegalArgumentException.class, () -> service.getOutstandingAirdrops(request)); + } } diff --git a/hedera-mirror-rest/api/v1/openapi.yml b/hedera-mirror-rest/api/v1/openapi.yml index 1c0600a8983..410871291d0 100644 --- a/hedera-mirror-rest/api/v1/openapi.yml +++ b/hedera-mirror-rest/api/v1/openapi.yml @@ -162,18 +162,17 @@ paths: $ref: "#/components/responses/NotFoundError" tags: - accounts - /api/v1/accounts/{senderIdOrAliasOrEvmAddress}/airdrops/outstanding: + /api/v1/accounts/{idOrAliasOrEvmAddress}/airdrops/outstanding: get: - summary: Get get pending airdrops sent by an account + summary: Get get outstanding airdrops sent by an account description: | - Returns pending airdrops that have been sent by an account. + Returns outstanding airdrops that have been sent by an account. operationId: getOutstandingTokenAirdrops parameters: - $ref: "#/components/parameters/accountIdOrAliasOrEvmAddressPathParam" - $ref: "#/components/parameters/limitQueryParam" - $ref: "#/components/parameters/orderQueryParam" - - $ref: "#/components/parameters/receiverIdOrAliasOrEvmAddressQueryParam" - - $ref: "#/components/parameters/serialNumberQueryParam" + - $ref: "#/components/parameters/receiverIdQueryParam" - $ref: "#/components/parameters/tokenIdQueryParam" responses: 200: @@ -187,7 +186,7 @@ paths: 404: $ref: "#/components/responses/NotFoundError" tags: - - accounts + - airdrops /api/v1/accounts/{idOrAliasOrEvmAddress}/allowances/crypto: get: summary: Get crypto allowances for an account info @@ -3352,6 +3351,7 @@ components: serial_number: example: 1 format: int64 + nullable: true type: integer timestamp: $ref: "#/components/schemas/TimestampRange" @@ -4454,26 +4454,40 @@ components: example: 3c3d546321ff6f63d701d2ec5c277095874e19f4a235bee1e6bb19258bf362be schema: type: string - receiverIdOrAliasOrEvmAddressQueryParam: + receiverIdQueryParam: name: receiver.id + description: The ID of the receiver to return information for in: query - description: Receiver account id or account alias with no shard realm or evm address with no shard realm examples: - aliasOnly: - value: HIQQEXWKW53RKN4W6XXC4Q232SYNZ3SZANVZZSUME5B5PRGXL663UAQA - accountNumOnly: - value: 8 - realmAccountNum: - value: 0.8 - shardRealmAccountNum: - value: 0.0.8 - evmAddress: - value: ac384c53f03855fa1b3616052f8ba32c6c2a2fec - evmAddressWithPrefix: - value: 0xac384c53f03855fa1b3616052f8ba32c6c2a2fec + noValue: + summary: -- + value: "" + entityNumNoOperator: + summary: Example of entityNum equals with no operator + value: 100 + idNoOperator: + summary: Example of id equals with no operator + value: 0.0.100 + entityNumEqOperator: + summary: Example of entityNum equals operator + value: eq:200 + idEqOperator: + summary: Example of id equals operator + value: eq:0.0.200 + idGtOperator: + summary: Example of id greather than operator + value: gt:0.0.200 + idGteOperator: + summary: Example of id greather than or equal to operator + value: gte:0.0.200 + idLtOperator: + summary: Example of id less than operator + value: lt:0.0.200 + idLteOperator: + summary: Example of id less than or equal to operator + value: lte:0.0.200 schema: - pattern: ^(\d{1,10}\.){0,2}(\d{1,10}|(0x)?[A-Fa-f0-9]{40}|(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}|[A-Z2-7]{4,5}|[A-Z2-7]{7,8}))$ - type: string + $ref: "#/components/schemas/EntityIdQuery" scheduledQueryParam: name: scheduled in: query From a404dd82650064f4288de5b60586241a3b7619d3 Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Wed, 11 Sep 2024 17:33:47 -0400 Subject: [PATCH 05/10] Remove serial number parameter for now Signed-off-by: Edwin Greene --- .../controller/TokenAirdropsController.java | 4 - .../restjava/dto/TokenAirdropRequest.java | 3 - .../restjava/repository/CustomRepository.java | 9 -- .../TokenAirdropRepositoryCustomImpl.java | 33 +----- .../TokenAirdropRepositoryTest.java | 108 ------------------ 5 files changed, 5 insertions(+), 152 deletions(-) diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java index 682bdb2fffc..d288c7e3d1e 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java @@ -19,7 +19,6 @@ import static com.hedera.mirror.restjava.common.Constants.DEFAULT_LIMIT; import static com.hedera.mirror.restjava.common.Constants.MAX_LIMIT; import static com.hedera.mirror.restjava.common.Constants.RECEIVER_ID; -import static com.hedera.mirror.restjava.common.Constants.SERIAL_NUMBER; import static com.hedera.mirror.restjava.common.Constants.TOKEN_ID; import com.google.common.collect.ImmutableSortedMap; @@ -28,7 +27,6 @@ import com.hedera.mirror.restjava.common.EntityIdParameter; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; import com.hedera.mirror.restjava.common.LinkFactory; -import com.hedera.mirror.restjava.common.NumberRangeParameter; import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import com.hedera.mirror.restjava.mapper.TokenAirdropsMapper; import com.hedera.mirror.restjava.service.TokenAirdropService; @@ -65,14 +63,12 @@ TokenAirdropsResponse getOutstandingAirdrops( @RequestParam(defaultValue = DEFAULT_LIMIT) @Positive @Max(MAX_LIMIT) int limit, @RequestParam(defaultValue = "asc") Sort.Direction order, @RequestParam(name = RECEIVER_ID, required = false) EntityIdRangeParameter receiverId, - @RequestParam(name = SERIAL_NUMBER, required = false) NumberRangeParameter serialNumber, @RequestParam(name = TOKEN_ID, required = false) EntityIdRangeParameter tokenId) { var request = TokenAirdropRequest.builder() .accountId(id) .entityId(receiverId) .limit(limit) .order(order) - .serialNumber(serialNumber) .tokenId(tokenId) .build(); var response = service.getOutstandingAirdrops(request); diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java index 06450e91dee..b17042730f9 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java @@ -18,7 +18,6 @@ import com.hedera.mirror.restjava.common.EntityIdParameter; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.NumberRangeParameter; import lombok.Builder; import lombok.Data; import org.springframework.data.domain.Sort; @@ -39,7 +38,5 @@ public class TokenAirdropRequest { // Receiver Id for Outstanding Airdrops, Sender Id for Pending Airdrops private EntityIdRangeParameter entityId; - private NumberRangeParameter serialNumber; - private EntityIdRangeParameter tokenId; } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java index 946e6b7cef7..6cae1621279 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java @@ -19,7 +19,6 @@ import static org.jooq.impl.DSL.noCondition; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.NumberRangeParameter; import com.hedera.mirror.restjava.common.RangeOperator; import org.jooq.Condition; import org.jooq.Field; @@ -34,14 +33,6 @@ default Condition getCondition(Field field, EntityIdRangeParameter param) return getCondition(field, param.operator(), param.value().getId()); } - default Condition getCondition(Field field, NumberRangeParameter param) { - if (param == null) { - return noCondition(); - } - - return getCondition(field, param.operator(), param.value().longValue()); - } - default Condition getCondition(Field field, RangeOperator operator, Long value) { return operator.getFunction().apply(field, value); } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java index 127f3cf5ee1..c8f8298a602 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java @@ -17,7 +17,6 @@ package com.hedera.mirror.restjava.repository; import static com.hedera.mirror.restjava.jooq.domain.Tables.TOKEN_AIRDROP; -import static org.jooq.impl.DSL.noCondition; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.token.TokenAirdrop; @@ -37,40 +36,20 @@ class TokenAirdropRepositoryCustomImpl implements TokenAirdropRepositoryCustom { private final DSLContext dslContext; - private static final Map>> OUTSTANDING_SORT_ORDERS = Map.of( - new OrderSpec(true, Direction.ASC), - List.of( - TOKEN_AIRDROP.RECEIVER_ID.asc(), - TOKEN_AIRDROP.TOKEN_ID.asc(), - TOKEN_AIRDROP.SERIAL_NUMBER.asc()), - new OrderSpec(true, Direction.DESC), - List.of( - TOKEN_AIRDROP.RECEIVER_ID.desc(), - TOKEN_AIRDROP.TOKEN_ID.desc(), - TOKEN_AIRDROP.SERIAL_NUMBER.desc()), - new OrderSpec(false, Direction.ASC), List.of(TOKEN_AIRDROP.RECEIVER_ID.asc(), TOKEN_AIRDROP.TOKEN_ID.asc()), - new OrderSpec(false, Direction.DESC), - List.of(TOKEN_AIRDROP.RECEIVER_ID.desc(), TOKEN_AIRDROP.TOKEN_ID.desc())); + private static final Map>> OUTSTANDING_SORT_ORDERS = Map.of( + Direction.ASC, List.of(TOKEN_AIRDROP.RECEIVER_ID.asc(), TOKEN_AIRDROP.TOKEN_ID.asc()), + Direction.DESC, List.of(TOKEN_AIRDROP.RECEIVER_ID.desc(), TOKEN_AIRDROP.TOKEN_ID.desc())); @Override public Collection findAllOutstanding(TokenAirdropRequest request, EntityId accountId) { - var serialNumberCondition = getCondition(TOKEN_AIRDROP.SERIAL_NUMBER, request.getSerialNumber()); - var includeSerialNumber = !serialNumberCondition.equals(noCondition()); - if (includeSerialNumber) { - // If the query includes a serial number, explicitly remove fungible tokens from the result as they have a - // serial number of 0 - serialNumberCondition = serialNumberCondition.and(TOKEN_AIRDROP.SERIAL_NUMBER.ne(0L)); - } - var condition = TOKEN_AIRDROP .SENDER_ID .eq(accountId.getId()) .and(TOKEN_AIRDROP.STATE.eq(AirdropState.PENDING)) .and(getCondition(TOKEN_AIRDROP.RECEIVER_ID, request.getEntityId())) - .and(getCondition(TOKEN_AIRDROP.TOKEN_ID, request.getTokenId())) - .and(serialNumberCondition); + .and(getCondition(TOKEN_AIRDROP.TOKEN_ID, request.getTokenId())); - var order = OUTSTANDING_SORT_ORDERS.get(new OrderSpec(includeSerialNumber, request.getOrder())); + var order = OUTSTANDING_SORT_ORDERS.get(request.getOrder()); return dslContext .selectFrom(TOKEN_AIRDROP) .where(condition) @@ -78,6 +57,4 @@ public Collection findAllOutstanding(TokenAirdropRequest request, .limit(request.getLimit()) .fetchInto(TokenAirdrop.class); } - - private record OrderSpec(boolean includeSerialNumber, Direction direction) {} } diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java index d10738e3d0f..8feb0773399 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java @@ -25,7 +25,6 @@ import com.hedera.mirror.restjava.RestJavaIntegrationTest; import com.hedera.mirror.restjava.common.EntityIdNumParameter; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.NumberRangeParameter; import com.hedera.mirror.restjava.common.RangeOperator; import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import java.util.List; @@ -151,112 +150,5 @@ void conditionalClauses(Direction order) { .build(); assertThat(repository.findAllOutstanding(request, sender.toEntityId())) .containsExactlyElementsOf(receiverIdAirdrops); - - // With serial number condition - var serialNumberAirdropsOrdered = order.isAscending() ? serialNumberAirdrops : serialNumberAirdrops.reversed(); - request = TokenAirdropRequest.builder() - .accountId(new EntityIdNumParameter(sender.toEntityId())) - .order(order) - .serialNumber(new NumberRangeParameter(RangeOperator.EQ, serialNumber)) - .build(); - assertThat(repository.findAllOutstanding(request, sender.toEntityId())) - .containsExactlyElementsOf(serialNumberAirdropsOrdered); - } - - @Test - void serialNumber() { - var sender = 1000; - var receiver = 3000; - var receiver2 = 4000; - var tokenId = 5000; - var nftTokenId = 6000; - var nftTokenId2 = 7000; - var serialNumber = 5; - var serialNumber2 = 10; - - var airdrop1 = domainBuilder - .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderId(sender).receiverId(receiver).tokenId(tokenId)) - .persist(); - var serialAirdrop1 = domainBuilder - .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderId(sender) - .receiverId(receiver) - .serialNumber(serialNumber) - .tokenId(nftTokenId)) - .persist(); - var serialAirdrop2 = domainBuilder - .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderId(sender) - .receiverId(receiver) - .serialNumber(serialNumber2) - .tokenId(nftTokenId)) - .persist(); - var serialAirdrop3 = domainBuilder - .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderId(sender) - .receiverId(receiver2) - .serialNumber(serialNumber) - .tokenId(nftTokenId2)) - .persist(); - - var expectedAirdrops = List.of(serialAirdrop1, serialAirdrop2, serialAirdrop3); - var request = TokenAirdropRequest.builder() - .accountId(new EntityIdNumParameter(EntityId.of(sender))) - .serialNumber(new NumberRangeParameter(RangeOperator.GT, 4L)) - .build(); - assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) - .containsExactlyElementsOf(expectedAirdrops); - - expectedAirdrops = List.of(serialAirdrop1, serialAirdrop3); - request = TokenAirdropRequest.builder() - .accountId(new EntityIdNumParameter(EntityId.of(sender))) - .serialNumber(new NumberRangeParameter(RangeOperator.LT, 10L)) - .build(); - assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) - .containsExactlyElementsOf(expectedAirdrops); - - expectedAirdrops = List.of(serialAirdrop1, serialAirdrop2); - request = TokenAirdropRequest.builder() - .accountId(new EntityIdNumParameter(EntityId.of(sender))) - .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(nftTokenId))) - .build(); - assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) - .containsExactlyElementsOf(expectedAirdrops); - - expectedAirdrops = List.of(serialAirdrop1); - request = TokenAirdropRequest.builder() - .accountId(new EntityIdNumParameter(EntityId.of(sender))) - .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(nftTokenId))) - .serialNumber(new NumberRangeParameter(RangeOperator.LTE, 5L)) - .build(); - assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) - .containsExactlyElementsOf(expectedAirdrops); - - expectedAirdrops = List.of(serialAirdrop2); - request = TokenAirdropRequest.builder() - .accountId(new EntityIdNumParameter(EntityId.of(sender))) - .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(nftTokenId))) - .serialNumber(new NumberRangeParameter(RangeOperator.GTE, 10L)) - .build(); - assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) - .containsExactlyElementsOf(expectedAirdrops); - - expectedAirdrops = List.of(airdrop1, serialAirdrop1, serialAirdrop2); - request = TokenAirdropRequest.builder() - .accountId(new EntityIdNumParameter(EntityId.of(sender))) - .entityId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver))) - .build(); - assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) - .containsExactlyElementsOf(expectedAirdrops); - - expectedAirdrops = List.of(serialAirdrop1); - request = TokenAirdropRequest.builder() - .accountId(new EntityIdNumParameter(EntityId.of(sender))) - .entityId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver))) - .serialNumber(new NumberRangeParameter(RangeOperator.LTE, 5L)) - .build(); - assertThat(repository.findAllOutstanding(request, EntityId.of(sender))) - .containsExactlyElementsOf(expectedAirdrops); } } From bc82e6c6a258b262d5d429c9a8ee9c73423bb613 Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Thu, 12 Sep 2024 16:06:17 -0400 Subject: [PATCH 06/10] Updates per feedback. Repository class changes Signed-off-by: Edwin Greene --- .../domain/token/AbstractTokenAirdrop.java | 12 +- .../mirror/common/domain/DomainBuilder.java | 8 +- ...tTokenUpdateAirdropTransactionHandler.java | 4 +- .../TokenAirdropTransactionHandler.java | 4 +- .../db/migration/common/R__01_temp_tables.sql | 2 +- .../v1/V1.100.0__add_token_airdrop.sql | 8 +- .../v2/R__02_temp_table_distribution.sql | 2 +- .../v2/V2.5.2__add_token_airdrop.sql | 12 +- .../EntityRecordItemListenerTokenTest.java | 24 +-- .../entity/sql/SqlEntityListenerTest.java | 20 +-- .../TokenAirdropTransactionHandlerTest.java | 8 +- ...enCancelAirdropTransactionHandlerTest.java | 4 +- ...kenClaimAirdropTransactionHandlerTest.java | 4 +- .../controller/TokenAirdropsController.java | 16 +- .../restjava/dto/TokenAirdropRequest.java | 6 +- .../restjava/mapper/TokenAirdropsMapper.java | 2 + .../restjava/repository/CustomRepository.java | 39 ----- .../restjava/repository/JooqRepository.java | 132 +++++++++++++++ .../NftAllowanceRepositoryCustom.java | 2 +- .../NftAllowanceRepositoryCustomImpl.java | 86 ++-------- .../TokenAirdropRepositoryCustom.java | 2 +- .../TokenAirdropRepositoryCustomImpl.java | 53 +++++- .../service/TokenAirdropServiceImpl.java | 8 +- .../common/NumberRangeParameterTest.java | 2 +- .../TokenAirdropsControllerTest.java | 152 +++++++++++++----- .../mapper/TokenAirdropsMapperTest.java | 4 +- .../TokenAirdropRepositoryTest.java | 149 +++++++++++------ .../service/TokenAirdropServiceTest.java | 23 +-- hedera-mirror-rest/api/v1/openapi.yml | 4 +- 29 files changed, 490 insertions(+), 302 deletions(-) delete mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java create mode 100644 hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java diff --git a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/AbstractTokenAirdrop.java b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/AbstractTokenAirdrop.java index 66dc7ea7bf6..6084281197c 100644 --- a/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/AbstractTokenAirdrop.java +++ b/hedera-mirror-common/src/main/java/com/hedera/mirror/common/domain/token/AbstractTokenAirdrop.java @@ -43,10 +43,10 @@ public class AbstractTokenAirdrop implements History { private Long amount; @jakarta.persistence.Id - private long receiverId; + private long receiverAccountId; @jakarta.persistence.Id - private long senderId; + private long senderAccountId; @jakarta.persistence.Id private long serialNumber; @@ -63,8 +63,8 @@ public class AbstractTokenAirdrop implements History { @JsonIgnore public Id getId() { Id id = new Id(); - id.setReceiverId(receiverId); - id.setSenderId(senderId); + id.setReceiverAccountId(receiverAccountId); + id.setSenderAccountId(senderAccountId); id.setSerialNumber(serialNumber); id.setTokenId(tokenId); return id; @@ -75,8 +75,8 @@ public static class Id implements Serializable { @Serial private static final long serialVersionUID = -8165098238647325621L; - private long receiverId; - private long senderId; + private long receiverAccountId; + private long senderAccountId; private long serialNumber; private long tokenId; } 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 02440687e43..24bd8b2a6d4 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 @@ -932,8 +932,8 @@ public DomainWrapper sidecarFile() public DomainWrapper> tokenAirdrop(TokenTypeEnum type) { long timestamp = timestamp(); var builder = TokenAirdrop.builder() - .receiverId(id()) - .senderId(id()) + .receiverAccountId(id()) + .senderAccountId(id()) .state(TokenAirdropStateEnum.PENDING) .timestampRange(Range.atLeast(timestamp)) .tokenId(id()); @@ -951,8 +951,8 @@ public DomainWrapper sidecarFile() TokenTypeEnum type) { long timestamp = timestamp(); var builder = TokenAirdropHistory.builder() - .receiverId(id()) - .senderId(id()) + .receiverAccountId(id()) + .senderAccountId(id()) .state(TokenAirdropStateEnum.PENDING) .timestampRange(Range.closedOpen(timestamp, timestamp + 10)) .tokenId(id()); diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTokenUpdateAirdropTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTokenUpdateAirdropTransactionHandler.java index 48d36158ea0..a15e56254ca 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTokenUpdateAirdropTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/AbstractTokenUpdateAirdropTransactionHandler.java @@ -65,8 +65,8 @@ public void doUpdateTransaction(Transaction transaction, RecordItem recordItem) var tokenAirdrop = new TokenAirdrop(); tokenAirdrop.setState(state); - tokenAirdrop.setReceiverId(receiver.getId()); - tokenAirdrop.setSenderId(sender.getId()); + tokenAirdrop.setReceiverAccountId(receiver.getId()); + tokenAirdrop.setSenderAccountId(sender.getId()); tokenAirdrop.setTimestampRange(Range.atLeast(recordItem.getConsensusTimestamp())); TokenID tokenId; diff --git a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandler.java b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandler.java index a491cb33beb..2949c1f0b35 100644 --- a/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandler.java +++ b/hedera-mirror-importer/src/main/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandler.java @@ -52,8 +52,8 @@ protected void doUpdateTransaction(Transaction transaction, RecordItem recordIte var tokenAirdrop = new TokenAirdrop(); tokenAirdrop.setState(TokenAirdropStateEnum.PENDING); - tokenAirdrop.setReceiverId(receiver.getId()); - tokenAirdrop.setSenderId(sender.getId()); + tokenAirdrop.setReceiverAccountId(receiver.getId()); + tokenAirdrop.setSenderAccountId(sender.getId()); tokenAirdrop.setTimestampRange(Range.atLeast(recordItem.getConsensusTimestamp())); TokenID tokenId; diff --git a/hedera-mirror-importer/src/main/resources/db/migration/common/R__01_temp_tables.sql b/hedera-mirror-importer/src/main/resources/db/migration/common/R__01_temp_tables.sql index 78912c64af6..380a84d1d38 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/common/R__01_temp_tables.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/common/R__01_temp_tables.sql @@ -30,7 +30,7 @@ call create_temp_table_safe('nft', 'token_id', 'serial_number'); call create_temp_table_safe('node', 'node_id'); call create_temp_table_safe('schedule', 'schedule_id'); call create_temp_table_safe('token_account', 'account_id', 'token_id'); -call create_temp_table_safe('token_airdrop', 'receiver_id', 'sender_id', 'serial_number', 'token_id'); +call create_temp_table_safe('token_airdrop', 'receiver_account_id', 'sender_account_id', 'serial_number', 'token_id'); call create_temp_table_safe('token_allowance', 'owner', 'spender', 'token_id'); call create_temp_table_safe('token', 'token_id'); call create_temp_table_safe('topic_message_lookup', 'topic_id', 'partition'); diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.100.0__add_token_airdrop.sql b/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.100.0__add_token_airdrop.sql index f0943252038..2b0fefe0cd7 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.100.0__add_token_airdrop.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/v1/V1.100.0__add_token_airdrop.sql @@ -3,16 +3,16 @@ create type airdrop_state as enum ('CANCELLED', 'CLAIMED', 'PENDING'); create table if not exists token_airdrop ( amount bigint, - receiver_id bigint not null, - sender_id bigint not null, + receiver_account_id bigint not null, + sender_account_id bigint not null, serial_number bigint not null, state airdrop_state not null default 'PENDING', timestamp_range int8range not null, token_id bigint not null ); -create unique index if not exists token_airdrop__sender_id on token_airdrop (sender_id, receiver_id, token_id, serial_number); -create index if not exists token_airdrop__receiver_id on token_airdrop (receiver_id, sender_id, token_id, serial_number); +create unique index if not exists token_airdrop__sender_id on token_airdrop (sender_account_id, receiver_account_id, token_id, serial_number); +create index if not exists token_airdrop__receiver_id on token_airdrop (receiver_account_id, sender_account_id, token_id, serial_number); create table if not exists token_airdrop_history ( diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v2/R__02_temp_table_distribution.sql b/hedera-mirror-importer/src/main/resources/db/migration/v2/R__02_temp_table_distribution.sql index a554fe850f6..4bbf3eb01e4 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/v2/R__02_temp_table_distribution.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/v2/R__02_temp_table_distribution.sql @@ -22,7 +22,7 @@ call create_distributed_table_safe('nft_allowance_temp', 'owner', 'nft_allowance call create_distributed_table_safe('nft_temp', 'token_id', 'nft'); call create_distributed_table_safe('schedule_temp', 'schedule_id', 'schedule'); call create_distributed_table_safe('token_account_temp', 'account_id', 'token_account'); -call create_distributed_table_safe('token_airdrop_temp', 'receiver_id', 'token_airdrop'); +call create_distributed_table_safe('token_airdrop_temp', 'receiver_account_id', 'token_airdrop'); call create_distributed_table_safe('token_allowance_temp', 'owner', 'token_allowance'); call create_distributed_table_safe('dissociate_token_transfer', 'token_id', 'nft'); call create_distributed_table_safe('token_temp', 'token_id', 'token'); diff --git a/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.5.2__add_token_airdrop.sql b/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.5.2__add_token_airdrop.sql index 0e2762299b7..54216bb9dd1 100644 --- a/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.5.2__add_token_airdrop.sql +++ b/hedera-mirror-importer/src/main/resources/db/migration/v2/V2.5.2__add_token_airdrop.sql @@ -3,8 +3,8 @@ create type airdrop_state as enum ('CANCELLED', 'CLAIMED', 'PENDING'); create table if not exists token_airdrop ( amount bigint, - receiver_id bigint not null, - sender_id bigint not null, + receiver_account_id bigint not null, + sender_account_id bigint not null, serial_number bigint not null, state airdrop_state not null default 'PENDING', timestamp_range int8range not null, @@ -18,9 +18,9 @@ create table if not exists token_airdrop_history ); comment on table token_airdrop_history is 'History of token airdrops'; -select create_distributed_table('token_airdrop', 'receiver_id', colocate_with => 'entity'); -select create_distributed_table('token_airdrop_history', 'receiver_id', colocate_with => 'token_airdrop'); +select create_distributed_table('token_airdrop', 'receiver_account_id', colocate_with => 'entity'); +select create_distributed_table('token_airdrop_history', 'receiver_account_id', colocate_with => 'token_airdrop'); -create unique index if not exists token_airdrop__sender_id on token_airdrop (sender_id, receiver_id, token_id, serial_number); -create index if not exists token_airdrop__receiver_id on token_airdrop (receiver_id, sender_id, token_id, serial_number); +create unique index if not exists token_airdrop__sender_id on token_airdrop (sender_account_id, receiver_account_id, token_id, serial_number); +create index if not exists token_airdrop__receiver_id on token_airdrop (receiver_account_id, sender_account_id, token_id, serial_number); create index if not exists token_airdrop_history__timestamp_range on token_airdrop_history using gist (timestamp_range); 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 6b346867ab9..4651322902f 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 @@ -3609,16 +3609,16 @@ void tokenAirdrop(TokenAirdropStateEnum airdropType) { var expectedPendingFungible = domainBuilder .tokenAirdrop(TokenTypeEnum.FUNGIBLE_COMMON) .customize(t -> t.amount(pendingAmount) - .receiverId(RECEIVER.getAccountNum()) - .senderId(PAYER.getAccountNum()) + .receiverAccountId(RECEIVER.getAccountNum()) + .senderAccountId(PAYER.getAccountNum()) .state(TokenAirdropStateEnum.PENDING) .timestampRange(Range.atLeast(airdropTimestamp)) .tokenId(TOKEN_ID.getTokenNum())) .get(); var expectedPendingNft = domainBuilder .tokenAirdrop(TokenTypeEnum.NON_FUNGIBLE_UNIQUE) - .customize(t -> t.receiverId(RECEIVER.getAccountNum()) - .senderId(PAYER.getAccountNum()) + .customize(t -> t.receiverAccountId(RECEIVER.getAccountNum()) + .senderAccountId(PAYER.getAccountNum()) .serialNumber(SERIAL_NUMBER_1) .state(TokenAirdropStateEnum.PENDING) .timestampRange(Range.atLeast(airdropTimestamp)) @@ -3738,15 +3738,15 @@ void tokenAirdropUpdateState(TokenAirdropStateEnum airdropType) { var expectedPendingFungible = domainBuilder .tokenAirdrop(TokenTypeEnum.FUNGIBLE_COMMON) .customize(t -> t.amount(pendingAmount) - .receiverId(RECEIVER.getAccountNum()) - .senderId(PAYER.getAccountNum()) + .receiverAccountId(RECEIVER.getAccountNum()) + .senderAccountId(PAYER.getAccountNum()) .timestampRange(Range.atLeast(airdropTimestamp)) .tokenId(TOKEN_ID.getTokenNum())) .get(); var expectedPendingNft = domainBuilder .tokenAirdrop(TokenTypeEnum.NON_FUNGIBLE_UNIQUE) - .customize(t -> t.receiverId(RECEIVER.getAccountNum()) - .senderId(PAYER.getAccountNum()) + .customize(t -> t.receiverAccountId(RECEIVER.getAccountNum()) + .senderAccountId(PAYER.getAccountNum()) .serialNumber(SERIAL_NUMBER_1) .timestampRange(Range.atLeast(airdropTimestamp)) .tokenId(nftTokenId.getTokenNum())) @@ -3843,15 +3843,15 @@ void tokenAirdropPartialData(TokenAirdropStateEnum airdropType) { .tokenAirdrop(TokenTypeEnum.FUNGIBLE_COMMON) // Amount will be null when there is no pending airdrop .customize(t -> t.amount(null) - .receiverId(RECEIVER.getAccountNum()) - .senderId(PAYER.getAccountNum()) + .receiverAccountId(RECEIVER.getAccountNum()) + .senderAccountId(PAYER.getAccountNum()) .timestampRange(Range.atLeast(updateTimestamp)) .tokenId(TOKEN_ID.getTokenNum())) .get(); var expectedPendingNft = domainBuilder .tokenAirdrop(TokenTypeEnum.NON_FUNGIBLE_UNIQUE) - .customize(t -> t.receiverId(RECEIVER.getAccountNum()) - .senderId(PAYER.getAccountNum()) + .customize(t -> t.receiverAccountId(RECEIVER.getAccountNum()) + .senderAccountId(PAYER.getAccountNum()) .serialNumber(SERIAL_NUMBER_1) .timestampRange(Range.atLeast(updateTimestamp)) .tokenId(nftTokenId.getTokenNum())) 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 59413f3a264..4425f2c9d6c 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 @@ -2722,8 +2722,8 @@ void onTokenAirdrop() { var updatedAmountAirdrop = domainBuilder .tokenAirdrop(TokenTypeEnum.FUNGIBLE_COMMON) .customize(a -> a.amount(newAmount) - .receiverId(tokenAirdrop.getReceiverId()) - .senderId(tokenAirdrop.getSenderId()) + .receiverAccountId(tokenAirdrop.getReceiverAccountId()) + .senderAccountId(tokenAirdrop.getSenderAccountId()) .tokenId(tokenAirdrop.getTokenId()) .timestampRange(Range.atLeast(domainBuilder.timestamp()))) .get(); @@ -2768,16 +2768,16 @@ void onTokenAirdropUpdate(TokenAirdropStateEnum state, int commitIndex) { .customize(a -> a.state(state) .amount(null) // Record files that change state do not include PendingAirdropValue so remove the // amount here - .receiverId(tokenAirdrop.getReceiverId()) - .senderId(tokenAirdrop.getSenderId()) + .receiverAccountId(tokenAirdrop.getReceiverAccountId()) + .senderAccountId(tokenAirdrop.getSenderAccountId()) .tokenId(tokenAirdrop.getTokenId()) .timestampRange(Range.atLeast(domainBuilder.timestamp()))) .get(); var nftAirdropUpdateState = domainBuilder .tokenAirdrop(TokenTypeEnum.NON_FUNGIBLE_UNIQUE) .customize(a -> a.state(state) - .receiverId(nftAirdrop.getReceiverId()) - .senderId(nftAirdrop.getSenderId()) + .receiverAccountId(nftAirdrop.getReceiverAccountId()) + .senderAccountId(nftAirdrop.getSenderAccountId()) .serialNumber(nftAirdrop.getSerialNumber()) .tokenId(nftAirdrop.getTokenId()) .timestampRange(Range.atLeast(domainBuilder.timestamp()))) @@ -2814,16 +2814,16 @@ void onTokenAirdropPartialUpdate(TokenAirdropStateEnum state) { .customize(a -> a.state(state) .amount(null) // Record files that change state do not include PendingAirdropValue so remove the // amount here - .receiverId(tokenAirdrop.getReceiverId()) - .senderId(tokenAirdrop.getSenderId()) + .receiverAccountId(tokenAirdrop.getReceiverAccountId()) + .senderAccountId(tokenAirdrop.getSenderAccountId()) .tokenId(tokenAirdrop.getTokenId()) .timestampRange(Range.atLeast(domainBuilder.timestamp()))) .get(); var nftAirdropUpdateState = domainBuilder .tokenAirdrop(TokenTypeEnum.NON_FUNGIBLE_UNIQUE) .customize(a -> a.state(state) - .receiverId(nftAirdrop.getReceiverId()) - .senderId(nftAirdrop.getSenderId()) + .receiverAccountId(nftAirdrop.getReceiverAccountId()) + .senderAccountId(nftAirdrop.getSenderAccountId()) .serialNumber(nftAirdrop.getSerialNumber()) .tokenId(nftAirdrop.getTokenId()) .timestampRange(Range.atLeast(domainBuilder.timestamp()))) diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandlerTest.java index cb7d701eb51..cf93dcff1f3 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenAirdropTransactionHandlerTest.java @@ -87,8 +87,8 @@ void updateTransactionSuccessfulFungiblePendingAirdrop() { verify(entityListener).onTokenAirdrop(tokenAirdrop.capture()); assertThat(tokenAirdrop.getValue()) .returns(amount, TokenAirdrop::getAmount) - .returns(receiver.getAccountNum(), TokenAirdrop::getReceiverId) - .returns(sender.getAccountNum(), TokenAirdrop::getSenderId) + .returns(receiver.getAccountNum(), TokenAirdrop::getReceiverAccountId) + .returns(sender.getAccountNum(), TokenAirdrop::getSenderAccountId) .returns(0L, TokenAirdrop::getSerialNumber) .returns(PENDING, TokenAirdrop::getState) .returns(Range.atLeast(timestamp), TokenAirdrop::getTimestampRange) @@ -130,8 +130,8 @@ void updateTransactionSuccessfulNftPendingAirdrop() { verify(entityListener).onTokenAirdrop(tokenAirdrop.capture()); assertThat(tokenAirdrop.getValue()) .returns(null, TokenAirdrop::getAmount) - .returns(receiver.getAccountNum(), TokenAirdrop::getReceiverId) - .returns(sender.getAccountNum(), TokenAirdrop::getSenderId) + .returns(receiver.getAccountNum(), TokenAirdrop::getReceiverAccountId) + .returns(sender.getAccountNum(), TokenAirdrop::getSenderAccountId) .returns(PENDING, TokenAirdrop::getState) .returns(Range.atLeast(timestamp), TokenAirdrop::getTimestampRange) .returns(token.getTokenNum(), TokenAirdrop::getTokenId); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenCancelAirdropTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenCancelAirdropTransactionHandlerTest.java index 4d74cfcaf4b..3508d7e8733 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenCancelAirdropTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenCancelAirdropTransactionHandlerTest.java @@ -101,8 +101,8 @@ void cancelAirdrop(TokenTypeEnum tokenType) { verify(entityListener).onTokenAirdrop(tokenAirdrop.capture()); assertThat(tokenAirdrop.getValue()) - .returns(receiver.getNum(), TokenAirdrop::getReceiverId) - .returns(sender.getNum(), TokenAirdrop::getSenderId) + .returns(receiver.getNum(), TokenAirdrop::getReceiverAccountId) + .returns(sender.getNum(), TokenAirdrop::getSenderAccountId) .returns(TokenAirdropStateEnum.CANCELLED, TokenAirdrop::getState) .returns(Range.atLeast(timestamp), TokenAirdrop::getTimestampRange) .returns(token.getTokenNum(), TokenAirdrop::getTokenId); diff --git a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenClaimAirdropTransactionHandlerTest.java b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenClaimAirdropTransactionHandlerTest.java index fdf1b823a5b..e0ada6968ee 100644 --- a/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenClaimAirdropTransactionHandlerTest.java +++ b/hedera-mirror-importer/src/test/java/com/hedera/mirror/importer/parser/record/transactionhandler/TokenClaimAirdropTransactionHandlerTest.java @@ -101,8 +101,8 @@ void claimAirdrop(TokenTypeEnum tokenType) { verify(entityListener).onTokenAirdrop(tokenAirdrop.capture()); assertThat(tokenAirdrop.getValue()) - .returns(receiver.getNum(), TokenAirdrop::getReceiverId) - .returns(sender.getNum(), TokenAirdrop::getSenderId) + .returns(receiver.getNum(), TokenAirdrop::getReceiverAccountId) + .returns(sender.getNum(), TokenAirdrop::getSenderAccountId) .returns(TokenAirdropStateEnum.CLAIMED, TokenAirdrop::getState) .returns(Range.atLeast(timestamp), TokenAirdrop::getTimestampRange) .returns(token.getTokenNum(), TokenAirdrop::getTokenId); diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java index d288c7e3d1e..7b524629cd0 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java @@ -16,6 +16,7 @@ package com.hedera.mirror.restjava.controller; +import static com.hedera.mirror.restjava.common.Constants.ACCOUNT_ID; import static com.hedera.mirror.restjava.common.Constants.DEFAULT_LIMIT; import static com.hedera.mirror.restjava.common.Constants.MAX_LIMIT; import static com.hedera.mirror.restjava.common.Constants.RECEIVER_ID; @@ -29,9 +30,12 @@ import com.hedera.mirror.restjava.common.LinkFactory; import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import com.hedera.mirror.restjava.mapper.TokenAirdropsMapper; +import com.hedera.mirror.restjava.service.Bound; import com.hedera.mirror.restjava.service.TokenAirdropService; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import java.util.List; import java.util.Map; import java.util.function.Function; import lombok.CustomLog; @@ -54,7 +58,7 @@ public class TokenAirdropsController { TOKEN_ID, tokenAirdrop.getTokenId()); private final LinkFactory linkFactory; - private final TokenAirdropsMapper tokenMapper; + private final TokenAirdropsMapper tokenAirdropsMapper; private final TokenAirdropService service; @GetMapping(value = "/outstanding") @@ -62,17 +66,17 @@ TokenAirdropsResponse getOutstandingAirdrops( @PathVariable EntityIdParameter id, @RequestParam(defaultValue = DEFAULT_LIMIT) @Positive @Max(MAX_LIMIT) int limit, @RequestParam(defaultValue = "asc") Sort.Direction order, - @RequestParam(name = RECEIVER_ID, required = false) EntityIdRangeParameter receiverId, - @RequestParam(name = TOKEN_ID, required = false) EntityIdRangeParameter tokenId) { + @RequestParam(name = RECEIVER_ID, required = false) @Size(max = 2) List receiverIds, + @RequestParam(name = TOKEN_ID, required = false) @Size(max = 2) List tokenIds) { var request = TokenAirdropRequest.builder() .accountId(id) - .entityId(receiverId) + .entityIds(new Bound(receiverIds, true, ACCOUNT_ID)) .limit(limit) .order(order) - .tokenId(tokenId) + .tokenIds(new Bound(tokenIds, false, TOKEN_ID)) .build(); var response = service.getOutstandingAirdrops(request); - var airdrops = tokenMapper.map(response); + var airdrops = tokenAirdropsMapper.map(response); var sort = Sort.by(order, RECEIVER_ID, TOKEN_ID); var pageable = PageRequest.of(0, limit, sort); var links = linkFactory.create(airdrops, pageable, EXTRACTOR); diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java index b17042730f9..494d83d744e 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/dto/TokenAirdropRequest.java @@ -17,7 +17,7 @@ package com.hedera.mirror.restjava.dto; import com.hedera.mirror.restjava.common.EntityIdParameter; -import com.hedera.mirror.restjava.common.EntityIdRangeParameter; +import com.hedera.mirror.restjava.service.Bound; import lombok.Builder; import lombok.Data; import org.springframework.data.domain.Sort; @@ -36,7 +36,7 @@ public class TokenAirdropRequest { private Sort.Direction order = Sort.Direction.ASC; // Receiver Id for Outstanding Airdrops, Sender Id for Pending Airdrops - private EntityIdRangeParameter entityId; + private Bound entityIds; - private EntityIdRangeParameter tokenId; + private Bound tokenIds; } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java index 9c1fd9d648a..69a52ccc8d9 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java @@ -24,6 +24,8 @@ @Mapper(config = MapperConfiguration.class) public interface TokenAirdropsMapper extends CollectionMapper { + @Mapping(source = "receiverAccountId", target = "receiverId") + @Mapping(source = "senderAccountId", target = "senderId") @Mapping(source = "serialNumber", target = "serialNumber", qualifiedByName = "mapToNullIfZero") @Mapping(source = "timestampRange", target = "timestamp") com.hedera.mirror.rest.model.TokenAirdrop map(TokenAirdrop source); diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java deleted file mode 100644 index 6cae1621279..00000000000 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/CustomRepository.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.mirror.restjava.repository; - -import static org.jooq.impl.DSL.noCondition; - -import com.hedera.mirror.restjava.common.EntityIdRangeParameter; -import com.hedera.mirror.restjava.common.RangeOperator; -import org.jooq.Condition; -import org.jooq.Field; - -interface CustomRepository { - - default Condition getCondition(Field field, EntityIdRangeParameter param) { - if (param == null) { - return noCondition(); - } - - return getCondition(field, param.operator(), param.value().getId()); - } - - default Condition getCondition(Field field, RangeOperator operator, Long value) { - return operator.getFunction().apply(field, value); - } -} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java new file mode 100644 index 00000000000..e0b270894e9 --- /dev/null +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.mirror.restjava.repository; + +import static com.hedera.mirror.restjava.common.RangeOperator.EQ; +import static com.hedera.mirror.restjava.common.RangeOperator.GT; +import static com.hedera.mirror.restjava.common.RangeOperator.LT; +import static org.jooq.impl.DSL.noCondition; + +import com.hedera.mirror.common.domain.entity.EntityId; +import com.hedera.mirror.restjava.common.EntityIdRangeParameter; +import com.hedera.mirror.restjava.common.RangeOperator; +import com.hedera.mirror.restjava.service.Bound; +import org.jooq.Condition; +import org.jooq.Field; + +interface JooqRepository { + + default Condition getCondition(Field field, EntityIdRangeParameter param) { + if (param == null || param == EntityIdRangeParameter.EMPTY) { + return noCondition(); + } + + return getCondition(field, param.operator(), param.value().getId()); + } + + default Condition getCondition(Field field, RangeOperator operator, Long value) { + return operator.getFunction().apply(field, value); + } + + default Condition getBoundCondition(FieldBound fieldBound) { + var primaryBound = fieldBound.primaryBound(); + var primaryLower = EntityIdRangeParameter.EMPTY; + var primaryUpper = EntityIdRangeParameter.EMPTY; + + if (primaryBound != null) { + primaryLower = primaryBound.getLower(); + primaryUpper = primaryBound.getUpper(); + + // If the primary param has a range with a single value, rewrite it to EQ + if (primaryBound.hasEqualBounds()) { + primaryLower = new EntityIdRangeParameter(EQ, EntityId.of(primaryBound.adjustLowerBound())); + primaryUpper = null; + } + } + + var secondaryBound = fieldBound.secondaryBound(); + var secondaryLower = EntityIdRangeParameter.EMPTY; + var secondaryUpper = EntityIdRangeParameter.EMPTY; + if (secondaryBound != null) { + secondaryLower = secondaryBound.getLower(); + secondaryUpper = secondaryBound.getUpper(); + // If the secondary param operator is EQ, set the secondary upper bound to the same + if (secondaryLower != null && secondaryLower.operator() == EQ) { + secondaryUpper = secondaryLower; + } + } + + var primaryField = fieldBound.primaryField(); + var secondaryField = fieldBound.secondaryField(); + var lowerCondition = getOuterBoundCondition(primaryLower, secondaryLower, primaryField, secondaryField); + var middleCondition = getMiddleCondition(primaryLower, secondaryLower, primaryField, secondaryField) + .and(getMiddleCondition(primaryUpper, secondaryUpper, primaryField, secondaryField)); + var upperCondition = getOuterBoundCondition(primaryUpper, secondaryUpper, primaryField, secondaryField); + + return lowerCondition.or(middleCondition).or(upperCondition); + } + + private Condition getOuterBoundCondition( + EntityIdRangeParameter primaryParam, + EntityIdRangeParameter secondaryParam, + Field primaryField, + Field secondaryField) { + // No outer bound condition if there is no primary parameter, or the operator is EQ. For EQ, everything should + // go into the middle condition + if (primaryParam == null + || primaryParam.equals(EntityIdRangeParameter.EMPTY) + || primaryParam.operator() == EQ) { + return noCondition(); + } + + // If the secondary param operator is EQ, there should only have the middle condition + if (secondaryParam != null && secondaryParam.operator() == EQ) { + return noCondition(); + } + + long value = primaryParam.value().getId(); + if (primaryParam.operator() == GT) { + value += 1L; + } else if (primaryParam.operator() == LT) { + value -= 1L; + } + + return getCondition(primaryField, EQ, value).and(getCondition(secondaryField, secondaryParam)); + } + + private Condition getMiddleCondition( + EntityIdRangeParameter primaryParam, + EntityIdRangeParameter secondaryParam, + Field primaryField, + Field secondaryField) { + if (primaryParam == null || primaryParam.equals(EntityIdRangeParameter.EMPTY)) { + return noCondition(); + } + + // When the primary param operator is EQ, or the secondary param operator is EQ, don't adjust the value for the + // primary param. + if (primaryParam.operator() == EQ || (secondaryParam != null && secondaryParam.operator() == EQ)) { + return getCondition(primaryField, primaryParam).and(getCondition(secondaryField, secondaryParam)); + } + + long value = primaryParam.value().getId(); + value += primaryParam.hasLowerBound() ? 1L : -1L; + return getCondition(primaryField, primaryParam.operator(), value); + } + + record FieldBound(Field primaryField, Field secondaryField, Bound primaryBound, Bound secondaryBound) {} +} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustom.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustom.java index 7eeba3390e2..e2897ddef36 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustom.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustom.java @@ -22,7 +22,7 @@ import jakarta.validation.constraints.NotNull; import java.util.Collection; -public interface NftAllowanceRepositoryCustom extends CustomRepository { +public interface NftAllowanceRepositoryCustom extends JooqRepository { /** * Find all NftAllowance matching the request parameters with the given limit, sort order, and byOwner flag diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java index b82ee8d0858..8b58d0f1a76 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java @@ -17,16 +17,11 @@ package com.hedera.mirror.restjava.repository; import static com.hedera.mirror.restjava.common.RangeOperator.EQ; -import static com.hedera.mirror.restjava.common.RangeOperator.GT; -import static com.hedera.mirror.restjava.common.RangeOperator.LT; import static com.hedera.mirror.restjava.jooq.domain.Tables.NFT_ALLOWANCE; -import static org.jooq.impl.DSL.noCondition; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.entity.NftAllowance; -import com.hedera.mirror.restjava.common.EntityIdRangeParameter; import com.hedera.mirror.restjava.dto.NftAllowanceRequest; -import com.hedera.mirror.restjava.service.Bound; import jakarta.inject.Named; import jakarta.validation.constraints.NotNull; import java.util.Collection; @@ -35,7 +30,6 @@ import lombok.RequiredArgsConstructor; import org.jooq.Condition; import org.jooq.DSLContext; -import org.jooq.Field; import org.jooq.SortField; import org.springframework.data.domain.Sort.Direction; @@ -56,8 +50,8 @@ class NftAllowanceRepositoryCustomImpl implements NftAllowanceRepositoryCustom { @Override public Collection findAll(NftAllowanceRequest request, EntityId accountId) { boolean byOwner = request.isOwner(); - var condition = getBaseCondition(accountId, byOwner) - .and(getBoundCondition(byOwner, request.getOwnerOrSpenderIds(), request.getTokenIds())); + var fieldBound = getFieldBound(request, byOwner); + var condition = getBaseCondition(accountId, byOwner).and(getBoundCondition(fieldBound)); return dslContext .selectFrom(NFT_ALLOWANCE) .where(condition) @@ -71,70 +65,18 @@ private Condition getBaseCondition(EntityId accountId, boolean byOwner) { .and(APPROVAL_CONDITION); } - private Condition getBoundCondition(boolean byOwner, Bound primaryBound, Bound tokenBound) { - var primaryField = byOwner ? NFT_ALLOWANCE.SPENDER : NFT_ALLOWANCE.OWNER; - var primaryLower = primaryBound.getLower(); - var primaryUpper = primaryBound.getUpper(); - var tokenLower = tokenBound.getLower(); - var tokenUpper = tokenBound.getUpper(); - - // If the primary param has a range with a single value, rewrite it to EQ - if (primaryBound.hasEqualBounds()) { - primaryLower = new EntityIdRangeParameter(EQ, EntityId.of(primaryBound.adjustLowerBound())); - primaryUpper = null; - } - - // If the token param operator is EQ, set the token upper bound to the same - if (tokenLower != null && tokenLower.operator() == EQ) { - tokenUpper = tokenLower; - } - - var lowerCondition = getOuterBoundCondition(primaryLower, tokenLower, primaryField); - var middleCondition = getMiddleCondition(primaryLower, tokenLower, primaryField) - .and(getMiddleCondition(primaryUpper, tokenUpper, primaryField)); - var upperCondition = getOuterBoundCondition(primaryUpper, tokenUpper, primaryField); - - return lowerCondition.or(middleCondition).or(upperCondition); - } - - private Condition getOuterBoundCondition( - EntityIdRangeParameter primaryParam, EntityIdRangeParameter tokenParam, Field primaryField) { - // No outer bound condition if there is no primary parameter, or the operator is EQ. For EQ, everything should - // go into the middle condition - if (primaryParam == null || primaryParam.operator() == EQ) { - return noCondition(); - } - - // If the token param operator is EQ, there should only have the middle condition - if (tokenParam != null && tokenParam.operator() == EQ) { - return noCondition(); - } - - long value = primaryParam.value().getId(); - if (primaryParam.operator() == GT) { - value += 1L; - } else if (primaryParam.operator() == LT) { - value -= 1L; - } - - return getCondition(primaryField, EQ, value).and(getCondition(NFT_ALLOWANCE.TOKEN_ID, tokenParam)); - } - - private Condition getMiddleCondition( - EntityIdRangeParameter primaryParam, EntityIdRangeParameter tokenParam, Field primaryField) { - if (primaryParam == null) { - return noCondition(); - } - - // When the primary param operator is EQ, or the token param operator is EQ, don't adjust the value for the - // primary param. - if (primaryParam.operator() == EQ || (tokenParam != null && tokenParam.operator() == EQ)) { - return getCondition(primaryField, primaryParam).and(getCondition(NFT_ALLOWANCE.TOKEN_ID, tokenParam)); - } - - long value = primaryParam.value().getId(); - value += primaryParam.hasLowerBound() ? 1L : -1L; - return getCondition(primaryField, primaryParam.operator(), value); + private FieldBound getFieldBound(NftAllowanceRequest request, boolean byOwner) { + return byOwner + ? new FieldBound( + NFT_ALLOWANCE.SPENDER, + NFT_ALLOWANCE.TOKEN_ID, + request.getOwnerOrSpenderIds(), + request.getTokenIds()) + : new FieldBound( + NFT_ALLOWANCE.OWNER, + NFT_ALLOWANCE.TOKEN_ID, + request.getOwnerOrSpenderIds(), + request.getTokenIds()); } private record OrderSpec(boolean byOwner, Direction direction) {} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java index f1919f96e7f..50b7a1868c4 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustom.java @@ -22,7 +22,7 @@ import jakarta.validation.constraints.NotNull; import java.util.Collection; -public interface TokenAirdropRepositoryCustom extends CustomRepository { +public interface TokenAirdropRepositoryCustom extends JooqRepository { @NotNull Collection findAllOutstanding(TokenAirdropRequest request, EntityId accountId); diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java index c8f8298a602..42470198321 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java @@ -16,6 +16,7 @@ package com.hedera.mirror.restjava.repository; +import static com.hedera.mirror.restjava.common.RangeOperator.EQ; import static com.hedera.mirror.restjava.jooq.domain.Tables.TOKEN_AIRDROP; import com.hedera.mirror.common.domain.entity.EntityId; @@ -27,6 +28,7 @@ import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; +import org.jooq.Condition; import org.jooq.DSLContext; import org.jooq.SortField; import org.springframework.data.domain.Sort.Direction; @@ -37,17 +39,16 @@ class TokenAirdropRepositoryCustomImpl implements TokenAirdropRepositoryCustom { private final DSLContext dslContext; private static final Map>> OUTSTANDING_SORT_ORDERS = Map.of( - Direction.ASC, List.of(TOKEN_AIRDROP.RECEIVER_ID.asc(), TOKEN_AIRDROP.TOKEN_ID.asc()), - Direction.DESC, List.of(TOKEN_AIRDROP.RECEIVER_ID.desc(), TOKEN_AIRDROP.TOKEN_ID.desc())); + Direction.ASC, List.of(TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID.asc(), TOKEN_AIRDROP.TOKEN_ID.asc()), + Direction.DESC, List.of(TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID.desc(), TOKEN_AIRDROP.TOKEN_ID.desc())); @Override public Collection findAllOutstanding(TokenAirdropRequest request, EntityId accountId) { - var condition = TOKEN_AIRDROP - .SENDER_ID - .eq(accountId.getId()) - .and(TOKEN_AIRDROP.STATE.eq(AirdropState.PENDING)) - .and(getCondition(TOKEN_AIRDROP.RECEIVER_ID, request.getEntityId())) - .and(getCondition(TOKEN_AIRDROP.TOKEN_ID, request.getTokenId())); + var fieldBound = getFieldBound(request, true); + var condition = getBaseCondition(accountId, true) + .and(getBoundCondition(fieldBound)) + // Exclude NFTs + .and(TOKEN_AIRDROP.SERIAL_NUMBER.eq(0L)); var order = OUTSTANDING_SORT_ORDERS.get(request.getOrder()); return dslContext @@ -57,4 +58,40 @@ public Collection findAllOutstanding(TokenAirdropRequest request, .limit(request.getLimit()) .fetchInto(TokenAirdrop.class); } + + private FieldBound getFieldBound(TokenAirdropRequest request, boolean outstanding) { + if (outstanding) { + return (request.getEntityIds() == null || request.getEntityIds().isEmpty()) + ? new FieldBound( + TOKEN_AIRDROP.TOKEN_ID, + TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID, + request.getTokenIds(), + request.getEntityIds()) + : new FieldBound( + TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID, + TOKEN_AIRDROP.TOKEN_ID, + request.getEntityIds(), + request.getTokenIds()); + } else { + return (request.getEntityIds() == null || request.getEntityIds().isEmpty()) + ? new FieldBound( + TOKEN_AIRDROP.TOKEN_ID, + TOKEN_AIRDROP.SENDER_ACCOUNT_ID, + request.getTokenIds(), + request.getEntityIds()) + : new FieldBound( + TOKEN_AIRDROP.SENDER_ACCOUNT_ID, + TOKEN_AIRDROP.TOKEN_ID, + request.getEntityIds(), + request.getTokenIds()); + } + } + + private Condition getBaseCondition(EntityId accountId, boolean outstanding) { + return getCondition( + outstanding ? TOKEN_AIRDROP.SENDER_ACCOUNT_ID : TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID, + EQ, + accountId.getId()) + .and(TOKEN_AIRDROP.STATE.eq(AirdropState.PENDING)); + } } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java index 95ac76b97cd..8e56a4da857 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/TokenAirdropServiceImpl.java @@ -16,8 +16,6 @@ package com.hedera.mirror.restjava.service; -import static com.hedera.mirror.restjava.common.Constants.SENDER_ID; - import com.hedera.mirror.common.domain.token.TokenAirdrop; import com.hedera.mirror.restjava.dto.TokenAirdropRequest; import com.hedera.mirror.restjava.repository.TokenAirdropRepository; @@ -33,11 +31,7 @@ public class TokenAirdropServiceImpl implements TokenAirdropService { private final TokenAirdropRepository repository; public Collection getOutstandingAirdrops(TokenAirdropRequest request) { - var accountId = request.getAccountId(); - if (accountId == null) { - throw new IllegalArgumentException(SENDER_ID + " is required"); - } - var id = entityService.lookup(accountId); + var id = entityService.lookup(request.getAccountId()); return repository.findAllOutstanding(request, id); } } diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java index a44a378769c..3fe4ff84637 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java @@ -73,7 +73,7 @@ void testEmpty() { } @ParameterizedTest - @ValueSource(strings = {"a", ".1", "someinvalidstring", "-1", "9223372036854775808", ":2000"}) + @ValueSource(strings = {"a", ".1", "someinvalidstring", "-1", "9223372036854775808", ":2000", ":", "eq:", ":1"}) @DisplayName("IntegerRangeParameter parse from string tests, negative cases") void testInvalidParam(String input) { assertThrows(IllegalArgumentException.class, () -> NumberRangeParameter.valueOf(input)); diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java index 11cee08eafa..f95e23b893c 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java @@ -57,16 +57,16 @@ protected String getUrl() { @Override protected RequestHeadersSpec defaultRequest(RequestHeadersUriSpec uriSpec) { var tokenAirdrop = domainBuilder.tokenAirdrop(FUNGIBLE_COMMON).persist(); - return uriSpec.uri("", tokenAirdrop.getSenderId()); + return uriSpec.uri("", tokenAirdrop.getSenderAccountId()); } @ValueSource(strings = {"1000", "0.1000", "0.0.1000"}) @ParameterizedTest - void outstandingAirdropByEntityId(String id) { + void entityId(String id) { // Given var tokenAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderId(1000L)) + .customize(a -> a.senderAccountId(1000L)) .persist(); // When @@ -80,12 +80,12 @@ void outstandingAirdropByEntityId(String id) { } @Test - void evmAddressOutstanding() { + void evmAddress() { // Given var entity = domainBuilder.entity().persist(); var tokenAirdrop = domainBuilder - .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderId(entity.getId())) + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId())) .persist(); // When @@ -100,12 +100,12 @@ void evmAddressOutstanding() { } @Test - void aliasOutstanding() { + void alias() { // Given var entity = domainBuilder.entity().persist(); var tokenAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderId(entity.getId())) + .customize(a -> a.senderAccountId(entity.getId())) .persist(); // When @@ -120,24 +120,25 @@ void aliasOutstanding() { } @Test - void followAscendingOrderLinkOutstanding() { + void followAscendingOrderLink() { // Given var entity = domainBuilder.entity().persist(); var id = entity.getId(); var tokenAirdrop1 = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderId(entity.getId())) + .customize(a -> a.senderAccountId(entity.getId())) .persist(); var tokenAirdrop2 = domainBuilder - .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderId(entity.getId())) + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId())) .persist(); var baseLink = "/api/v1/accounts/%d/airdrops/outstanding".formatted(id); // When var result = restClient.get().uri("?limit=1", id).retrieve().body(TokenAirdropsResponse.class); var nextParams = "?limit=1&receiver.id=gte:%s&token.id=gt:%s" - .formatted(EntityId.of(tokenAirdrop1.getReceiverId()), EntityId.of(tokenAirdrop1.getTokenId())); + .formatted( + EntityId.of(tokenAirdrop1.getReceiverAccountId()), EntityId.of(tokenAirdrop1.getTokenId())); // Then assertThat(result).isEqualTo(getExpectedResponse(List.of(tokenAirdrop1), baseLink + nextParams)); @@ -147,51 +148,55 @@ void followAscendingOrderLinkOutstanding() { // Then nextParams = "?limit=1&receiver.id=gte:%s&token.id=gt:%s" - .formatted(EntityId.of(tokenAirdrop2.getReceiverId()), EntityId.of(tokenAirdrop2.getTokenId())); + .formatted( + EntityId.of(tokenAirdrop2.getReceiverAccountId()), EntityId.of(tokenAirdrop2.getTokenId())); assertThat(result).isEqualTo(getExpectedResponse(List.of(tokenAirdrop2), baseLink + nextParams)); // When follow link 2 result = restClient .get() - .uri(nextParams, tokenAirdrop1.getReceiverId()) + .uri(nextParams, tokenAirdrop1.getReceiverAccountId()) .retrieve() .body(TokenAirdropsResponse.class); // Then assertThat(result).isEqualTo(getExpectedResponse(List.of(), null)); } + // TODO fix this test @Test - void followDescendingOrderLinkOutstanding() { + void followDescendingOrderLink() { // Given long sender = 1000; long receiver = 2000; long fungibleTokenId = 100; long token1 = 300; long token2 = 301; - long serial1 = 100; - var tokenAirdrop1 = domainBuilder + var airdrop1 = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderId(sender).receiverId(receiver).tokenId(fungibleTokenId)) + .customize(a -> a.senderAccountId(sender) + .receiverAccountId(receiver) + .tokenId(fungibleTokenId)) .persist(); - - var nftAirdrop = domainBuilder - .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderId(sender) - .receiverId(receiver) - .tokenId(token1) - .serialNumber(serial1)) + var airdrop2 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(sender) + .receiverAccountId(receiver) + .tokenId(token1)) .persist(); - var nftAirdrop2 = domainBuilder - .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderId(sender) - .receiverId(receiver) - .tokenId(token2) - .serialNumber(serial1)) + var airdrop3 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(sender) + .receiverAccountId(receiver) + .tokenId(token2)) + .persist(); + domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.receiverAccountId(receiver)) .persist(); domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.receiverId(receiver)) + .customize(a -> a.receiverAccountId(receiver)) .persist(); var uriParams = "?limit=1&receiver.id=gte:%s&order=desc".formatted(receiver); @@ -205,21 +210,21 @@ void followDescendingOrderLinkOutstanding() { var nextParams = "?limit=1&receiver.id=gte:2000&receiver.id=lte:0.0.2000&order=desc&token.id=lt:0.0.301"; // Then - assertThat(result).isEqualTo(getExpectedResponse(List.of(nftAirdrop2), baseLink + nextParams)); + assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop3), baseLink + nextParams)); // When follow link result = restClient.get().uri(nextParams, sender).retrieve().body(TokenAirdropsResponse.class); // Then nextParams = "?limit=1&receiver.id=gte:2000&receiver.id=lte:0.0.2000&order=desc&token.id=lt:0.0.300"; - assertThat(result).isEqualTo(getExpectedResponse(List.of(nftAirdrop), baseLink + nextParams)); + assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop2), baseLink + nextParams)); // When follow link result = restClient.get().uri(nextParams, sender).retrieve().body(TokenAirdropsResponse.class); // Then nextParams = "?limit=1&receiver.id=gte:2000&receiver.id=lte:0.0.2000&order=desc&token.id=lt:0.0.100"; - assertThat(result).isEqualTo(getExpectedResponse(List.of(tokenAirdrop1), baseLink + nextParams)); + assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop1), baseLink + nextParams)); // When follow link result = restClient.get().uri(nextParams, sender).retrieve().body(TokenAirdropsResponse.class); @@ -228,6 +233,62 @@ void followDescendingOrderLinkOutstanding() { assertThat(result).isEqualTo(getExpectedResponse(List.of(), null)); } + // TODO fix this test + @Test + void paginationReceiverAndToken() { + // Given + var entity = domainBuilder.entity().persist(); + var airdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId()) + .receiverAccountId(3L) + .tokenId(5L)) + .persist(); + var airdrop2 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId()) + .receiverAccountId(3L) + .tokenId(6L)) + .persist(); + var airdrop3 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId()) + .receiverAccountId(4L) + .tokenId(5L)) + .persist(); + var airdrop4 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId()) + .receiverAccountId(5L) + .tokenId(6L)) + .persist(); + + var baseLink = "/api/v1/accounts/%d/airdrops/outstanding".formatted(entity.getId()); + var uriParams = "?limit=1&receiver.id=gt:2&token.id=gt:4"; + var nextParams = "?limit=1&receiver.id=gte:0.0.3&token.id=gt:0.0.5"; + + // When + var result = + restClient.get().uri(uriParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); + + // Then + assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop), baseLink + nextParams)); + + // When follow link + result = restClient.get().uri(nextParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); + + // Then + nextParams = "?limit=1&receiver.id=gte:0.0.3&token.id=gt:0.0.6"; + assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop2), baseLink + nextParams)); + + // When follow link + result = restClient.get().uri(nextParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); + + // Then + nextParams = "?limit=1&receiver.id=gte:0.0.3&token.id=gt:0.0.5"; + assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop3), baseLink + nextParams)); + } + @ParameterizedTest @ValueSource( strings = { @@ -357,6 +418,25 @@ void invalidTokenId(String tokenId) { HttpClientErrorException.BadRequest.class, "Failed to convert 'token.id' with value: '" + tokenId + "'"); } + + // @ParameterizedTest + // @CsvSource({ + // "?token.id=gt:0.0.1000&token.id=lt:0.0.2000,Only one occurrence of token.id", + // }) + // void invalidNumberOfParameters(String uri) { + // // When + // ThrowingCallable callable = () -> restClient + // .get() + // .uri(uri, "0.0.1001") + // .retrieve() + // .body(TokenAirdropsResponse.class); + // + // // Then + // validateError( + // callable, + // HttpClientErrorException.BadRequest.class, + // "Failed to convert 'token.id' with value: "); + // } } private TokenAirdropsResponse getExpectedResponse(List tokenAirdrops, String next) { diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java index 2596ef1182b..4e9fbda42c1 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java @@ -53,8 +53,8 @@ void map(TokenTypeEnum tokenType) { assertThat(mapper.map(List.of(tokenAirdrop))) .first() .returns(tokenType == NON_FUNGIBLE_UNIQUE ? null : tokenAirdrop.getAmount(), TokenAirdrop::getAmount) - .returns(EntityId.of(tokenAirdrop.getReceiverId()).toString(), TokenAirdrop::getReceiverId) - .returns(EntityId.of(tokenAirdrop.getSenderId()).toString(), TokenAirdrop::getSenderId) + .returns(EntityId.of(tokenAirdrop.getReceiverAccountId()).toString(), TokenAirdrop::getReceiverId) + .returns(EntityId.of(tokenAirdrop.getSenderAccountId()).toString(), TokenAirdrop::getSenderId) .returns( tokenType == FUNGIBLE_COMMON ? null : tokenAirdrop.getSerialNumber(), TokenAirdrop::getSerialNumber) diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java index 8feb0773399..f4e3ce3383f 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java @@ -20,13 +20,14 @@ import static com.hedera.mirror.common.domain.token.TokenTypeEnum.NON_FUNGIBLE_UNIQUE; import static org.assertj.core.api.Assertions.assertThat; -import com.google.common.collect.Range; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.restjava.RestJavaIntegrationTest; +import com.hedera.mirror.restjava.common.Constants; import com.hedera.mirror.restjava.common.EntityIdNumParameter; import com.hedera.mirror.restjava.common.EntityIdRangeParameter; import com.hedera.mirror.restjava.common.RangeOperator; import com.hedera.mirror.restjava.dto.TokenAirdropRequest; +import com.hedera.mirror.restjava.service.Bound; import java.util.List; import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; @@ -48,43 +49,24 @@ void findById() { @Test void findBySenderId() { var tokenAirdrop = domainBuilder.tokenAirdrop(FUNGIBLE_COMMON).persist(); - var entityId = EntityId.of(tokenAirdrop.getSenderId()); + var entityId = EntityId.of(tokenAirdrop.getSenderAccountId()); var request = TokenAirdropRequest.builder() .accountId(new EntityIdNumParameter(entityId)) .build(); assertThat(repository.findAllOutstanding(request, entityId)).contains(tokenAirdrop); } - @ParameterizedTest - @EnumSource(Direction.class) - void findBySenderIdOrder(Direction order) { - var tokenAirdrop = domainBuilder - .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.timestampRange(Range.atLeast(1000L))) - .persist(); - var tokenAirdrop2 = domainBuilder - .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderId(tokenAirdrop.getSenderId()).timestampRange(Range.atLeast(2000L))) - .persist(); - var entityId = EntityId.of(tokenAirdrop.getSenderId()); - var outstandingTokenAirdropRequest = TokenAirdropRequest.builder() - .accountId(new EntityIdNumParameter(entityId)) - .order(order) - .build(); - - var expected = - order.isAscending() ? List.of(tokenAirdrop, tokenAirdrop2) : List.of(tokenAirdrop2, tokenAirdrop); - assertThat(repository.findAllOutstanding(outstandingTokenAirdropRequest, entityId)) - .containsExactlyElementsOf(expected); - } - @Test void noMatch() { var tokenAirdrop = domainBuilder.tokenAirdrop(FUNGIBLE_COMMON).persist(); - var entityId = EntityId.of(tokenAirdrop.getSenderId()); + var entityId = EntityId.of(tokenAirdrop.getSenderAccountId()); var request = TokenAirdropRequest.builder() .accountId(new EntityIdNumParameter(entityId)) - .entityId(new EntityIdRangeParameter(RangeOperator.GT, EntityId.of(tokenAirdrop.getReceiverId()))) + .entityIds(new Bound( + List.of(new EntityIdRangeParameter( + RangeOperator.GT, EntityId.of(tokenAirdrop.getReceiverAccountId()))), + true, + Constants.ACCOUNT_ID)) .build(); assertThat(repository.findAllOutstanding(request, entityId)).isEmpty(); } @@ -94,34 +76,56 @@ void noMatch() { void conditionalClauses(Direction order) { var sender = domainBuilder.entity().get(); var receiver = domainBuilder.entity().get(); - var token = domainBuilder.token().get(); - var serialNumber = 5L; + var tokenId = 5000L; - var tokenAirdrop = domainBuilder + var receiverSpecifiedAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderId(sender.getId()).receiverId(receiver.getId())) + .customize(a -> a.senderAccountId(sender.getId()).receiverAccountId(receiver.getId())) .persist(); - var tokenAirdrop2 = domainBuilder + var receiverSpecifiedAirdrop2 = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderId(sender.getId()).tokenId(token.getTokenId())) + .customize(a -> a.senderAccountId(sender.getId()).receiverAccountId(receiver.getId())) .persist(); - var nftAirdrop = domainBuilder - .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderId(sender.getId()) - .receiverId(receiver.getId()) - .serialNumber(serialNumber) - .tokenId(token.getTokenId())) + var tokenReceiverSpecifiedAirdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(sender.getId()) + .receiverAccountId(receiver.getId()) + .tokenId(tokenId)) + .persist(); + var tokenReceiverSpecifiedAirdrop2 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(sender.getId()) + .receiverAccountId(receiver.getId()) + .tokenId(tokenId + 1)) + .persist(); + var tokenSpecifiedAirdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> + a.senderAccountId(sender.getId()).receiverAccountId(1).tokenId(tokenId)) .persist(); - var nftAirdrop2 = domainBuilder + domainBuilder .tokenAirdrop(NON_FUNGIBLE_UNIQUE) - .customize(a -> a.senderId(sender.getId()).serialNumber(serialNumber)) + .customize(a -> a.senderAccountId(sender.getId()) + .receiverAccountId(receiver.getId()) + .serialNumber(5) + .tokenId(tokenId)) .persist(); // Default asc ordering by receiver, tokenId - var allAirdrops = List.of(nftAirdrop, tokenAirdrop, tokenAirdrop2, nftAirdrop2); - var receiverSpecifiedAirdrops = List.of(nftAirdrop, tokenAirdrop); - var tokenSpecifiedAirdrops = List.of(nftAirdrop, tokenAirdrop2); - var serialNumberAirdrops = List.of(nftAirdrop, nftAirdrop2); + var allAirdrops = List.of( + tokenSpecifiedAirdrop, + receiverSpecifiedAirdrop, + receiverSpecifiedAirdrop2, + tokenReceiverSpecifiedAirdrop, + tokenReceiverSpecifiedAirdrop2); + var receiverSpecifiedAirdrops = List.of( + receiverSpecifiedAirdrop, + receiverSpecifiedAirdrop2, + tokenReceiverSpecifiedAirdrop, + tokenReceiverSpecifiedAirdrop2); + var tokenReceiverAirdrops = List.of(tokenReceiverSpecifiedAirdrop, tokenReceiverSpecifiedAirdrop2); + var tokenSpecifiedAirdrops = + List.of(tokenSpecifiedAirdrop, tokenReceiverSpecifiedAirdrop, tokenReceiverSpecifiedAirdrop2); var orderedAirdrops = order.isAscending() ? allAirdrops : allAirdrops.reversed(); var request = TokenAirdropRequest.builder() @@ -131,24 +135,63 @@ void conditionalClauses(Direction order) { assertThat(repository.findAllOutstanding(request, sender.toEntityId())) .containsExactlyElementsOf(orderedAirdrops); - // With token id condition - var tokenIdAirdrops = order.isAscending() ? tokenSpecifiedAirdrops : tokenSpecifiedAirdrops.reversed(); + // With receiver id condition + var receiverAirdrops = order.isAscending() ? receiverSpecifiedAirdrops : receiverSpecifiedAirdrops.reversed(); request = TokenAirdropRequest.builder() .accountId(new EntityIdNumParameter(sender.toEntityId())) .order(order) - .tokenId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(token.getTokenId()))) + .entityIds(new Bound( + List.of(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver.getId()))), + true, + Constants.ACCOUNT_ID)) .build(); assertThat(repository.findAllOutstanding(request, sender.toEntityId())) - .containsExactlyElementsOf(tokenIdAirdrops); + .containsExactlyElementsOf(receiverAirdrops); - // With receiver id condition - var receiverIdAirdrops = order.isAscending() ? receiverSpecifiedAirdrops : receiverSpecifiedAirdrops.reversed(); + // With token id and receiver condition + var tokenAirdrops = order.isAscending() ? tokenReceiverAirdrops : tokenReceiverAirdrops.reversed(); + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(sender.toEntityId())) + .entityIds(new Bound( + List.of(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver.getId()))), + true, + Constants.ACCOUNT_ID)) + .order(order) + .tokenIds(new Bound( + List.of(new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(tokenId))), + false, + Constants.TOKEN_ID)) + .build(); + assertThat(repository.findAllOutstanding(request, sender.toEntityId())) + .containsExactlyElementsOf(tokenAirdrops); + + // With token id condition as primary sort field and with receiver id request = TokenAirdropRequest.builder() .accountId(new EntityIdNumParameter(sender.toEntityId())) .order(order) - .entityId(new EntityIdRangeParameter(RangeOperator.EQ, EntityId.of(receiver.getId()))) + .entityIds(new Bound( + List.of(new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(receiver.getId()))), + false, + Constants.ACCOUNT_ID)) + .tokenIds(new Bound( + List.of(new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(tokenId))), + true, + Constants.TOKEN_ID)) .build(); assertThat(repository.findAllOutstanding(request, sender.toEntityId())) - .containsExactlyElementsOf(receiverIdAirdrops); + .containsExactlyElementsOf(tokenAirdrops); + + // With token id condition but no receiver id + var tokenIdAirdrops = order.isAscending() ? tokenSpecifiedAirdrops : tokenSpecifiedAirdrops.reversed(); + request = TokenAirdropRequest.builder() + .accountId(new EntityIdNumParameter(sender.toEntityId())) + .order(order) + .tokenIds(new Bound( + List.of(new EntityIdRangeParameter(RangeOperator.GTE, EntityId.of(tokenId))), + false, + Constants.TOKEN_ID)) + .build(); + assertThat(repository.findAllOutstanding(request, sender.toEntityId())) + .containsExactlyElementsOf(tokenIdAirdrops); } } diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java index ca1efaaf0ac..dde37d63140 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/service/TokenAirdropServiceTest.java @@ -18,7 +18,6 @@ import static com.hedera.mirror.common.domain.token.TokenTypeEnum.FUNGIBLE_COMMON; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.restjava.RestJavaIntegrationTest; @@ -38,12 +37,12 @@ class TokenAirdropServiceTest extends RestJavaIntegrationTest { private static final EntityId TOKEN_ID = EntityId.of(5000L); @Test - void getOutstandingAirdrops() { + void getOutstanding() { var fungibleAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) .customize(a -> a.amount(100L) - .receiverId(RECEIVER.getId()) - .senderId(SENDER.getId()) + .receiverAccountId(RECEIVER.getId()) + .senderAccountId(SENDER.getId()) .tokenId(TOKEN_ID.getId())) .persist(); @@ -55,11 +54,11 @@ void getOutstandingAirdrops() { } @Test - void getOutstandingAirdropByAlias() { + void getOutstandingByAlias() { var entity = domainBuilder.entity().persist(); var tokenAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderId(entity.getId())) + .customize(a -> a.senderAccountId(entity.getId())) .persist(); var request = TokenAirdropRequest.builder() .accountId(new EntityIdAliasParameter(entity.getShard(), entity.getRealm(), entity.getAlias())) @@ -69,11 +68,11 @@ void getOutstandingAirdropByAlias() { } @Test - void getOutstandingAirdropByEvmAddress() { + void getOutstandingByEvmAddress() { var entity = domainBuilder.entity().persist(); var tokenAirdrop = domainBuilder .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderId(entity.getId())) + .customize(a -> a.senderAccountId(entity.getId())) .persist(); var request = TokenAirdropRequest.builder() .accountId( @@ -84,17 +83,11 @@ void getOutstandingAirdropByEvmAddress() { } @Test - void getOutstandingAirdropNotFound() { + void getOutstandingNotFound() { var request = TokenAirdropRequest.builder() .accountId(new EntityIdNumParameter(SENDER)) .build(); var response = service.getOutstandingAirdrops(request); assertThat(response).isEmpty(); } - - @Test - void getOutstandingNoSender() { - var request = TokenAirdropRequest.builder().build(); - assertThrows(IllegalArgumentException.class, () -> service.getOutstandingAirdrops(request)); - } } diff --git a/hedera-mirror-rest/api/v1/openapi.yml b/hedera-mirror-rest/api/v1/openapi.yml index 410871291d0..df6d7cd34f4 100644 --- a/hedera-mirror-rest/api/v1/openapi.yml +++ b/hedera-mirror-rest/api/v1/openapi.yml @@ -164,9 +164,9 @@ paths: - accounts /api/v1/accounts/{idOrAliasOrEvmAddress}/airdrops/outstanding: get: - summary: Get get outstanding airdrops sent by an account + summary: Get get outstanding fungible token airdrops sent by an account description: | - Returns outstanding airdrops that have been sent by an account. + Returns outstanding fungible token airdrops that have been sent by an account. NFT airdrops will not be included in the response. operationId: getOutstandingTokenAirdrops parameters: - $ref: "#/components/parameters/accountIdOrAliasOrEvmAddressPathParam" From a6459ab8f113f82ddedb8cc6bddde951b6306533 Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Thu, 12 Sep 2024 16:21:14 -0400 Subject: [PATCH 07/10] Reformat line breaks for easier diff review. Rename mapper Signed-off-by: Edwin Greene --- .../mirror/restjava/controller/TokenAirdropsController.java | 6 +++--- .../{TokenAirdropsMapper.java => TokenAirdropMapper.java} | 2 +- .../restjava/controller/TokenAirdropsControllerTest.java | 4 ++-- ...nAirdropsMapperTest.java => TokenAirdropMapperTest.java} | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) rename hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/{TokenAirdropsMapper.java => TokenAirdropMapper.java} (92%) rename hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/{TokenAirdropsMapperTest.java => TokenAirdropMapperTest.java} (96%) diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java index 7b524629cd0..7088e15ef01 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/controller/TokenAirdropsController.java @@ -29,7 +29,7 @@ import com.hedera.mirror.restjava.common.EntityIdRangeParameter; import com.hedera.mirror.restjava.common.LinkFactory; import com.hedera.mirror.restjava.dto.TokenAirdropRequest; -import com.hedera.mirror.restjava.mapper.TokenAirdropsMapper; +import com.hedera.mirror.restjava.mapper.TokenAirdropMapper; import com.hedera.mirror.restjava.service.Bound; import com.hedera.mirror.restjava.service.TokenAirdropService; import jakarta.validation.constraints.Max; @@ -58,7 +58,7 @@ public class TokenAirdropsController { TOKEN_ID, tokenAirdrop.getTokenId()); private final LinkFactory linkFactory; - private final TokenAirdropsMapper tokenAirdropsMapper; + private final TokenAirdropMapper tokenAirdropMapper; private final TokenAirdropService service; @GetMapping(value = "/outstanding") @@ -76,7 +76,7 @@ TokenAirdropsResponse getOutstandingAirdrops( .tokenIds(new Bound(tokenIds, false, TOKEN_ID)) .build(); var response = service.getOutstandingAirdrops(request); - var airdrops = tokenAirdropsMapper.map(response); + var airdrops = tokenAirdropMapper.map(response); var sort = Sort.by(order, RECEIVER_ID, TOKEN_ID); var pageable = PageRequest.of(0, limit, sort); var links = linkFactory.create(airdrops, pageable, EXTRACTOR); diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropMapper.java similarity index 92% rename from hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java rename to hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropMapper.java index 69a52ccc8d9..21bd71ad4d2 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapper.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/TokenAirdropMapper.java @@ -22,7 +22,7 @@ import org.mapstruct.Named; @Mapper(config = MapperConfiguration.class) -public interface TokenAirdropsMapper extends CollectionMapper { +public interface TokenAirdropMapper extends CollectionMapper { @Mapping(source = "receiverAccountId", target = "receiverId") @Mapping(source = "senderAccountId", target = "senderId") diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java index f95e23b893c..b94adfcf72b 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java @@ -26,7 +26,7 @@ import com.hedera.mirror.common.util.DomainUtils; import com.hedera.mirror.rest.model.Links; import com.hedera.mirror.rest.model.TokenAirdropsResponse; -import com.hedera.mirror.restjava.mapper.TokenAirdropsMapper; +import com.hedera.mirror.restjava.mapper.TokenAirdropMapper; import java.util.List; import lombok.RequiredArgsConstructor; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; @@ -43,7 +43,7 @@ @RequiredArgsConstructor class TokenAirdropsControllerTest extends ControllerTest { - private final TokenAirdropsMapper mapper; + private final TokenAirdropMapper mapper; @DisplayName("/api/v1/accounts/{id}/airdrops/outstanding") @Nested diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropMapperTest.java similarity index 96% rename from hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java rename to hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropMapperTest.java index 4e9fbda42c1..79d62b6493c 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropsMapperTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/mapper/TokenAirdropMapperTest.java @@ -31,16 +31,16 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -class TokenAirdropsMapperTest { +class TokenAirdropMapperTest { private CommonMapper commonMapper; private DomainBuilder domainBuilder; - private TokenAirdropsMapper mapper; + private TokenAirdropMapper mapper; @BeforeEach void setup() { commonMapper = new CommonMapperImpl(); - mapper = new TokenAirdropsMapperImpl(commonMapper); + mapper = new TokenAirdropMapperImpl(commonMapper); domainBuilder = new DomainBuilder(); } From 3f72fd3c8caf6f25d4a067e496f20d6be8b84767 Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Fri, 13 Sep 2024 16:20:27 -0400 Subject: [PATCH 08/10] Update jooqRepository and simplify conditionals Signed-off-by: Edwin Greene --- .../restjava/repository/JooqRepository.java | 45 +++--- .../NftAllowanceRepositoryCustomImpl.java | 21 +-- .../TokenAirdropRepositoryCustomImpl.java | 35 +---- .../hedera/mirror/restjava/service/Bound.java | 3 + .../TokenAirdropsControllerTest.java | 145 ++++++++---------- .../TokenAirdropRepositoryTest.java | 2 +- 6 files changed, 110 insertions(+), 141 deletions(-) diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java index e0b270894e9..b2cd9caa163 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java @@ -42,15 +42,11 @@ default Condition getCondition(Field field, RangeOperator operator, Long v return operator.getFunction().apply(field, value); } - default Condition getBoundCondition(FieldBound fieldBound) { - var primaryBound = fieldBound.primaryBound(); - var primaryLower = EntityIdRangeParameter.EMPTY; - var primaryUpper = EntityIdRangeParameter.EMPTY; - - if (primaryBound != null) { - primaryLower = primaryBound.getLower(); - primaryUpper = primaryBound.getUpper(); - + default Condition getBoundCondition(ConditionalFieldBounds fieldBounds) { + var primaryBound = fieldBounds.primary.bound(); + var primaryLower = primaryBound.getLower(); + var primaryUpper = primaryBound.getUpper(); + if (!primaryBound.isEmpty()) { // If the primary param has a range with a single value, rewrite it to EQ if (primaryBound.hasEqualBounds()) { primaryLower = new EntityIdRangeParameter(EQ, EntityId.of(primaryBound.adjustLowerBound())); @@ -58,20 +54,18 @@ default Condition getBoundCondition(FieldBound fieldBound) { } } - var secondaryBound = fieldBound.secondaryBound(); - var secondaryLower = EntityIdRangeParameter.EMPTY; - var secondaryUpper = EntityIdRangeParameter.EMPTY; - if (secondaryBound != null) { - secondaryLower = secondaryBound.getLower(); - secondaryUpper = secondaryBound.getUpper(); + var secondaryBound = fieldBounds.secondary().bound(); + var secondaryLower = secondaryBound.getLower(); + var secondaryUpper = secondaryBound.getUpper(); + if (!secondaryBound.isEmpty()) { // If the secondary param operator is EQ, set the secondary upper bound to the same if (secondaryLower != null && secondaryLower.operator() == EQ) { secondaryUpper = secondaryLower; } } - var primaryField = fieldBound.primaryField(); - var secondaryField = fieldBound.secondaryField(); + var primaryField = fieldBounds.primary().field(); + var secondaryField = fieldBounds.secondary().field(); var lowerCondition = getOuterBoundCondition(primaryLower, secondaryLower, primaryField, secondaryField); var middleCondition = getMiddleCondition(primaryLower, secondaryLower, primaryField, secondaryField) .and(getMiddleCondition(primaryUpper, secondaryUpper, primaryField, secondaryField)); @@ -113,8 +107,8 @@ private Condition getMiddleCondition( EntityIdRangeParameter secondaryParam, Field primaryField, Field secondaryField) { - if (primaryParam == null || primaryParam.equals(EntityIdRangeParameter.EMPTY)) { - return noCondition(); + if (primaryParam == null) { + return getCondition(secondaryField, secondaryParam); } // When the primary param operator is EQ, or the secondary param operator is EQ, don't adjust the value for the @@ -128,5 +122,16 @@ private Condition getMiddleCondition( return getCondition(primaryField, primaryParam.operator(), value); } - record FieldBound(Field primaryField, Field secondaryField, Bound primaryBound, Bound secondaryBound) {} + record ConditionalFieldBounds(FieldBound primary, FieldBound secondary) {} + + record FieldBound(Field field, Bound bound) { + public FieldBound { + if (field == null) { + throw new IllegalArgumentException("Conditional field cannot be null"); + } + if (bound == null) { + bound = Bound.EMPTY; + } + } + } } diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java index 8b58d0f1a76..2dff5333c09 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/NftAllowanceRepositoryCustomImpl.java @@ -50,8 +50,8 @@ class NftAllowanceRepositoryCustomImpl implements NftAllowanceRepositoryCustom { @Override public Collection findAll(NftAllowanceRequest request, EntityId accountId) { boolean byOwner = request.isOwner(); - var fieldBound = getFieldBound(request, byOwner); - var condition = getBaseCondition(accountId, byOwner).and(getBoundCondition(fieldBound)); + var fieldBounds = getFieldBounds(request, byOwner); + var condition = getBaseCondition(accountId, byOwner).and(getBoundCondition(fieldBounds)); return dslContext .selectFrom(NFT_ALLOWANCE) .where(condition) @@ -65,18 +65,11 @@ private Condition getBaseCondition(EntityId accountId, boolean byOwner) { .and(APPROVAL_CONDITION); } - private FieldBound getFieldBound(NftAllowanceRequest request, boolean byOwner) { - return byOwner - ? new FieldBound( - NFT_ALLOWANCE.SPENDER, - NFT_ALLOWANCE.TOKEN_ID, - request.getOwnerOrSpenderIds(), - request.getTokenIds()) - : new FieldBound( - NFT_ALLOWANCE.OWNER, - NFT_ALLOWANCE.TOKEN_ID, - request.getOwnerOrSpenderIds(), - request.getTokenIds()); + private ConditionalFieldBounds getFieldBounds(NftAllowanceRequest request, boolean byOwner) { + var field = byOwner ? NFT_ALLOWANCE.SPENDER : NFT_ALLOWANCE.OWNER; + return new ConditionalFieldBounds( + new FieldBound(field, request.getOwnerOrSpenderIds()), + new FieldBound(NFT_ALLOWANCE.TOKEN_ID, request.getTokenIds())); } private record OrderSpec(boolean byOwner, Direction direction) {} diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java index 42470198321..fd132a4a156 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java @@ -44,9 +44,9 @@ class TokenAirdropRepositoryCustomImpl implements TokenAirdropRepositoryCustom { @Override public Collection findAllOutstanding(TokenAirdropRequest request, EntityId accountId) { - var fieldBound = getFieldBound(request, true); + var fieldBounds = getFieldBound(request, true); var condition = getBaseCondition(accountId, true) - .and(getBoundCondition(fieldBound)) + .and(getBoundCondition(fieldBounds)) // Exclude NFTs .and(TOKEN_AIRDROP.SERIAL_NUMBER.eq(0L)); @@ -59,32 +59,11 @@ public Collection findAllOutstanding(TokenAirdropRequest request, .fetchInto(TokenAirdrop.class); } - private FieldBound getFieldBound(TokenAirdropRequest request, boolean outstanding) { - if (outstanding) { - return (request.getEntityIds() == null || request.getEntityIds().isEmpty()) - ? new FieldBound( - TOKEN_AIRDROP.TOKEN_ID, - TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID, - request.getTokenIds(), - request.getEntityIds()) - : new FieldBound( - TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID, - TOKEN_AIRDROP.TOKEN_ID, - request.getEntityIds(), - request.getTokenIds()); - } else { - return (request.getEntityIds() == null || request.getEntityIds().isEmpty()) - ? new FieldBound( - TOKEN_AIRDROP.TOKEN_ID, - TOKEN_AIRDROP.SENDER_ACCOUNT_ID, - request.getTokenIds(), - request.getEntityIds()) - : new FieldBound( - TOKEN_AIRDROP.SENDER_ACCOUNT_ID, - TOKEN_AIRDROP.TOKEN_ID, - request.getEntityIds(), - request.getTokenIds()); - } + private ConditionalFieldBounds getFieldBound(TokenAirdropRequest request, boolean outstanding) { + var primaryField = outstanding ? TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID : TOKEN_AIRDROP.SENDER_ACCOUNT_ID; + var primary = new FieldBound(primaryField, request.getEntityIds()); + var secondary = new FieldBound(TOKEN_AIRDROP.TOKEN_ID, request.getTokenIds()); + return new ConditionalFieldBounds(primary, secondary); } private Condition getBaseCondition(EntityId accountId, boolean outstanding) { diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/Bound.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/Bound.java index 910c3c4c665..2172760ae6f 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/Bound.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/service/Bound.java @@ -22,10 +22,13 @@ import java.util.EnumMap; import java.util.List; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; import org.springframework.util.CollectionUtils; public class Bound { + public static final Bound EMPTY = new Bound(null, false, StringUtils.EMPTY); + @Getter private EntityIdRangeParameter lower; diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java index b94adfcf72b..2ad1917772d 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/controller/TokenAirdropsControllerTest.java @@ -21,7 +21,6 @@ import static org.assertj.core.api.Assertions.assertThat; import com.google.common.io.BaseEncoding; -import com.hedera.mirror.common.domain.entity.EntityId; import com.hedera.mirror.common.domain.token.TokenAirdrop; import com.hedera.mirror.common.util.DomainUtils; import com.hedera.mirror.rest.model.Links; @@ -119,50 +118,6 @@ void alias() { assertThat(response.getBody().getAirdrops().getFirst()).isEqualTo(mapper.map(tokenAirdrop)); } - @Test - void followAscendingOrderLink() { - // Given - var entity = domainBuilder.entity().persist(); - var id = entity.getId(); - var tokenAirdrop1 = domainBuilder - .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderAccountId(entity.getId())) - .persist(); - var tokenAirdrop2 = domainBuilder - .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderAccountId(entity.getId())) - .persist(); - var baseLink = "/api/v1/accounts/%d/airdrops/outstanding".formatted(id); - - // When - var result = restClient.get().uri("?limit=1", id).retrieve().body(TokenAirdropsResponse.class); - var nextParams = "?limit=1&receiver.id=gte:%s&token.id=gt:%s" - .formatted( - EntityId.of(tokenAirdrop1.getReceiverAccountId()), EntityId.of(tokenAirdrop1.getTokenId())); - - // Then - assertThat(result).isEqualTo(getExpectedResponse(List.of(tokenAirdrop1), baseLink + nextParams)); - - // When follow link - result = restClient.get().uri(nextParams, id).retrieve().body(TokenAirdropsResponse.class); - - // Then - nextParams = "?limit=1&receiver.id=gte:%s&token.id=gt:%s" - .formatted( - EntityId.of(tokenAirdrop2.getReceiverAccountId()), EntityId.of(tokenAirdrop2.getTokenId())); - assertThat(result).isEqualTo(getExpectedResponse(List.of(tokenAirdrop2), baseLink + nextParams)); - - // When follow link 2 - result = restClient - .get() - .uri(nextParams, tokenAirdrop1.getReceiverAccountId()) - .retrieve() - .body(TokenAirdropsResponse.class); - // Then - assertThat(result).isEqualTo(getExpectedResponse(List.of(), null)); - } - - // TODO fix this test @Test void followDescendingOrderLink() { // Given @@ -233,9 +188,8 @@ void followDescendingOrderLink() { assertThat(result).isEqualTo(getExpectedResponse(List.of(), null)); } - // TODO fix this test @Test - void paginationReceiverAndToken() { + void followAscendingOrderLink() { // Given var entity = domainBuilder.entity().persist(); var airdrop = domainBuilder @@ -256,37 +210,91 @@ void paginationReceiverAndToken() { .receiverAccountId(4L) .tokenId(5L)) .persist(); - var airdrop4 = domainBuilder - .tokenAirdrop(FUNGIBLE_COMMON) - .customize(a -> a.senderAccountId(entity.getId()) - .receiverAccountId(5L) - .tokenId(6L)) - .persist(); var baseLink = "/api/v1/accounts/%d/airdrops/outstanding".formatted(entity.getId()); - var uriParams = "?limit=1&receiver.id=gt:2&token.id=gt:4"; var nextParams = "?limit=1&receiver.id=gte:0.0.3&token.id=gt:0.0.5"; - // When + // When no primary or secondary parameters are specified + var uriParams = "?limit=1"; var result = restClient.get().uri(uriParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); + // Then + assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop), baseLink + nextParams)); + + // When primary and secondary fields are specified + uriParams = "?limit=1&receiver.id=gt:2&token.id=gt:4"; + result = restClient.get().uri(uriParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); + // Then + assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop), baseLink + nextParams)); + // When only the secondary field is specified + uriParams = "?limit=1&token.id=gt:4"; + result = restClient.get().uri(uriParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); // Then assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop), baseLink + nextParams)); // When follow link result = restClient.get().uri(nextParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); - - // Then nextParams = "?limit=1&receiver.id=gte:0.0.3&token.id=gt:0.0.6"; + // Then assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop2), baseLink + nextParams)); // When follow link result = restClient.get().uri(nextParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); - + nextParams = "?limit=1&receiver.id=gte:0.0.4&token.id=gt:0.0.5"; // Then - nextParams = "?limit=1&receiver.id=gte:0.0.3&token.id=gt:0.0.5"; assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop3), baseLink + nextParams)); + + // When follow link + result = restClient.get().uri(nextParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); + // Then + assertThat(result).isEqualTo(getExpectedResponse(List.of(), null)); + } + + @Test + void allParameters() { + // Given + var entity = domainBuilder.entity().persist(); + var airdrop = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId()) + .receiverAccountId(2L) + .tokenId(5L)) + .persist(); + var airdrop2 = domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId()) + .receiverAccountId(3L) + .tokenId(5L)) + .persist(); + domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId()) + .receiverAccountId(3L) + .tokenId(6L)) + .persist(); + domainBuilder + .tokenAirdrop(FUNGIBLE_COMMON) + .customize(a -> a.senderAccountId(entity.getId()) + .receiverAccountId(4L) + .tokenId(5L)) + .persist(); + + var baseLink = "/api/v1/accounts/%d/airdrops/outstanding".formatted(entity.getId()); + var uriParams = "?limit=1&receiver.id=gte:0.0.1&receiver.id=lt:0.0.4&token.id=lte:0.0.5&token.id=gt:0.0.3"; + + // When + var result = + restClient.get().uri(uriParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); + var nextParams = "?limit=1&receiver.id=lt:0.0.4&receiver.id=gte:0.0.2&token.id=lte:0.0.5&token.id=gt:0.0.5"; + // Then + assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop), baseLink + nextParams)); + + // When + result = restClient.get().uri(nextParams, entity.getId()).retrieve().body(TokenAirdropsResponse.class); + nextParams = "?limit=1&receiver.id=lt:0.0.4&receiver.id=gte:0.0.3&token.id=lte:0.0.5&token.id=gt:0.0.5"; + // Then + assertThat(result).isEqualTo(getExpectedResponse(List.of(airdrop2), baseLink + nextParams)); } @ParameterizedTest @@ -418,25 +426,6 @@ void invalidTokenId(String tokenId) { HttpClientErrorException.BadRequest.class, "Failed to convert 'token.id' with value: '" + tokenId + "'"); } - - // @ParameterizedTest - // @CsvSource({ - // "?token.id=gt:0.0.1000&token.id=lt:0.0.2000,Only one occurrence of token.id", - // }) - // void invalidNumberOfParameters(String uri) { - // // When - // ThrowingCallable callable = () -> restClient - // .get() - // .uri(uri, "0.0.1001") - // .retrieve() - // .body(TokenAirdropsResponse.class); - // - // // Then - // validateError( - // callable, - // HttpClientErrorException.BadRequest.class, - // "Failed to convert 'token.id' with value: "); - // } } private TokenAirdropsResponse getExpectedResponse(List tokenAirdrops, String next) { diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java index f4e3ce3383f..7a0a587fb5e 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryTest.java @@ -73,7 +73,7 @@ void noMatch() { @ParameterizedTest @EnumSource(Direction.class) - void conditionalClauses(Direction order) { + void conditionalClausesByDirection(Direction order) { var sender = domainBuilder.entity().get(); var receiver = domainBuilder.entity().get(); var tokenId = 5000L; From e131e22d4c8bd34373a7d89dd5f21b1b7f3e52da Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Fri, 13 Sep 2024 16:38:42 -0400 Subject: [PATCH 09/10] Fix code smells Signed-off-by: Edwin Greene --- .../mirror/restjava/repository/JooqRepository.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java index b2cd9caa163..f65443b88f4 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/JooqRepository.java @@ -46,22 +46,18 @@ default Condition getBoundCondition(ConditionalFieldBounds fieldBounds) { var primaryBound = fieldBounds.primary.bound(); var primaryLower = primaryBound.getLower(); var primaryUpper = primaryBound.getUpper(); - if (!primaryBound.isEmpty()) { + if (!primaryBound.isEmpty() && primaryBound.hasEqualBounds()) { // If the primary param has a range with a single value, rewrite it to EQ - if (primaryBound.hasEqualBounds()) { - primaryLower = new EntityIdRangeParameter(EQ, EntityId.of(primaryBound.adjustLowerBound())); - primaryUpper = null; - } + primaryLower = new EntityIdRangeParameter(EQ, EntityId.of(primaryBound.adjustLowerBound())); + primaryUpper = null; } var secondaryBound = fieldBounds.secondary().bound(); var secondaryLower = secondaryBound.getLower(); var secondaryUpper = secondaryBound.getUpper(); - if (!secondaryBound.isEmpty()) { + if (!secondaryBound.isEmpty() && (secondaryLower != null && secondaryLower.operator() == EQ)) { // If the secondary param operator is EQ, set the secondary upper bound to the same - if (secondaryLower != null && secondaryLower.operator() == EQ) { - secondaryUpper = secondaryLower; - } + secondaryUpper = secondaryLower; } var primaryField = fieldBounds.primary().field(); From 8c0abbc68a23d5fd4e6a1f4592670bf34b00126f Mon Sep 17 00:00:00 2001 From: Edwin Greene Date: Mon, 16 Sep 2024 11:00:52 -0400 Subject: [PATCH 10/10] Update openapi, tests and query Signed-off-by: Edwin Greene --- .../repository/TokenAirdropRepositoryCustomImpl.java | 8 ++++---- .../restjava/common/NumberRangeParameterTest.java | 12 ++++++------ hedera-mirror-rest/api/v1/openapi.yml | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java index fd132a4a156..13c308631f3 100644 --- a/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java +++ b/hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/repository/TokenAirdropRepositoryCustomImpl.java @@ -47,6 +47,7 @@ public Collection findAllOutstanding(TokenAirdropRequest request, var fieldBounds = getFieldBound(request, true); var condition = getBaseCondition(accountId, true) .and(getBoundCondition(fieldBounds)) + .and(TOKEN_AIRDROP.STATE.eq(AirdropState.PENDING)) // Exclude NFTs .and(TOKEN_AIRDROP.SERIAL_NUMBER.eq(0L)); @@ -68,9 +69,8 @@ private ConditionalFieldBounds getFieldBound(TokenAirdropRequest request, boolea private Condition getBaseCondition(EntityId accountId, boolean outstanding) { return getCondition( - outstanding ? TOKEN_AIRDROP.SENDER_ACCOUNT_ID : TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID, - EQ, - accountId.getId()) - .and(TOKEN_AIRDROP.STATE.eq(AirdropState.PENDING)); + outstanding ? TOKEN_AIRDROP.SENDER_ACCOUNT_ID : TOKEN_AIRDROP.RECEIVER_ACCOUNT_ID, + EQ, + accountId.getId()); } } diff --git a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java index 3fe4ff84637..9b732944d7f 100644 --- a/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java +++ b/hedera-mirror-rest-java/src/test/java/com/hedera/mirror/restjava/common/NumberRangeParameterTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -60,16 +61,15 @@ void testNoOperatorPresent() { @ParameterizedTest @EnumSource(RangeOperator.class) - void testGte(RangeOperator operator) { + void testRangeOperator(RangeOperator operator) { assertThat(new NumberRangeParameter(operator, 2000L)) .isEqualTo(NumberRangeParameter.valueOf(operator + ":2000")); } - @Test - void testEmpty() { - assertThat(NumberRangeParameter.EMPTY) - .isEqualTo(NumberRangeParameter.valueOf("")) - .isEqualTo(NumberRangeParameter.valueOf(null)); + @ParameterizedTest + @NullAndEmptySource + void testEmpty(String input) { + assertThat(NumberRangeParameter.valueOf(input)).isEqualTo(NumberRangeParameter.EMPTY); } @ParameterizedTest diff --git a/hedera-mirror-rest/api/v1/openapi.yml b/hedera-mirror-rest/api/v1/openapi.yml index e936d53a7d7..94e8a8969bb 100644 --- a/hedera-mirror-rest/api/v1/openapi.yml +++ b/hedera-mirror-rest/api/v1/openapi.yml @@ -166,7 +166,7 @@ paths: get: summary: Get get outstanding fungible token airdrops sent by an account description: | - Returns outstanding fungible token airdrops that have been sent by an account. NFT airdrops will not be included in the response. + Returns outstanding fungible token airdrops that have been sent by an account. This API is currently under development. Support for NFT airdrops will be added in a future release. operationId: getOutstandingTokenAirdrops parameters: - $ref: "#/components/parameters/accountIdOrAliasOrEvmAddressPathParam"