Skip to content

Commit

Permalink
Add topic custom fee fields to /topics/{id} endpoint (#10450)
Browse files Browse the repository at this point in the history
- Add topic custom fee fields to openapi spec
- Update TopicController to also get topic custom fee schedule
- Add mapper support for new fields

Signed-off-by: Xin Li <[email protected]>
  • Loading branch information
xin-hedera authored Feb 21, 2025
1 parent 7b3c84c commit 8cb44af
Show file tree
Hide file tree
Showing 19 changed files with 626 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
import com.hedera.services.stream.proto.ContractAction.ResultDataCase;
import com.hedera.services.stream.proto.ContractActionType;
import com.hederahashgraph.api.proto.java.AccountID;
import com.hederahashgraph.api.proto.java.FeeExemptKeyList;
import com.hederahashgraph.api.proto.java.FreezeType;
import com.hederahashgraph.api.proto.java.Key;
import com.hederahashgraph.api.proto.java.Key.KeyCase;
Expand Down Expand Up @@ -571,34 +572,6 @@ public DomainWrapper<FileData, FileData.FileDataBuilder> fileData() {
return new DomainWrapperImpl<>(builder, builder::build);
}

private FallbackFee fallbackFee() {
return FallbackFee.builder()
.amount(number())
.denominatingTokenId(entityId())
.build();
}

private FixedFee fixedFee() {
return FixedFee.builder()
.allCollectorsAreExempt(true)
.amount(number())
.collectorAccountId(entityId())
.denominatingTokenId(entityId())
.build();
}

private FractionalFee fractionalFee() {
return FractionalFee.builder()
.allCollectorsAreExempt(true)
.collectorAccountId(entityId())
.denominator(number())
.maximumAmount(number())
.minimumAmount(1L)
.numerator(number())
.netOfTransfers(true)
.build();
}

public DomainWrapper<LiveHash, LiveHash.LiveHashBuilder> liveHash() {
var builder = LiveHash.builder().consensusTimestamp(timestamp()).livehash(bytes(64));
return new DomainWrapperImpl<>(builder, builder::build);
Expand Down Expand Up @@ -800,16 +773,6 @@ public DomainWrapper<RecordFile, RecordFile.RecordFileBuilder> recordFile() {
return new DomainWrapperImpl<>(builder, builder::build);
}

private RoyaltyFee royaltyFee() {
return RoyaltyFee.builder()
.allCollectorsAreExempt(true)
.collectorAccountId(entityId())
.denominator(number())
.fallbackFee(fallbackFee())
.numerator(number())
.build();
}

public DomainWrapper<Schedule, Schedule.ScheduleBuilder> schedule() {
var builder = Schedule.builder()
.consensusTimestamp(timestamp())
Expand Down Expand Up @@ -1025,7 +988,7 @@ public DomainWrapper<TokenTransfer, TokenTransfer.TokenTransferBuilder> tokenTra
.adminKey(bytes(32))
.createdTimestamp(timestamp)
.id(id())
.feeExemptKeyList(bytes(32))
.feeExemptKeyList(feeExemptKeyList())
.feeScheduleKey(bytes(32))
.submitKey(bytes(32))
.timestampRange(Range.atLeast(timestamp));
Expand Down Expand Up @@ -1155,12 +1118,25 @@ public byte[] bytes(int length) {
return bytes;
}

public EntityId entityId() {
return EntityId.of(0L, 0L, id());
}

public byte[] evmAddress() {
return bytes(20);
}

public EntityId entityId() {
return EntityId.of(0L, 0L, id());
public FixedFee fixedFee() {
return FixedFee.builder()
.allCollectorsAreExempt(true)
.amount(number())
.collectorAccountId(entityId())
.denominatingTokenId(entityId())
.build();
}

public String hash(int characters) {
return RandomStringUtils.secure().next(characters, "0123456789abcdef");
}

/**
Expand All @@ -1173,44 +1149,12 @@ public long id() {
return id.incrementAndGet() + LAST_RESERVED_ID;
}

// SQL timestamp type only supports up to microsecond granularity
private Instant instant() {
return now.truncatedTo(ChronoUnit.MILLIS).plusMillis(number());
}

public byte[] key() {
return id.get() % 2 == 0 ? key(KeyCase.ECDSA_SECP256K1) : key(KeyCase.ED25519);
}

public byte[] key(KeyCase keyCase) {
var key =
switch (keyCase) {
case ECDSA_SECP256K1 -> Key.newBuilder().setECDSASecp256K1(generateSecp256k1Key());
case ED25519 -> Key.newBuilder().setEd25519(ByteString.copyFrom(bytes(KEY_LENGTH_ED25519)));
default -> throw new UnsupportedOperationException("Key type not supported");
};

return key.build().toByteArray();
}

@SneakyThrows
private ByteString generateSecp256k1Key() {
var keyPair = Keys.createEcKeyPair();
var publicKey = keyPair.getPublicKey();

// Convert BigInteger public key to a full 65-byte uncompressed key
var fullPublicKey = Numeric.hexStringToByteArray(Numeric.toHexStringWithPrefixZeroPadded(publicKey, 130));

// Convert to compressed format (33 bytes)
var prefix = (byte) (fullPublicKey[64] % 2 == 0 ? 0x02 : 0x03); // 0x02 for even Y, 0x03 for odd Y
var compressedKey = new byte[33];
compressedKey[0] = prefix;
System.arraycopy(fullPublicKey, 1, compressedKey, 1, 32); // Copy only X coordinate
return ByteString.copyFrom(compressedKey);
}

public long number() {
return id.incrementAndGet();
return protobufKey(keyCase).toByteArray();
}

public byte[] nonZeroBytes(int length) {
Expand All @@ -1223,12 +1167,28 @@ public byte[] nonZeroBytes(int length) {
return bytes;
}

public String text(int characters) {
return RandomStringUtils.secure().nextAlphanumeric(characters);
public long number() {
return id.incrementAndGet();
}

public String hash(int characters) {
return RandomStringUtils.secure().next(characters, "0123456789abcdef");
public Key protobufKey(KeyCase keyCase) {
return switch (keyCase) {
case ECDSA_SECP256K1 -> Key.newBuilder()
.setECDSASecp256K1(generateSecp256k1Key())
.build();
case ED25519 -> Key.newBuilder()
.setEd25519(ByteString.copyFrom(bytes(KEY_LENGTH_ED25519)))
.build();
default -> throw new UnsupportedOperationException("Key type not supported");
};
}

public Timestamp protoTimestamp() {
long timestamp = timestamp();
return Timestamp.newBuilder()
.setSeconds(timestamp / DomainUtils.NANOS_PER_SECOND)
.setNanos((int) (timestamp % DomainUtils.NANOS_PER_SECOND))
.build();
}

/**
Expand All @@ -1240,12 +1200,8 @@ public void resetTimestamp(long value) {
timestampOffset = value - timestampNoOffset();
}

public Timestamp protoTimestamp() {
long timestamp = timestamp();
return Timestamp.newBuilder()
.setSeconds(timestamp / DomainUtils.NANOS_PER_SECOND)
.setNanos((int) (timestamp % DomainUtils.NANOS_PER_SECOND))
.build();
public String text(int characters) {
return RandomStringUtils.secure().nextAlphanumeric(characters);
}

public long timestamp() {
Expand All @@ -1256,13 +1212,71 @@ private long tinybar() {
return number() * TINYBARS_IN_ONE_HBAR;
}

private FallbackFee fallbackFee() {
return FallbackFee.builder()
.amount(number())
.denominatingTokenId(entityId())
.build();
}

private byte[] feeExemptKeyList() {
return FeeExemptKeyList.newBuilder()
.addKeys(protobufKey(KeyCase.ECDSA_SECP256K1))
.addKeys(protobufKey(KeyCase.ED25519))
.build()
.toByteArray();
}

private FractionalFee fractionalFee() {
return FractionalFee.builder()
.allCollectorsAreExempt(true)
.collectorAccountId(entityId())
.denominator(number())
.maximumAmount(number())
.minimumAmount(1L)
.numerator(number())
.netOfTransfers(true)
.build();
}

@SneakyThrows
private ByteString generateSecp256k1Key() {
var keyPair = Keys.createEcKeyPair();
var publicKey = keyPair.getPublicKey();

// Convert BigInteger public key to a full 65-byte uncompressed key
var fullPublicKey = Numeric.hexStringToByteArray(Numeric.toHexStringWithPrefixZeroPadded(publicKey, 130));

// Convert to compressed format (33 bytes)
var prefix = (byte) (fullPublicKey[64] % 2 == 0 ? 0x02 : 0x03); // 0x02 for even Y, 0x03 for odd Y
var compressedKey = new byte[33];
compressedKey[0] = prefix;
System.arraycopy(fullPublicKey, 1, compressedKey, 1, 32); // Copy only X coordinate
return ByteString.copyFrom(compressedKey);
}

private long getEpochDay(long timestamp) {
return LocalDate.ofInstant(Instant.ofEpochSecond(0, timestamp), ZoneId.of("UTC"))
.atStartOfDay()
.toLocalDate()
.toEpochDay();
}

// SQL timestamp type only supports up to microsecond granularity
private Instant instant() {
return now.truncatedTo(ChronoUnit.MILLIS).plusMillis(number());
}

private RoyaltyFee royaltyFee() {
return RoyaltyFee.builder()
.allCollectorsAreExempt(true)
.collectorAccountId(entityId())
.denominator(number())
.fallbackFee(fallbackFee())
.numerator(number())
.build();
}

private long timestampNoOffset() {
return DomainUtils.convertToNanosMax(now.getEpochSecond(), now.getNano()) + number();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.hedera.mirror.rest.model.Topic;
import com.hedera.mirror.restjava.common.EntityIdNumParameter;
import com.hedera.mirror.restjava.mapper.TopicMapper;
import com.hedera.mirror.restjava.service.CustomFeeService;
import com.hedera.mirror.restjava.service.EntityService;
import com.hedera.mirror.restjava.service.TopicService;
import lombok.CustomLog;
Expand All @@ -34,6 +35,7 @@
@RestController
public class TopicController {

private final CustomFeeService customFeeService;
private final EntityService entityService;
private final TopicMapper topicMapper;
private final TopicService topicService;
Expand All @@ -42,6 +44,7 @@ public class TopicController {
Topic getTopic(@PathVariable EntityIdNumParameter id) {
var topic = topicService.findById(id.id());
var entity = entityService.findById(id.id());
return topicMapper.map(entity, topic);
var customFee = customFeeService.findById(id.id());
return topicMapper.map(customFee, entity, topic);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Hedera Hashgraph, LLC
* Copyright (C) 2025 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.
Expand All @@ -19,12 +19,12 @@
import java.io.Serial;

@SuppressWarnings("java:S110")
public class InvalidFilterException extends RestJavaException {
public class InvalidMappingException extends RestJavaException {

@Serial
private static final long serialVersionUID = 1518569037954950068L;
private static final long serialVersionUID = -857679581991526245L;

public InvalidFilterException(String message) {
super(message);
public InvalidMappingException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.util.CollectionUtils;

public interface CollectionMapper<S, T> {

T map(S source);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@
package com.hedera.mirror.restjava.mapper;

import com.google.common.collect.Range;
import com.google.protobuf.InvalidProtocolBufferException;
import com.hedera.mirror.common.domain.entity.EntityId;
import com.hedera.mirror.rest.model.Key;
import com.hedera.mirror.rest.model.Key.TypeEnum;
import com.hedera.mirror.rest.model.TimestampRange;
import com.hedera.mirror.restjava.exception.InvalidMappingException;
import com.hederahashgraph.api.proto.java.KeyList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.mapstruct.Mapper;
import org.mapstruct.MappingInheritanceStrategy;
Expand Down Expand Up @@ -70,6 +76,29 @@ default Key mapKey(byte[] source) {
return new Key().key(hex).type(TypeEnum.PROTOBUF_ENCODED);
}

default List<Key> mapKeyList(byte[] source) {
if (ArrayUtils.isEmpty(source)) {
return Collections.emptyList();
}

try {
var keyList = KeyList.parseFrom(source);
return keyList.getKeysList().stream()
.map(key -> mapKey(key.toByteArray()))
.toList();
} catch (InvalidProtocolBufferException e) {
throw new InvalidMappingException("Error parsing protobuf message", e);
}
}

default String mapLowerRange(Range<Long> source) {
if (source == null || !source.hasLowerBound()) {
return null;
}

return mapTimestamp(source.lowerEndpoint());
}

default TimestampRange mapRange(Range<Long> source) {
if (source == null) {
return null;
Expand Down
Loading

0 comments on commit 8cb44af

Please sign in to comment.