From fef724c37d61e1b1425cb16a6b7bde902c46ca92 Mon Sep 17 00:00:00 2001 From: Thomas Kammerlocher Date: Mon, 11 Mar 2024 09:46:11 +0100 Subject: [PATCH] Feat/implement construction metadata (#82) * implemented /metadata * implemented construction metadata * chore: added postman tests * adjusted readme * chore: fixed NetworkEnum * chore: added CardanoConfigService to get ProtocolParams from configfile * chore: added CardanoConfigService to get ProtocolParams from configfile * chore: fixed names of config file --- .github/workflows/integration-test.yaml | 2 + README.md | 6 +- .../rosetta/api/mapper/DataMapper.java | 15 ++ .../api/model/entity/EpochParamEntity.java | 43 ++++ .../api/model/entity/ProtocolParams.java | 233 ++++++++++++++++++ .../api/model/enumeration/NetworkEnum.java | 10 +- .../api/model/rest/ProtocolParameters.java | 95 +++++++ .../api/repository/EpochParamRepository.java | 13 + .../rosetta/api/service/BlockService.java | 1 + .../api/service/CardanoConfigService.java | 9 + .../rosetta/api/service/CardanoService.java | 6 + .../service/LedgerDataProviderService.java | 50 +--- .../PostgresLedgerDataProviderService.java | 35 ++- .../impl/CardanoConfigServiceImpl.java | 50 ++++ .../api/service/impl/CardanoServiceImpl.java | 30 +++ .../impl/ConstructionApiServiceImpl.java | 27 +- .../Rosetta-java-env.postman_environment.json | 16 +- .../rosetta-java.postman_collection.json | 50 +++- yaci-indexer/pom.xml | 5 + 19 files changed, 618 insertions(+), 78 deletions(-) create mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/model/entity/EpochParamEntity.java create mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/model/entity/ProtocolParams.java create mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/model/rest/ProtocolParameters.java create mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/repository/EpochParamRepository.java create mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/service/CardanoConfigService.java create mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/CardanoConfigServiceImpl.java diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml index 3479a2923..31b92f0ca 100644 --- a/.github/workflows/integration-test.yaml +++ b/.github/workflows/integration-test.yaml @@ -13,6 +13,8 @@ jobs: - uses: actions/checkout@v3 - name: "Set up environment" run: docker compose --env-file .env.IntegrationTest -f docker-integration-test-environment.yaml up --build -d --wait + - name: "Wait for node to be populated" + run: "sleep 30s" - name: "Install Node" uses: actions/setup-node@v1 with: diff --git a/README.md b/README.md index 590a8c3f3..41825f22c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ to fetch the data from the node. - Construction API - [x] /construction/derive - [ ] /construction/preprocess - - [ ] /construction/metadata + - [x] /construction/metadata - [ ] /construction/payloads - [ ] /construction/combine - [ ] /construction/parse @@ -52,10 +52,10 @@ to fetch the data from the node. ### How to run integration tests -- Run `docker compose --env-file .env.IntegrationTest -f docker-integration-test-environment.yaml up --build -d` +- Run `docker compose --env-file .env.IntegrationTest -f docker-integration-test-environment.yaml up --build -d --wait` - Using CLI - Install newman `npm install -g newman` (Node version 14+ needed) - - Run `newman run ./postmanTests/rosetta-java.postman_collection.json -e ./postmanTests/Rosetta-java-env.postman_environment -r cli` + - Run `newman run ./postmanTests/rosetta-java.postman_collection.json -e ./postmanTests/Rosetta-java-env.postman_environment.json -r cli` - Using Postman - Install [Postman](https://www.postman.com) - Import the collection `./postmanTests/rosetta-java.postman_collection.json` diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/mapper/DataMapper.java b/api/src/main/java/org/cardanofoundation/rosetta/api/mapper/DataMapper.java index a120bb5ce..8821ec98b 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/mapper/DataMapper.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/mapper/DataMapper.java @@ -7,6 +7,7 @@ import org.cardanofoundation.rosetta.api.model.constants.Constants; import org.cardanofoundation.rosetta.api.model.dto.*; import org.cardanofoundation.rosetta.api.model.dto.TransactionDto; +import org.cardanofoundation.rosetta.api.model.entity.ProtocolParams; import org.cardanofoundation.rosetta.api.model.enumeration.NetworkEnum; import org.cardanofoundation.rosetta.api.model.rest.TransactionMetadata; import org.cardanofoundation.rosetta.api.model.rosetta.BlockMetadata; @@ -232,6 +233,20 @@ public static AccountCoinsResponse mapToAccountCoinsResponse(BlockDto block, Lis .build()).build()).build()).toList()).build(); } + + public static ConstructionMetadataResponse mapToMetadataResponse(ProtocolParams protocolParams, Long ttl, Long suggestedFee) { + return ConstructionMetadataResponse.builder() + .metadata(Map.of("protocol_parameters", protocolParams, "ttl", ttl)) + .suggestedFee(List.of(Amount.builder() + .value(suggestedFee.toString()) + .currency(Currency.builder() + .decimals(ADA_DECIMALS) + .symbol(ADA) + .build()) + .build())) + .build(); + } + } diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/model/entity/EpochParamEntity.java b/api/src/main/java/org/cardanofoundation/rosetta/api/model/entity/EpochParamEntity.java new file mode 100644 index 000000000..20c9bceb3 --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/model/entity/EpochParamEntity.java @@ -0,0 +1,43 @@ +package org.cardanofoundation.rosetta.api.model.entity; + +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.annotations.Type; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +@Entity +@Table(name = "epoch_param") +@Slf4j +public class EpochParamEntity extends BlockAwareEntity { + @Id + @Column(name = "epoch") + private Integer epoch; + + @Type(JsonType.class) + @Column(name = "params", columnDefinition = "json") + private ProtocolParams params; + + @Column(name = "cost_model_hash") + private String costModelHash; + + @Column(name = "slot") + private Long slot; + + @PrePersist + public void preSave() { + if (this.getParams() == null) + return; + + //reset these fields + if (this.getParams().getCostModels() != null) + this.getParams().setCostModels(null); + } +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/model/entity/ProtocolParams.java b/api/src/main/java/org/cardanofoundation/rosetta/api/model/entity/ProtocolParams.java new file mode 100644 index 000000000..94975c28a --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/model/entity/ProtocolParams.java @@ -0,0 +1,233 @@ +package org.cardanofoundation.rosetta.api.model.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.json.JSONObject; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ProtocolParams { + private Integer minFeeA; //0 + private Integer minFeeB; //1 + private Integer maxBlockSize; //2 + private Integer maxTxSize; //3 + private Integer maxBlockHeaderSize; //4 + private BigInteger keyDeposit; //5 + private BigInteger poolDeposit; //6 + private Integer maxEpoch; //7 + @JsonProperty("nopt") + private Integer nOpt; //8 + private BigDecimal poolPledgeInfluence; //rational //9 + private BigDecimal expansionRate; //unit interval //10 + private BigDecimal treasuryGrowthRate; //11 + private BigDecimal decentralisationParam; //12 + private String extraEntropy; //13 + private Integer protocolMajorVer; //14 + private Integer protocolMinorVer; //14 + private BigInteger minUtxo; //TODO //15 + + private BigInteger minPoolCost; //16 + private BigInteger adaPerUtxoByte; //17 + //private String nonce; + + //Alonzo changes + private Map costModels; //18 + private String costModelsHash; + + //ex_unit_prices + private BigDecimal priceMem; //19 + private BigDecimal priceStep; //19 + + //max tx ex units + private BigInteger maxTxExMem; //20 + private BigInteger maxTxExSteps; //20 + + //max block ex units + private BigInteger maxBlockExMem; //21 + private BigInteger maxBlockExSteps; //21 + + private Long maxValSize; //22 + + private Integer collateralPercent; //23 + private Integer maxCollateralInputs; //24 + +// //Cost per UTxO word for Alonzo. +// //Cost per UTxO byte for Babbage and later. +// private String coinsPerUtxoSize; +// @Deprecated +// private String coinsPerUtxoWord; + + //Conway era fields +// private PoolVotingThresholds poolVotingThresholds; //25 +// private DrepVoteThresholds drepVotingThresholds; //26 + private Integer committeeMinSize; //27 + private Integer committeeMaxTermLength; //28 + private Integer govActionLifetime; //29 + private BigInteger govActionDeposit; //30 + private BigInteger drepDeposit; //31 + private Integer drepActivity; //32 + + // TODO clarify if parameters are correctly set + public static ProtocolParams fromJSONObject(JSONObject shelleyJsonObject) { + ProtocolParams p = new ProtocolParams(); + JSONObject shelleyProtocolParams = shelleyJsonObject.getJSONObject("protocolParams"); + p.setMinFeeA(shelleyProtocolParams.getInt("minFeeA")); + p.setMinFeeB(shelleyProtocolParams.getInt("minFeeB")); + p.setMaxBlockSize(shelleyProtocolParams.getInt("maxBlockBodySize")); + p.setMaxTxSize(shelleyProtocolParams.getInt("maxTxSize")); + p.setMaxBlockHeaderSize(shelleyProtocolParams.getInt("maxBlockHeaderSize")); + p.setKeyDeposit(shelleyProtocolParams.getBigInteger("keyDeposit")); + p.setPoolDeposit(shelleyProtocolParams.getBigInteger("poolDeposit")); + p.setNOpt(shelleyProtocolParams.getInt("nOpt")); + p.setDecentralisationParam(shelleyProtocolParams.getBigDecimal("decentralisationParam")); + p.setExtraEntropy(shelleyProtocolParams.getJSONObject("extraEntropy").getString("tag")); + JSONObject protolVersion = shelleyProtocolParams.getJSONObject("protocolVersion"); + p.setProtocolMajorVer(protolVersion.getInt("major")); + p.setProtocolMinorVer(protolVersion.getInt("minor")); + p.setMinUtxo(shelleyProtocolParams.getBigInteger("minUTxOValue")); + p.setMinPoolCost(shelleyProtocolParams.getBigInteger("minPoolCost")); + p.setAdaPerUtxoByte(shelleyProtocolParams.getBigInteger("minFeeA")); + + return p; + } + + public void merge(ProtocolParams other) { + if (this.minFeeA == null) { + this.minFeeA = other.minFeeA; + } + if (this.minFeeB == null) { + this.minFeeB = other.minFeeB; + } + if (this.maxBlockSize == null) { + this.maxBlockSize = other.maxBlockSize; + } + if (this.maxTxSize == null) { + this.maxTxSize = other.maxTxSize; + } + if (this.maxBlockHeaderSize == null) { + this.maxBlockHeaderSize = other.maxBlockHeaderSize; + } + if (this.keyDeposit == null) { + this.keyDeposit = other.keyDeposit; + } + if (this.poolDeposit == null) { + this.poolDeposit = other.poolDeposit; + } + if (this.maxEpoch == null) { + this.maxEpoch = other.maxEpoch; + } + if (this.nOpt == null) { + this.nOpt = other.nOpt; + } + if (this.poolPledgeInfluence == null) { + this.poolPledgeInfluence = other.poolPledgeInfluence; + } + if (this.expansionRate == null) { + this.expansionRate = other.expansionRate; + } + if (this.treasuryGrowthRate == null) { + this.treasuryGrowthRate = other.treasuryGrowthRate; + } + if (this.decentralisationParam == null) { + this.decentralisationParam = other.decentralisationParam; + } + if (this.extraEntropy == null) { + this.extraEntropy = other.extraEntropy; + } + if (this.protocolMajorVer == null) { + this.protocolMajorVer = other.protocolMajorVer; + } + if (this.protocolMinorVer == null) { + this.protocolMinorVer = other.protocolMinorVer; + } + if (this.minUtxo == null) { + this.minUtxo = other.minUtxo; + } + if (this.minPoolCost == null) { + this.minPoolCost = other.minPoolCost; + } + if (this.adaPerUtxoByte == null) { + this.adaPerUtxoByte = other.adaPerUtxoByte; + } + if (this.costModels == null) { + if (this.costModels == null) { + this.costModels = other.getCostModels(); + } else { + var keys = other.getCostModels().keySet(); + keys.forEach(key -> this.costModels.put(key, other.costModels.get(key))); + } + } + + if (this.costModelsHash == null) { + this.costModelsHash = other.costModelsHash; + } + + if (this.priceMem == null) { + this.priceMem = other.priceMem; + } + if (this.priceStep == null) { + this.priceStep = other.priceStep; + } + if (this.maxTxExMem == null) { + this.maxTxExMem = other.maxTxExMem; + } + if (this.maxTxExSteps == null) { + this.maxTxExSteps = other.maxTxExSteps; + } + if (this.maxBlockExMem == null) { + this.maxBlockExMem = other.maxBlockExMem; + } + if (this.maxBlockExSteps == null) { + this.maxBlockExSteps = other.maxBlockExSteps; + } + if (this.maxValSize == null) { + this.maxValSize = other.maxValSize; + } + if (this.collateralPercent == null) { + this.collateralPercent = other.collateralPercent; + } + if (this.maxCollateralInputs == null) { + this.maxCollateralInputs = other.maxCollateralInputs; + } +// if (other.poolVotingThresholds == null) { +// this.poolVotingThresholds = other.poolVotingThresholds; +// } +// if (other.drepVotingThresholds == null) { +// this.drepVotingThresholds = other.drepVotingThresholds; +// } + if (this.committeeMinSize == null) { + this.committeeMinSize = other.committeeMinSize; + } + if (this.committeeMaxTermLength == null) { + this.committeeMaxTermLength = other.committeeMaxTermLength; + } + if (this.govActionLifetime == null) { + this.govActionLifetime = other.govActionLifetime; + } + if (this.govActionDeposit == null) { + this.govActionDeposit = other.govActionDeposit; + } + if (this.drepDeposit == null) { + this.drepDeposit = other.drepDeposit; + } + if (this.drepActivity == null) { + this.drepActivity = other.drepActivity; + } + } +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/model/enumeration/NetworkEnum.java b/api/src/main/java/org/cardanofoundation/rosetta/api/model/enumeration/NetworkEnum.java index 1fe95de2b..0766ff71a 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/model/enumeration/NetworkEnum.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/model/enumeration/NetworkEnum.java @@ -2,11 +2,13 @@ import com.bloxbean.cardano.client.common.model.Network; import com.bloxbean.cardano.client.common.model.Networks; +import lombok.Getter; +@Getter public enum NetworkEnum { MAINNET("mainnet", Networks.mainnet()), - PREPROD("preprod", Networks.testnet()), + PREPROD("preprod", Networks.preprod()), TESTNET("testnet", Networks.testnet()), DEVNET("devnet", new Network(0b0000, 42)); @@ -18,11 +20,7 @@ public enum NetworkEnum { this.network = network; } - public String getValue() { - return value; - } - - final public Network getNetwork() { + public final Network getNetwork() { return network; } diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/model/rest/ProtocolParameters.java b/api/src/main/java/org/cardanofoundation/rosetta/api/model/rest/ProtocolParameters.java new file mode 100644 index 000000000..b629b247e --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/model/rest/ProtocolParameters.java @@ -0,0 +1,95 @@ +package org.cardanofoundation.rosetta.api.model.rest; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; +import org.cardanofoundation.rosetta.api.model.entity.ProtocolParams; + +import java.math.BigInteger; + +/** + * ProtocolParameters + */ + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@Builder + public class ProtocolParameters { + + @JsonProperty("coinsPerUtxoSize") + private String coinsPerUtxoSize; + + @JsonProperty("maxTxSize") + private Integer maxTxSize; + + @JsonProperty("maxValSize") + private BigInteger maxValSize; + + @JsonProperty("keyDeposit") + private String keyDeposit; + + @JsonProperty("maxCollateralInputs") + private Integer maxCollateralInputs; + + @JsonProperty("minFeeCoefficient") + private Integer minFeeCoefficient; + + @JsonProperty("minFeeConstant") + private Integer minFeeConstant; + + @JsonProperty("minPoolCost") + private String minPoolCost; + + @JsonProperty("poolDeposit") + private String poolDeposit; + + @JsonProperty("protocol") + private Integer protocol; + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ProtocolParameters {\n"); + sb.append(" coinsPerUtxoSize: ").append(toIndentedString(coinsPerUtxoSize)).append("\n"); + sb.append(" maxTxSize: ").append(toIndentedString(maxTxSize)).append("\n"); + sb.append(" maxValSize: ").append(toIndentedString(maxValSize)).append("\n"); + sb.append(" keyDeposit: ").append(toIndentedString(keyDeposit)).append("\n"); + sb.append(" maxCollateralInputs: ").append(toIndentedString(maxCollateralInputs)).append("\n"); + sb.append(" minFeeCoefficient: ").append(toIndentedString(minFeeCoefficient)).append("\n"); + sb.append(" minFeeConstant: ").append(toIndentedString(minFeeConstant)).append("\n"); + sb.append(" minPoolCost: ").append(toIndentedString(minPoolCost)).append("\n"); + sb.append(" poolDeposit: ").append(toIndentedString(poolDeposit)).append("\n"); + sb.append(" protocol: ").append(toIndentedString(protocol)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + public ProtocolParameters fromEntity(ProtocolParams entity) { + return ProtocolParameters.builder() + .coinsPerUtxoSize(entity.getAdaPerUtxoByte().toString()) // TODO check if it's the right value + .maxTxSize(entity.getMaxTxSize()) + .maxValSize(BigInteger.valueOf(entity.getMaxValSize())) + .keyDeposit(entity.getKeyDeposit().toString()) + .maxCollateralInputs(entity.getMaxCollateralInputs()) + .minFeeCoefficient(entity.getMinFeeA()) + .minFeeConstant(entity.getMinFeeB()) + .minPoolCost(entity.getMinPoolCost().toString()) + .poolDeposit(entity.getPoolDeposit().toString()) + .protocol(entity.getProtocolMajorVer()) + .build(); + } +} + diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/repository/EpochParamRepository.java b/api/src/main/java/org/cardanofoundation/rosetta/api/repository/EpochParamRepository.java new file mode 100644 index 000000000..5f000ff3c --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/repository/EpochParamRepository.java @@ -0,0 +1,13 @@ +package org.cardanofoundation.rosetta.api.repository; + +import com.bloxbean.cardano.yaci.core.model.Epoch; +import org.cardanofoundation.rosetta.api.model.entity.EpochParamEntity; +import org.cardanofoundation.rosetta.api.model.entity.ProtocolParams; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface EpochParamRepository extends JpaRepository { + + @Query("SELECT e.params FROM EpochParamEntity e ORDER BY e.epoch DESC LIMIT 1") + ProtocolParams findLatestProtocolParams(); +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/service/BlockService.java b/api/src/main/java/org/cardanofoundation/rosetta/api/service/BlockService.java index 1ca4a1118..3c0fa4682 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/service/BlockService.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/service/BlockService.java @@ -22,4 +22,5 @@ AccountBalanceResponse findBalanceDataByAddressAndBlock(String address, List findTransactionsByBlock(BlockDto block); BlockTransactionResponse getBlockTransaction(BlockTransactionRequest blockTransactionRequest); + } diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/service/CardanoConfigService.java b/api/src/main/java/org/cardanofoundation/rosetta/api/service/CardanoConfigService.java new file mode 100644 index 000000000..17d6c8ff1 --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/service/CardanoConfigService.java @@ -0,0 +1,9 @@ +package org.cardanofoundation.rosetta.api.service; + +import org.cardanofoundation.rosetta.api.model.entity.ProtocolParams; + +import java.io.FileNotFoundException; + +public interface CardanoConfigService { + ProtocolParams getProtocolParameters() throws FileNotFoundException; +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/service/CardanoService.java b/api/src/main/java/org/cardanofoundation/rosetta/api/service/CardanoService.java index 61cc621ae..71ac5d418 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/service/CardanoService.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/service/CardanoService.java @@ -1,9 +1,15 @@ package org.cardanofoundation.rosetta.api.service; +import co.nstant.in.cbor.CborException; import co.nstant.in.cbor.model.Array; +import com.bloxbean.cardano.client.exception.CborSerializationException; +import org.cardanofoundation.rosetta.api.model.entity.ProtocolParams; public interface CardanoService { String getHashOfSignedTransaction(String signedTransaction); Array decodeExtraData(String encoded); + Long calculateTtl(Long ttlOffset); + Long updateTxSize(Long previousTxSize, Long previousTtl, Long updatedTtl) throws CborSerializationException, CborException; + Long calculateTxMinimumFee(Long transactionSize, ProtocolParams protocolParameters); } diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/service/LedgerDataProviderService.java b/api/src/main/java/org/cardanofoundation/rosetta/api/service/LedgerDataProviderService.java index 31741e4a5..d50043a24 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/service/LedgerDataProviderService.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/service/LedgerDataProviderService.java @@ -5,7 +5,7 @@ import java.util.List; -import org.cardanofoundation.rosetta.api.model.entity.Amt; +import org.cardanofoundation.rosetta.api.model.entity.ProtocolParams; import org.openapitools.client.model.Currency; /** @@ -24,52 +24,14 @@ public interface LedgerDataProviderService { Long findLatestBlockNumber(); -// ProtocolParameters findProtocolParameters(); - -// List findMaBalanceByAddressAndBlock(String address, String hash); + ProtocolParams findProtocolParametersFromIndexer(); BlockDto findLatestBlock(); List findTransactionsByBlock(Long number, String hash); -// List fillTransaction(List transactions); -// -// List populateTransactions( -// Map transactionsMap); -// -// -// PopulatedTransaction findTransactionByHashAndBlock(String transactionHash, Long blockNumber, String blockHash); -// -// List getFindTransactionsInputs(List transactionsHashes); -// -// List getFindPoolRetirements(List transactionsHashes); -// -// List getFindTransactionPoolRelays( -// List transactionsHashes); -// -// List getFindTransactionPoolOwners( -// List transactionsHashes); -// -// List getTransactionPoolRegistrationsData( -// List transactionsHashes); -// -// List getFindTransactionPoolRegistrationsData( -// List transactionsHashes); -// -// List getTransactionMetadataDtos(List transactionsHashes); -// -// List getFindTransactionDelegations( -// List transactionsHashes); -// -// List getFindTransactionDeregistrations( -// List transactionsHashes); -// -// List getFindTransactionRegistrations( -// List transactionsHashes); -// -// List getFindTransactionWithdrawals( -// List transactionsHashes); -// -// List getFindTransactionsOutputs( -// List transactionsHashes); + ProtocolParams findProtolParametersFromConfig(); + + ProtocolParams findProtocolParametersFromIndexerAndConfig(); + } diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/service/PostgresLedgerDataProviderService.java b/api/src/main/java/org/cardanofoundation/rosetta/api/service/PostgresLedgerDataProviderService.java index 9669438d4..540d4bf23 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/service/PostgresLedgerDataProviderService.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/service/PostgresLedgerDataProviderService.java @@ -1,10 +1,8 @@ package org.cardanofoundation.rosetta.api.service; +import java.io.FileNotFoundException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.TimeZone; +import java.util.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,6 +29,9 @@ public class PostgresLedgerDataProviderService implements LedgerDataProviderServ private final PoolRegistrationRepository poolRegistrationRepository; private final PoolRetirementRepository poolRetirementRepository; private final StakeAddressRepository stakeAddressRepository; + private final EpochParamRepository epochParamRepository; + + private final CardanoConfigService cardanoConfigService; @Override public GenesisBlockDto findGenesisBlock() { @@ -155,7 +156,7 @@ public List findTransactionsByBlock(Long blockNumber, String blo log.debug( "[findTransactionsByBlock] No block found for blockNumber: {} blockHash: {}", blockNumber, blockHash); - return null; + return Collections.emptyList(); } List txList = txRepository.findTransactionsByBlockHash(byNumberAndHash.getFirst().getHash()); log.debug( @@ -163,6 +164,28 @@ public List findTransactionsByBlock(Long blockNumber, String blo if (ObjectUtils.isNotEmpty(txList)) { return txList.stream().map(TransactionDto::fromTx).toList(); } - return null; + return Collections.emptyList(); + } + + @Override + public ProtocolParams findProtocolParametersFromIndexer() { + return epochParamRepository.findLatestProtocolParams(); + } + + @Override + public ProtocolParams findProtolParametersFromConfig() { + try { + return cardanoConfigService.getProtocolParameters(); + } catch (FileNotFoundException e) { + log.error("[findProtolParametersFromConfig] Protocol parameters not found"); + return null; + } + } + + @Override + public ProtocolParams findProtocolParametersFromIndexerAndConfig() { + ProtocolParams protocolParams = findProtocolParametersFromIndexer(); + protocolParams.merge(findProtolParametersFromConfig()); + return protocolParams; } } diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/CardanoConfigServiceImpl.java b/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/CardanoConfigServiceImpl.java new file mode 100644 index 000000000..8ddd25d9c --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/CardanoConfigServiceImpl.java @@ -0,0 +1,50 @@ +package org.cardanofoundation.rosetta.api.service.impl; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.cardanofoundation.rosetta.api.exception.ExceptionFactory; +import org.cardanofoundation.rosetta.api.exception.ServerException; +import org.cardanofoundation.rosetta.api.model.entity.ProtocolParams; +import org.cardanofoundation.rosetta.api.service.CardanoConfigService; +import org.cardanofoundation.rosetta.api.util.FileUtils; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +@Service +@Slf4j +@RequiredArgsConstructor +public class CardanoConfigServiceImpl implements CardanoConfigService { + + @Value("${cardano.rosetta.GENESIS_SHELLEY_PATH}") + private String genesisShelleyPath; + + @PostConstruct + public void filePathExistingValidator() throws ServerException { + validator(genesisShelleyPath); + } + + private void validator( String path) throws ServerException { + if(!new File(path).exists()) { + throw ExceptionFactory.configNotFoundException(); + } + } + + @Override + public ProtocolParams getProtocolParameters() throws FileNotFoundException { + String shelleyContent = null; + try { + shelleyContent = FileUtils.fileReader(genesisShelleyPath); + } catch (IOException e) { + throw new FileNotFoundException("Genesis shelley file not found: " + genesisShelleyPath); + } + JSONObject shelleyJsonObject = new JSONObject(shelleyContent); + return ProtocolParams.fromJSONObject(shelleyJsonObject); + } + +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/CardanoServiceImpl.java b/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/CardanoServiceImpl.java index ced0a7248..30251fe12 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/CardanoServiceImpl.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/CardanoServiceImpl.java @@ -1,15 +1,21 @@ package org.cardanofoundation.rosetta.api.service.impl; +import co.nstant.in.cbor.CborException; import co.nstant.in.cbor.model.Array; import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.UnsignedInteger; import com.bloxbean.cardano.client.common.cbor.CborSerializationUtil; import com.bloxbean.cardano.client.crypto.Blake2bUtil; import com.bloxbean.cardano.client.transaction.spec.Transaction; import com.bloxbean.cardano.client.transaction.spec.TransactionBody; import com.bloxbean.cardano.client.util.HexUtil; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.cardanofoundation.rosetta.api.exception.ExceptionFactory; +import org.cardanofoundation.rosetta.api.model.dto.BlockDto; +import org.cardanofoundation.rosetta.api.model.entity.ProtocolParams; import org.cardanofoundation.rosetta.api.service.CardanoService; +import org.cardanofoundation.rosetta.api.service.LedgerDataProviderService; import org.cardanofoundation.rosetta.api.util.CardanoAddressUtils; import org.springframework.stereotype.Service; @@ -17,8 +23,11 @@ @Slf4j @Service +@RequiredArgsConstructor public class CardanoServiceImpl implements CardanoService { + private final LedgerDataProviderService ledgerDataProviderService; + @Override public String getHashOfSignedTransaction(String signedTransaction) { try { @@ -58,4 +67,25 @@ public Array decodeExtraData(String encoded) { throw ExceptionFactory.cantBuildSignedTransaction(); } } + + @Override + public Long calculateTtl(Long ttlOffset) { + BlockDto latestBlock = ledgerDataProviderService.findLatestBlock(); + return latestBlock.getSlotNo() + ttlOffset; + } + + @Override + public Long updateTxSize(Long previousTxSize, Long previousTtl, Long updatedTtl) + throws CborException { + return previousTxSize + com.bloxbean.cardano.client.common.cbor.CborSerializationUtil.serialize( + new UnsignedInteger(updatedTtl)).length - + com.bloxbean.cardano.client.common.cbor.CborSerializationUtil.serialize( + new UnsignedInteger(previousTtl)).length; + } + + @Override + public Long calculateTxMinimumFee(Long transactionSize, ProtocolParams protocolParameters) { + return protocolParameters.getMinFeeA() * transactionSize + + protocolParameters.getMinFeeB(); + } } diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/ConstructionApiServiceImpl.java b/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/ConstructionApiServiceImpl.java index 0d36208a2..5e5a5cd4c 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/ConstructionApiServiceImpl.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/service/impl/ConstructionApiServiceImpl.java @@ -4,11 +4,11 @@ import co.nstant.in.cbor.model.Array; import co.nstant.in.cbor.model.UnicodeString; import com.bloxbean.cardano.client.exception.AddressExcepion; -import com.bloxbean.cardano.client.exception.CborDeserializationException; import com.bloxbean.cardano.client.exception.CborSerializationException; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.cardanofoundation.rosetta.api.mapper.DataMapper; +import org.cardanofoundation.rosetta.api.model.entity.ProtocolParams; import org.cardanofoundation.rosetta.api.model.enumeration.AddressType; import org.cardanofoundation.rosetta.api.model.cardano.ConstructionDeriveRequestMetadata; @@ -16,11 +16,13 @@ import org.cardanofoundation.rosetta.api.service.CardanoAddressService; import org.cardanofoundation.rosetta.api.service.CardanoService; import org.cardanofoundation.rosetta.api.service.ConstructionApiService; +import org.cardanofoundation.rosetta.api.service.LedgerDataProviderService; import org.openapitools.client.model.*; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.HashMap; +import java.util.Map; @Service @Slf4j @@ -29,6 +31,8 @@ public class ConstructionApiServiceImpl implements ConstructionApiService { private final CardanoAddressService cardanoAddressService; private final CardanoService cardanoService; + private final LedgerDataProviderService ledgerService; + private final DataMapper dataMapper; @Override public ConstructionDeriveResponse constructionDeriveService(ConstructionDeriveRequest constructionDeriveRequest) throws IllegalAccessException, CborSerializationException { @@ -50,7 +54,7 @@ public ConstructionDeriveResponse constructionDeriveService(ConstructionDeriveRe stakingCredential = metadata.getStakingCredential(); } String address = cardanoAddressService.getCardanoAddress(addressType, stakingCredential, publicKey, network); - return new ConstructionDeriveResponse(address, null, null); + return new ConstructionDeriveResponse(null, new AccountIdentifier(address, null, null), null); } @Override @@ -60,7 +64,22 @@ public ConstructionPreprocessResponse constructionPreprocessService(Construction @Override public ConstructionMetadataResponse constructionMetadataService(ConstructionMetadataRequest constructionMetadataRequest) throws CborException, CborSerializationException { - return null; + Map options = (Map) constructionMetadataRequest.getOptions(); + Double relativeTtl = (Double) options.get("relative_ttl"); + Double txSize = (Double) options.get("transaction_size"); + log.debug("[constructionMetadata] Calculating ttl based on {} relative ttl", relativeTtl); + Long ttl = cardanoService.calculateTtl(relativeTtl.longValue()); + log.debug("[constructionMetadata] ttl is {}", ttl); + log.debug("[constructionMetadata] updating tx size from {}", txSize); + Long updatedTxSize = cardanoService.updateTxSize(txSize.longValue(), 0L, ttl); + log.debug("[constructionMetadata] updated txSize size is ${updatedTxSize}"); + ProtocolParams protocolParams = ledgerService.findProtocolParametersFromIndexerAndConfig(); + log.debug("[constructionMetadata] received protocol parameters from block-service {}", + protocolParams); + Long suggestedFee = cardanoService.calculateTxMinimumFee(updatedTxSize, + protocolParams); + log.debug("[constructionMetadata] suggested fee is ${suggestedFee}"); + return DataMapper.mapToMetadataResponse(protocolParams, ttl, suggestedFee); } @Override diff --git a/postmanTests/Rosetta-java-env.postman_environment.json b/postmanTests/Rosetta-java-env.postman_environment.json index d041bca1c..811b7354e 100644 --- a/postmanTests/Rosetta-java-env.postman_environment.json +++ b/postmanTests/Rosetta-java-env.postman_environment.json @@ -96,9 +96,21 @@ "value": "333a6ccaaa639f7b451ce93764f54f654ef499fdb7b8b24374ee9d99eab9d795", "type": "default", "enabled": true + }, + { + "key": "transactionSize", + "value": "40.0", + "type": "default", + "enabled": true + }, + { + "key": "suggestedFee", + "value": "157317", + "type": "default", + "enabled": true } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2024-03-07T11:54:40.373Z", - "_postman_exported_using": "Postman/10.23.10" + "_postman_exported_at": "2024-03-08T13:49:25.404Z", + "_postman_exported_using": "Postman/10.23.11" } \ No newline at end of file diff --git a/postmanTests/rosetta-java.postman_collection.json b/postmanTests/rosetta-java.postman_collection.json index 9a255cd90..6839a6383 100644 --- a/postmanTests/rosetta-java.postman_collection.json +++ b/postmanTests/rosetta-java.postman_collection.json @@ -16,7 +16,7 @@ "pm.test(\"Test: Ok\", function () {", " pm.response.to.have.status(200);", "});", - "", + "console.log(pm.response.json())", "pm.test(\"Contains Json Body\", function() {", " pm.response.to.have.jsonBody('network_identifiers')", " .and.to.have.jsonBody('network_identifiers[0].blockchain')", @@ -76,7 +76,8 @@ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});", - " ", + "console.log(pm.response.json())", + "", "var jsonData = pm.response.json();", "postman.setEnvironmentVariable(\"blockIndex\", jsonData.current_block_identifier.index)", "postman.setEnvironmentVariable(\"blockHash\", jsonData.current_block_identifier.hash)", @@ -126,6 +127,7 @@ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", " });", + "console.log(pm.response.json())", "", "pm.test(\"Contains Json Body\", function() {", " pm.response.to.have.jsonBody('version.rosetta_version')", @@ -176,6 +178,7 @@ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});", + "console.log(pm.response.json())", "", "pm.test(\"Contains Json Body\", function() {", " pm.response.to.have.jsonBody('block.block_identifier.index')", @@ -229,7 +232,8 @@ "exec": [ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", - "});" + "});", + "console.log(pm.response.json())" ], "type": "text/javascript" } @@ -377,16 +381,17 @@ "pm.test('Status code is 200', function () {", " pm.response.to.have.status(200);", "})", + "console.log(pm.response.json())", "", "pm.test('Contains Json Body', function () {", - " pm.response.to.have.jsonBody('address');", + " pm.response.to.have.jsonBody('account_identifier.address');", "})", "", "pm.test(\"Correct Address\", function () {", " const testAccountBaseAddress = pm.environment.get('TestAccountBaseAddress');", " var responseData = pm.response.json();", " pm.expect(responseData).to.be.an('object');", - " pm.expect(responseData.address).to.eql(testAccountBaseAddress);", + " pm.expect(responseData.account_identifier.address).to.eql(testAccountBaseAddress);", "});", "", "" @@ -430,18 +435,19 @@ "pm.test('Status code is 200', function () {", " pm.response.to.have.status(200);", "})", + "console.log(pm.response.json())", "", "pm.test('Contains Json Body', function () {", " pm.response.to.have.jsonBody('transaction_identifier');", "})", "", "pm.test(\"Correct Address\", function () {", - " const hashedSignedTransaction = pm.environment.get('hashedSignedTransaction');", - " var responseData = pm.response.json();", - " pm.expect(responseData).to.be.an('object');", - " pm.expect(responseData.transaction_identifier).to.be.an('object');", - " pm.expect(responseData.transaction_identifier.hash).to.eql(hashedSignedTransaction);", - "});" + " const hashedSignedTransaction = pm.environment.get('hashedSignedTransaction');", + " var responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.transaction_identifier).to.be.an('object');", + " pm.expect(responseData.transaction_identifier.hash).to.eql(hashedSignedTransaction);", + " });" ], "type": "text/javascript" } @@ -452,7 +458,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"cardano\",\n \"network\": \"devkit\"\n },\n \"signed_transaction\": \"{{signedTransaction}}\"\n}", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"cardano\",\n \"network\": \"{{networkId}}\"\n },\n \"signed_transaction\": \"{{signedTransaction}}\"\n}", "options": { "raw": { "language": "json" @@ -474,12 +480,30 @@ }, { "name": "/construction/metadata", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "})", + "console.log(pm.response.json())", + "", + "pm.test('Contains Json Body', function () {", + " pm.response.to.have.jsonBody('suggested_fee');", + "})" + ], + "type": "text/javascript" + } + } + ], "request": { "method": "POST", "header": [], "body": { "mode": "raw", - "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"cardano\",\n \"network\": \"{{networkId}}\"\n },\n \"options\": {\n \"relative_ttl\": 100,\n \"transaction_size\": 225\n }\n}", + "raw": "{\n \"network_identifier\": {\n \"blockchain\": \"cardano\",\n \"network\": \"{{networkId}}\"\n },\n \"options\": {\n \"transaction_size\": 40.0,\n \"relative_ttl\": 10.0\n }\n}", "options": { "raw": { "language": "json" diff --git a/yaci-indexer/pom.xml b/yaci-indexer/pom.xml index 6732d86cb..25d9b710a 100644 --- a/yaci-indexer/pom.xml +++ b/yaci-indexer/pom.xml @@ -89,6 +89,11 @@ yaci-store-mir-spring-boot-starter ${yaci-store.version} + + com.bloxbean.cardano + yaci-store-epoch-spring-boot-starter + ${yaci-store.version} + org.postgresql