Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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) {

Check notice on line 48 in rest-java/src/main/java/org/hiero/mirror/restjava/controller/NetworkController.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

rest-java/src/main/java/org/hiero/mirror/restjava/controller/NetworkController.java#L48

Consider using varargs for methods or constructors which take an array the last parameter.
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 @@ public interface CommonMapper {
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";

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

@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 @@ default String mapTimestamp(long timestamp) {
.toString();
}

default Long mapTimestampSeconds(TimestampSeconds source) {
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 @@ default TimestampRange mapTimestampRange(long stakingPeriod) {
}

/**
* 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")
@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")
@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) {
throw new IllegalArgumentException(ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0

package org.hiero.mirror.restjava.repository;

import java.util.Optional;
import org.hiero.mirror.common.domain.file.FileData;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface FileDataRepository extends CrudRepository<FileData, Long> {

@Query(

Check notice on line 14 in rest-java/src/main/java/org/hiero/mirror/restjava/repository/FileDataRepository.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

rest-java/src/main/java/org/hiero/mirror/restjava/repository/FileDataRepository.java#L14

Implied modifier 'abstract' should be explicit.
nativeQuery = true,
value =
"""
select
min(consensus_timestamp) as consensus_timestamp,
?1 as entity_id,
string_agg(file_data, '' order by consensus_timestamp) as file_data,
null as transaction_type
from file_data
where entity_id = ?1
and consensus_timestamp >= (
select consensus_timestamp
from file_data
where entity_id = ?1
and consensus_timestamp >= ?2
and consensus_timestamp <= ?3
and (transaction_type = 17 or (transaction_type = 19 and length(file_data) <> 0))
order by consensus_timestamp desc
limit 1
) and consensus_timestamp >= ?2 and consensus_timestamp <= ?3
""")
Optional<FileData> getFileAtTimestamp(long fileId, long lowerTimestamp, long upperTimestamp);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: Apache-2.0

package org.hiero.mirror.restjava.service;

import com.hederahashgraph.api.proto.java.ExchangeRateSet;
import org.hiero.mirror.restjava.dto.SystemFile;
import org.jspecify.annotations.NullMarked;

@NullMarked
public interface FileService {

SystemFile<ExchangeRateSet> getExchangeRate(Bound timestamp);

Check notice on line 12 in rest-java/src/main/java/org/hiero/mirror/restjava/service/FileService.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

rest-java/src/main/java/org/hiero/mirror/restjava/service/FileService.java#L12

Implied modifier 'abstract' should be explicit.
}
Loading
Loading