Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions charts/hedera-mirror-rest-java/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,17 @@ gateway:
- path:
type: RegularExpression
value: '/api/v1/accounts/(\d+\.){0,2}(\d+|(0x)?[A-Fa-f0-9]{40}|(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}|[A-Z2-7]{4,5}|[A-Z2-7]{7,8}))/airdrops/pending'
{{- if .Values.routes.exchangeRate }}
- path:
type: RegularExpression
value: '/api/v1/topics/(\d+\.){0,2}\d+$'
value: '/api/v1/network/exchangerate'
{{- end }}
- path:
type: RegularExpression
value: '/api/v1/network/stake$'
- path:
type: RegularExpression
value: '/api/v1/topics/(\d+\.){0,2}\d+$'
target:
group: ""
kind: Service
Expand Down Expand Up @@ -134,8 +139,10 @@ ingress:
- path: '/api/v1/accounts/(\d+\.){0,2}(\d+|(0x)?[A-Fa-f0-9]{40}|(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}|[A-Z2-7]{4,5}|[A-Z2-7]{7,8}))/allowances/nfts'
- path: '/api/v1/accounts/(\d+\.){0,2}(\d+|(0x)?[A-Fa-f0-9]{40}|(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}|[A-Z2-7]{4,5}|[A-Z2-7]{7,8}))/airdrops/outstanding'
- path: '/api/v1/accounts/(\d+\.){0,2}(\d+|(0x)?[A-Fa-f0-9]{40}|(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}|[A-Z2-7]{4,5}|[A-Z2-7]{7,8}))/airdrops/pending'
- path: '/api/v1/topics/(\d+\.){0,2}\d+$'
- path: '/api/v1/network/exchangerate'
condition: "{{ .Values.routes.exchangeRate }}"
- path: '/api/v1/network/stake$'
- path: '/api/v1/topics/(\d+\.){0,2}\d+$'
tls:
enabled: false
secretName: ""
Expand Down Expand Up @@ -295,7 +302,6 @@ prometheusRules:
application: rest-java
severity: warning


readinessProbe:
httpGet:
path: /actuator/health/readiness
Expand All @@ -316,7 +322,8 @@ resources:

revisionHistoryLimit: 3

routes: {}
routes:
exchangeRate: false

securityContext:
allowPrivilegeEscalation: false
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,9 @@ configs:

# rest-java host
location ~ "^/api/v1/accounts/(\d+\.){0,2}(\d+|(0x)?[A-Fa-f0-9]{40}|(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}|[A-Z2-7]{4,5}|[A-Z2-7]{7,8}))/(allowances/nfts|airdrops)" { proxy_pass http://rest_java_host$$request_uri; }
location ~ "^/api/v1/topics/(\d+\.){0,2}\d+$" { proxy_pass http://rest_java_host$$request_uri; }
location = /api/v1/network/exchangerate { proxy_pass http://rest_java_host$$request_uri; }
location = /api/v1/network/stake { proxy_pass http://rest_java_host$$request_uri; }
location ~ "^/api/v1/topics/(\d+\.){0,2}\d+$" { proxy_pass http://rest_java_host$$request_uri; }

# rest host
location /api/v1/ { proxy_pass http://rest_host$$request_uri; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ List<FileData> findAddressBooksBetween(
long startConsensusTimestamp, long endConsensusTimestamp, Collection<Long> entityIds, long limit);

@Query(
nativeQuery = true,
value =
"""
select
min(consensus_timestamp) as consensus_timestamp,
max(consensus_timestamp) as consensus_timestamp,
?1 as entity_id,
string_agg(file_data, '' order by consensus_timestamp) as file_data,
null as transaction_type
Expand All @@ -62,13 +63,11 @@ List<FileData> findAddressBooksBetween(
from file_data
where entity_id = ?1
and consensus_timestamp <= ?2
and (transaction_type = 17
or (transaction_type = 19
and
length(file_data) <> 0))
and (transaction_type = 17 or (transaction_type = 19 and length(file_data) <> 0))
order by consensus_timestamp desc
limit 1
) and consensus_timestamp <= ?2""",
nativeQuery = true)
) and consensus_timestamp <= ?2
and (transaction_type <> 19 or length(file_data) <> 0)
""")
Optional<FileData> getFileAtTimestamp(long fileId, long timestamp);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class FileDataRepositoryTest extends ImporterIntegrationTest {
final class FileDataRepositoryTest extends ImporterIntegrationTest {

@Resource
private FileDataRepository fileDataRepository;
Expand Down Expand Up @@ -133,6 +133,7 @@ void getFileAtTimestamp() {
.customize(
f -> f.entityId(create.getEntityId()).transactionType(TransactionType.FILEAPPEND.getProtoId()))
.persist();
expected.setConsensusTimestamp(append.getConsensusTimestamp());
expected.setFileData(Bytes.concat(create.getFileData(), append.getFileData()));
softly.assertThat(fileDataRepository.getFileAtTimestamp(entityId, append.getConsensusTimestamp()))
.contains(expected);
Expand Down
1 change: 1 addition & 0 deletions rest-java/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-actuator-autoconfigure")
implementation("org.springframework.boot:spring-boot-configuration-processor")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.retry:spring-retry")
runtimeOnly("org.postgresql:postgresql")
testImplementation(project(path = ":common", configuration = "testClasses"))
testImplementation("org.mockito:mockito-inline")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class Constants {
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 TIMESTAMP = "timestamp";
public static final String TOKEN_ID = "token.id";

public static final int MAX_LIMIT = 100;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.hiero.mirror.rest.model.ErrorStatus;
import org.hiero.mirror.rest.model.ErrorStatusMessagesInner;
import org.hiero.mirror.restjava.RestJavaProperties;
import org.springframework.beans.TypeMismatchException;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.support.DefaultMessageSourceResolvable;
Expand All @@ -45,6 +46,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.util.WebUtils;
Expand All @@ -65,6 +67,14 @@ private void responseHeaders(HttpServletRequest request, HttpServletResponse res
responseHeaders.forEach(response::setHeader);
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
private ResponseEntity<Object> typeMismatch(final TypeMismatchException e, final WebRequest request) {
// Explicitly don't log the parameter value to avoid cross-site scripting attacks
final var detail = "Failed to convert '%s'".formatted(e.getPropertyName());
var problem = ProblemDetail.forStatusAndDetail(BAD_REQUEST, detail);
return handleExceptionInternal(e, problem, null, BAD_REQUEST, request);
}

@ExceptionHandler({
HttpMessageConversionException.class,
IllegalArgumentException.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,61 @@

package org.hiero.mirror.restjava.controller;

import static org.hiero.mirror.restjava.common.Constants.TIMESTAMP;

import jakarta.validation.constraints.Size;
import lombok.RequiredArgsConstructor;
import org.hiero.mirror.rest.model.NetworkExchangeRateSetResponse;
import org.hiero.mirror.rest.model.NetworkStakeResponse;
import org.hiero.mirror.restjava.common.RangeOperator;
import org.hiero.mirror.restjava.jooq.domain.tables.FileData;
import org.hiero.mirror.restjava.mapper.ExchangeRateMapper;
import org.hiero.mirror.restjava.mapper.NetworkStakeMapper;
import org.hiero.mirror.restjava.parameter.TimestampParameter;
import org.hiero.mirror.restjava.service.Bound;
import org.hiero.mirror.restjava.service.FileService;
import org.hiero.mirror.restjava.service.NetworkService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/api/v1/network")
@RequiredArgsConstructor
@RestController
public class NetworkController {
final class NetworkController {

private final ExchangeRateMapper exchangeRateMapper;
private final FileService fileService;
private final NetworkService networkService;
private final NetworkStakeMapper networkStakeMapper;

@GetMapping("/exchangerate")
NetworkExchangeRateSetResponse getExchangeRate(
@RequestParam(required = false) @Size(max = 2) TimestampParameter[] timestamp) {
final var bound = timestampBound(timestamp);
final var exchangeRateSet = fileService.getExchangeRate(bound);
return exchangeRateMapper.map(exchangeRateSet);
}

@GetMapping("/stake")
NetworkStakeResponse getNetworkStake() {
final var networkStake = networkService.getLatestNetworkStake();
return networkStakeMapper.map(networkStake);
}

private Bound timestampBound(TimestampParameter[] timestamp) {
if (timestamp == null || timestamp.length == 0) {
return Bound.EMPTY;
}

for (int i = 0; i < timestamp.length; ++i) {
var param = timestamp[i];
if (param.operator() == RangeOperator.EQ) {
timestamp[i] = new TimestampParameter(RangeOperator.LTE, param.value());
}
}

return new Bound(timestamp, false, TIMESTAMP, FileData.FILE_DATA.CONSENSUS_TIMESTAMP);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: Apache-2.0

package org.hiero.mirror.restjava.dto;

import com.google.protobuf.GeneratedMessage;
import org.hiero.mirror.common.domain.file.FileData;

public record SystemFile<T extends GeneratedMessage>(FileData fileData, T protobuf) {}

Check notice on line 8 in rest-java/src/main/java/org/hiero/mirror/restjava/dto/SystemFile.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

rest-java/src/main/java/org/hiero/mirror/restjava/dto/SystemFile.java#L8

'{' is not followed by whitespace.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.google.common.collect.Range;
import com.google.protobuf.InvalidProtocolBufferException;
import com.hederahashgraph.api.proto.java.KeyList;
import com.hederahashgraph.api.proto.java.TimestampSeconds;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
Expand Down Expand Up @@ -38,6 +39,7 @@
Pattern PATTERN_ECDSA = Pattern.compile("^(3a21|32250a233a21|2a29080112250a233a21)([A-Fa-f0-9]{66})$");
Pattern PATTERN_ED25519 = Pattern.compile("^(1220|32240a221220|2a28080112240a221220)([A-Fa-f0-9]{64})$");
long SECONDS_PER_DAY = 86400L;
String TIMESTAMP_ZERO = "0.0";

Check notice on line 42 in rest-java/src/main/java/org/hiero/mirror/restjava/mapper/CommonMapper.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

rest-java/src/main/java/org/hiero/mirror/restjava/mapper/CommonMapper.java#L42

Implied modifier 'final' should be explicit.

default String mapEntityId(Long source) {
if (source == null || source == 0) {
Expand Down Expand Up @@ -114,9 +116,13 @@
}

@Named(QUALIFIER_TIMESTAMP)
default String mapTimestamp(long timestamp) {
default String mapTimestamp(Long timestamp) {
if (timestamp == null) {
return null;
}

if (timestamp == 0) {
return "0.0";
return TIMESTAMP_ZERO;
}

var timestampString = StringUtils.leftPad(String.valueOf(timestamp), NANO_DIGITS + 1, '0');
Expand All @@ -125,6 +131,13 @@
.toString();
}

default Long mapTimestampSeconds(TimestampSeconds source) {

Check notice on line 134 in rest-java/src/main/java/org/hiero/mirror/restjava/mapper/CommonMapper.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

rest-java/src/main/java/org/hiero/mirror/restjava/mapper/CommonMapper.java#L134

Implied modifier 'public' should be explicit.
if (source == null) {
return null;
}
return source.getSeconds();
}

@Named(QUALIFIER_TIMESTAMP_RANGE)
default TimestampRange mapTimestampRange(long stakingPeriod) {
final long fromNs = stakingPeriod + 1;
Expand All @@ -134,7 +147,8 @@
}

/**
* Calculates the fractional value of a numerator and denominator as a float with up to {@value #FRACTION_SCALE} decimal places.
* Calculates the fractional value of a numerator and denominator as a float with up to {@value #FRACTION_SCALE}
* decimal places.
*
* @param numerator the numerator of the fraction
* @param denominator the denominator of the fraction
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0

package org.hiero.mirror.restjava.mapper;

import static org.hiero.mirror.restjava.mapper.CommonMapper.QUALIFIER_TIMESTAMP;

import com.hederahashgraph.api.proto.java.ExchangeRateSet;
import org.hiero.mirror.rest.model.ExchangeRate;
import org.hiero.mirror.rest.model.NetworkExchangeRateSetResponse;
import org.hiero.mirror.restjava.dto.SystemFile;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(config = MapperConfiguration.class, uses = CommonMapper.class)
public interface ExchangeRateMapper {

@Mapping(source = "protobuf.currentRate", target = "currentRate")

Check notice on line 17 in rest-java/src/main/java/org/hiero/mirror/restjava/mapper/ExchangeRateMapper.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

rest-java/src/main/java/org/hiero/mirror/restjava/mapper/ExchangeRateMapper.java#L17

Implied modifier 'abstract' should be explicit.
@Mapping(source = "protobuf.nextRate", target = "nextRate")
@Mapping(source = "fileData.consensusTimestamp", target = "timestamp", qualifiedByName = QUALIFIER_TIMESTAMP)
NetworkExchangeRateSetResponse map(SystemFile<ExchangeRateSet> source);

@Mapping(source = "centEquiv", target = "centEquivalent")

Check notice on line 22 in rest-java/src/main/java/org/hiero/mirror/restjava/mapper/ExchangeRateMapper.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

rest-java/src/main/java/org/hiero/mirror/restjava/mapper/ExchangeRateMapper.java#L22

Implied modifier 'abstract' should be explicit.
@Mapping(source = "hbarEquiv", target = "hbarEquivalent")
ExchangeRate map(com.hederahashgraph.api.proto.java.ExchangeRate source);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: Apache-2.0

package org.hiero.mirror.restjava.parameter;

import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.hiero.mirror.common.util.DomainUtils;
import org.hiero.mirror.restjava.common.RangeOperator;
import org.hiero.mirror.restjava.common.RangeParameter;

public record TimestampParameter(RangeOperator operator, Long value) implements RangeParameter<Long> {

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

private static final String ERROR = "Invalid timestamp parameter";
private static final Pattern PATTERN =
Pattern.compile("^((eq|gt|gte|lt|lte|ne):)?(\\d{1,17})(\\.(\\d{1,9}))?$", Pattern.CASE_INSENSITIVE);

public static TimestampParameter valueOf(String param) {
if (StringUtils.isBlank(param)) {
return EMPTY;
}

var matcher = PATTERN.matcher(param);
if (!matcher.matches()) {
throw new IllegalArgumentException(ERROR);
}

final var operator = parseOperator(matcher.group(2));
final var timestamp = parseTimestamp(matcher.group(3), matcher.group(5));
return new TimestampParameter(operator, timestamp);
}

private static RangeOperator parseOperator(String name) {
if (StringUtils.isEmpty(name)) {
return RangeOperator.EQ;
}

final var operator = RangeOperator.of(name);

if (operator == RangeOperator.NE) {
throw new IllegalArgumentException(ERROR);
}

return operator;
}

private static long parseTimestamp(String secondsStr, String nanosStr) {
try {
final long seconds = Long.parseLong(secondsStr);
final long nanos = StringUtils.isNotEmpty(nanosStr) ? Long.parseLong(nanosStr) : 0L;
return DomainUtils.convertToNanos(seconds, nanos);
} catch (RuntimeException e) {

Check warning on line 53 in rest-java/src/main/java/org/hiero/mirror/restjava/parameter/TimestampParameter.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

rest-java/src/main/java/org/hiero/mirror/restjava/parameter/TimestampParameter.java#L53

Catching 'RuntimeException' is not allowed.

Check notice on line 53 in rest-java/src/main/java/org/hiero/mirror/restjava/parameter/TimestampParameter.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

rest-java/src/main/java/org/hiero/mirror/restjava/parameter/TimestampParameter.java#L53

Unused catch parameter 'e' should be unnamed.
throw new IllegalArgumentException(ERROR);
}
}
}
Loading
Loading