Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add outstanding token airdrops to REST API #9286

Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@
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 SENDER_ID = "sender.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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 NumberRangeParameter(RangeOperator operator, Long value) implements RangeParameter<Long> {

public static final NumberRangeParameter EMPTY = new NumberRangeParameter(null, null);

public static NumberRangeParameter valueOf(String valueRangeParam) {
if (StringUtils.isBlank(valueRangeParam)) {
return EMPTY;
}

var splitVal = valueRangeParam.split(":");
return switch (splitVal.length) {
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: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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,13 @@ public interface RangeParameter<T> {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,7 +53,6 @@
@RestController
public class AllowancesController {

private static final String DEFAULT_LIMIT = "25";
private static final Map<Boolean, Function<NftAllowance, Map<String, String>>> EXTRACTORS = Map.of(
true,
nftAllowance -> ImmutableSortedMap.of(
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.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;
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.LinkFactory;
import com.hedera.mirror.restjava.dto.TokenAirdropRequest;
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;
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;
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<TokenAirdrop, Map<String, String>> EXTRACTOR = tokenAirdrop -> ImmutableSortedMap.of(
RECEIVER_ID, tokenAirdrop.getReceiverId(),
TOKEN_ID, tokenAirdrop.getTokenId());

private final LinkFactory linkFactory;
private final TokenAirdropMapper tokenAirdropMapper;
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) @Size(max = 2) List<EntityIdRangeParameter> receiverIds,
@RequestParam(name = TOKEN_ID, required = false) @Size(max = 2) List<EntityIdRangeParameter> tokenIds) {
var request = TokenAirdropRequest.builder()
.accountId(id)
.entityIds(new Bound(receiverIds, true, ACCOUNT_ID))
.limit(limit)
.order(order)
.tokenIds(new Bound(tokenIds, false, TOKEN_ID))
.build();
var response = service.getOutstandingAirdrops(request);
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);
return new TokenAirdropsResponse().airdrops(airdrops).links(links);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.service.Bound;
import lombok.Builder;
import lombok.Data;
import org.springframework.data.domain.Sort;

@Data
@Builder
public class TokenAirdropRequest {

// Sender Id for Outstanding Airdrops, Receiver Id for Pending Airdrops
private EntityIdParameter accountId;

@Builder.Default
private int limit = 25;

@Builder.Default
private Sort.Direction order = Sort.Direction.ASC;

// Receiver Id for Outstanding Airdrops, Sender Id for Pending Airdrops
private Bound entityIds;

private Bound tokenIds;
}
Original file line number Diff line number Diff line change
@@ -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<S, T> {

T map(S source);

default List<T> map(Collection<S> sources) {
if (sources == null) {
return Collections.emptyList();

Check warning on line 30 in hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/CollectionMapper.java

View check run for this annotation

Codecov / codecov/patch

hedera-mirror-rest-java/src/main/java/com/hedera/mirror/restjava/mapper/CollectionMapper.java#L30

Added line #L30 was not covered by tests
}

List<T> list = new ArrayList<>(sources.size());
for (S source : sources) {
list.add(map(source));
}

return list;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<NftAllowance, com.hedera.mirror.rest.model.NftAllowance> {

@Mapping(source = "timestampRange", target = "timestamp")
com.hedera.mirror.rest.model.NftAllowance map(NftAllowance source);

default List<com.hedera.mirror.rest.model.NftAllowance> map(Collection<NftAllowance> source) {
if (source == null) {
return Collections.emptyList();
}

List<com.hedera.mirror.rest.model.NftAllowance> list = new ArrayList<>(source.size());
for (NftAllowance allowance : source) {
list.add(map(allowance));
}

return list;
}
}
Original file line number Diff line number Diff line change
@@ -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 com.hedera.mirror.common.domain.token.TokenAirdrop;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;

@Mapper(config = MapperConfiguration.class)
public interface TokenAirdropMapper extends CollectionMapper<TokenAirdrop, com.hedera.mirror.rest.model.TokenAirdrop> {

@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);

@Named("mapToNullIfZero")
default Long mapToNullIfZero(long serialNumber) {
if (serialNumber == 0L) {
return null;
}
return serialNumber;
}
}
Loading