From 8c8d23b8bfb8318925ca2015e170aabe0b5cb649 Mon Sep 17 00:00:00 2001 From: Mateusz Czeladka Date: Tue, 25 Nov 2025 15:48:53 +0100 Subject: [PATCH] feat: drep vote delegation available in network operations. --- .../api/block/mapper/TransactionMapper.java | 60 +++- .../block/mapper/TransactionMapperUtils.java | 15 + .../block/model/domain/DRepDelegation.java | 2 + .../entity/DrepVoteDelegationEntity.java | 65 ++++ .../model/entity/DrepVoteDelegationId.java | 17 ++ ...nEntity.java => PoolDelegationEntity.java} | 4 +- ...elegationId.java => PoolDelegationId.java} | 2 +- .../repository/DelegationRepository.java | 16 - .../DrepVoteDelegationRepository.java | 16 + .../repository/PoolDelegationRepository.java | 16 + .../block/service/LedgerBlockServiceImpl.java | 63 ++-- .../block/mapper/DRepTypeConversionTest.java | 277 ++++++++++++++++++ .../LedgerBlockServiceImplIntTest.java | 10 +- .../service/LedgerBlockServiceImplTest.java | 22 +- .../common/TestTransactionNames.java | 6 +- .../impl/GovernanceTransactions.java | 152 ++++++++++ .../src/main/resources/application.properties | 3 + .../application-test-integration.properties | 4 +- 18 files changed, 688 insertions(+), 62 deletions(-) create mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DrepVoteDelegationEntity.java create mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DrepVoteDelegationId.java rename api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/{DelegationEntity.java => PoolDelegationEntity.java} (87%) rename api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/{DelegationId.java => PoolDelegationId.java} (78%) delete mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/DelegationRepository.java create mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/DrepVoteDelegationRepository.java create mode 100644 api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/PoolDelegationRepository.java create mode 100644 api/src/test/java/org/cardanofoundation/rosetta/api/block/mapper/DRepTypeConversionTest.java create mode 100644 test-data-generator/src/main/java/org/cardanofoundation/rosetta/testgenerator/transactions/impl/GovernanceTransactions.java diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/mapper/TransactionMapper.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/mapper/TransactionMapper.java index cceaa5227d..2fb9c9204c 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/block/mapper/TransactionMapper.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/mapper/TransactionMapper.java @@ -47,7 +47,21 @@ public interface TransactionMapper { @Mapping(target = "operationIdentifier", source = "index", qualifiedByName = "OperationIdentifier") Operation mapPoolRetirementToOperation(PoolRetirement model, OperationStatus status, int index); - StakePoolDelegation mapDelegationEntityToDelegation(DelegationEntity entity); + @Mapping(source = "txHash", target = "txHash") + @Mapping(source = "certIndex", target = "certIndex") + @Mapping(source = "address", target = "address") + @Mapping(source = "drepHash", target = "drep.drepId") // no this is not a mistake we don't have bech32 representation in rosetta + @Mapping(source = "drepType", target = "drep.drepType", qualifiedByName = "convertYaciDrepType") + DRepDelegation mapEntityToDRepDelegation(DrepVoteDelegationEntity entity); + + @Mapping(source = "txHash", target = "txHash") + @Mapping(source = "certIndex", target = "certIndex") + @Mapping(source = "address", target = "address") + @Mapping(source = "drep.drepId", target = "drepHash") // no this is not a mistake we don't have bech32 representation in rosetta + @Mapping(source = "drep.drepType", target = "drepType", qualifiedByName = "convertClientDrepType") + DrepVoteDelegationEntity mapDRepDelegationToEntity(DRepDelegation dRepDelegation); + + StakePoolDelegation mapPoolDelegationEntityToDelegation(PoolDelegationEntity entity); @Mapping(target = "status", source = "status.status") @Mapping(target = "type", constant = Constants.OPERATION_TYPE_STAKE_DELEGATION) @@ -56,6 +70,50 @@ public interface TransactionMapper { @Mapping(target = "metadata.poolKeyHash", source = "model.poolId") Operation mapStakeDelegationToOperation(StakePoolDelegation model, OperationStatus status, int index); + default DRepDelegation mapDrepVoteDelegationEntityToDRepDelegation(DrepVoteDelegationEntity entity) { + if (entity == null) { + return null; + } + + return DRepDelegation.builder() + .txHash(entity.getTxHash()) + .certIndex(entity.getCertIndex()) + .address(entity.getAddress()) + .drep(new DRepDelegation.DRep( + entity.getDrepHash(), + convertYaciDrepType(entity.getDrepType()) + )) + .build(); + } + + @Named("convertYaciDrepType") + default com.bloxbean.cardano.client.transaction.spec.governance.DRepType convertYaciDrepType( + com.bloxbean.cardano.yaci.core.model.governance.DrepType yaciDrepType) { + if (yaciDrepType == null) { + return null; + } + return switch (yaciDrepType) { + case ADDR_KEYHASH -> com.bloxbean.cardano.client.transaction.spec.governance.DRepType.ADDR_KEYHASH; + case SCRIPTHASH -> com.bloxbean.cardano.client.transaction.spec.governance.DRepType.SCRIPTHASH; + case ABSTAIN -> com.bloxbean.cardano.client.transaction.spec.governance.DRepType.ABSTAIN; + case NO_CONFIDENCE -> com.bloxbean.cardano.client.transaction.spec.governance.DRepType.NO_CONFIDENCE; + }; + } + + @Named("convertClientDrepType") + default com.bloxbean.cardano.yaci.core.model.governance.DrepType convertClientDrepType( + com.bloxbean.cardano.client.transaction.spec.governance.DRepType clientDrepType) { + if (clientDrepType == null) { + return null; + } + return switch (clientDrepType) { + case ADDR_KEYHASH -> com.bloxbean.cardano.yaci.core.model.governance.DrepType.ADDR_KEYHASH; + case SCRIPTHASH -> com.bloxbean.cardano.yaci.core.model.governance.DrepType.SCRIPTHASH; + case ABSTAIN -> com.bloxbean.cardano.yaci.core.model.governance.DrepType.ABSTAIN; + case NO_CONFIDENCE -> com.bloxbean.cardano.yaci.core.model.governance.DrepType.NO_CONFIDENCE; + }; + } + @Mapping(target = "status", source = "status.status") @Mapping(target = "type", constant = Constants.OPERATION_TYPE_DREP_VOTE_DELEGATION) @Mapping(target = "operationIdentifier", source = "index", qualifiedByName = "OperationIdentifier") diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/mapper/TransactionMapperUtils.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/mapper/TransactionMapperUtils.java index ced554b0a4..3b3f4835d0 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/block/mapper/TransactionMapperUtils.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/mapper/TransactionMapperUtils.java @@ -84,6 +84,21 @@ public DRepParams convertDRepFromRosetta(DRepDelegation.DRep drep) { return DRepDelegation.DRep.convertDRepFromRosetta(drep); } + @Named("convertDrepTypeStringToEnum") + public com.bloxbean.cardano.client.transaction.spec.governance.DRepType convertDrepTypeStringToEnum(@Nullable String drepType) { + if (drepType == null) { + return null; + } + + return switch (drepType) { + case "ADDR_KEYHASH" -> com.bloxbean.cardano.client.transaction.spec.governance.DRepType.ADDR_KEYHASH; + case "SCRIPTHASH" -> com.bloxbean.cardano.client.transaction.spec.governance.DRepType.SCRIPTHASH; + case "ABSTAIN" -> com.bloxbean.cardano.client.transaction.spec.governance.DRepType.ABSTAIN; + case "NO_CONFIDENCE" -> com.bloxbean.cardano.client.transaction.spec.governance.DRepType.NO_CONFIDENCE; + default -> null; + }; + } + @Named("mapAmountsToOperationMetadataInputWithCache") public OperationMetadata mapToOperationMetaDataInputWithCache(List amounts, @Context Map metadataMap) { diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/domain/DRepDelegation.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/domain/DRepDelegation.java index 81ececba03..e9efb966ba 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/domain/DRepDelegation.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/domain/DRepDelegation.java @@ -10,6 +10,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@Setter public class DRepDelegation { private String txHash; @@ -20,6 +21,7 @@ public class DRepDelegation { @AllArgsConstructor @NoArgsConstructor @Getter + @Setter public static class DRep { private String drepId; diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DrepVoteDelegationEntity.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DrepVoteDelegationEntity.java new file mode 100644 index 0000000000..001aabcdc4 --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DrepVoteDelegationEntity.java @@ -0,0 +1,65 @@ +package org.cardanofoundation.rosetta.api.block.model.entity; + +import com.bloxbean.cardano.yaci.core.model.certs.StakeCredType; +import com.bloxbean.cardano.yaci.core.model.governance.DrepType; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "delegation_vote") +@IdClass(DrepVoteDelegationId.class) +public class DrepVoteDelegationEntity { + + @jakarta.persistence.Id + @Column(name = "tx_hash") + private String txHash; + + @jakarta.persistence.Id + @Column(name = "cert_index") + private long certIndex; + + @Column(name = "slot") + private Long slot; + + @Column(name = "block") + private Long blockNumber; + + @Column(name = "block_time") + private Long blockTime; + + @Column(name = "update_datetime") + private LocalDateTime updateDateTime; + + @Column(name = "address") + private String address; + + @Column(name = "drep_hash") // actual drep id as hex hash + private String drepHash; + + @Column(name = "drep_id") // bech 32 + private String drepId; + + @Column(name = "drep_type") + @Enumerated(EnumType.STRING) + private DrepType drepType; + + @Column(name = "credential") + private String credential; + + @Column(name = "cred_type") + @Enumerated(EnumType.STRING) + private StakeCredType credType; + + @Column(name = "epoch") + private Integer epoch; + +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DrepVoteDelegationId.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DrepVoteDelegationId.java new file mode 100644 index 0000000000..a9167ec9b7 --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DrepVoteDelegationId.java @@ -0,0 +1,17 @@ +package org.cardanofoundation.rosetta.api.block.model.entity; + +import jakarta.persistence.Column; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +@EqualsAndHashCode +public class DrepVoteDelegationId implements Serializable { + + @Column(name = "tx_hash") + private String txHash; + + @Column(name = "cert_index") + private long certIndex; + +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DelegationEntity.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/PoolDelegationEntity.java similarity index 87% rename from api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DelegationEntity.java rename to api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/PoolDelegationEntity.java index 804895f49b..ac8ab54a7d 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DelegationEntity.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/PoolDelegationEntity.java @@ -11,8 +11,8 @@ @NoArgsConstructor @Entity @Table(name = "delegation") -@IdClass(DelegationId.class) -public class DelegationEntity { +@IdClass(PoolDelegationId.class) +public class PoolDelegationEntity { @Id @Column(name = "tx_hash") diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DelegationId.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/PoolDelegationId.java similarity index 78% rename from api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DelegationId.java rename to api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/PoolDelegationId.java index 3489f8f1bb..b39bcffab7 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/DelegationId.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/entity/PoolDelegationId.java @@ -5,7 +5,7 @@ import lombok.EqualsAndHashCode; @EqualsAndHashCode -public class DelegationId implements Serializable { +public class PoolDelegationId implements Serializable { private String txHash; private long certIndex; diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/DelegationRepository.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/DelegationRepository.java deleted file mode 100644 index 7a015d4d54..0000000000 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/DelegationRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.cardanofoundation.rosetta.api.block.model.repository; - -import java.util.List; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import org.cardanofoundation.rosetta.api.block.model.entity.DelegationEntity; -import org.cardanofoundation.rosetta.api.block.model.entity.DelegationId; - -@Repository -public interface DelegationRepository extends JpaRepository { - - List findByTxHashIn(List txHashes); - -} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/DrepVoteDelegationRepository.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/DrepVoteDelegationRepository.java new file mode 100644 index 0000000000..a7cf1a961b --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/DrepVoteDelegationRepository.java @@ -0,0 +1,16 @@ +package org.cardanofoundation.rosetta.api.block.model.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import org.cardanofoundation.rosetta.api.block.model.entity.DrepVoteDelegationEntity; +import org.cardanofoundation.rosetta.api.block.model.entity.DrepVoteDelegationId; + +@Repository +public interface DrepVoteDelegationRepository extends JpaRepository { + + List findByTxHashInAndCertIndex(List txHashes, long certIndex); + +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/PoolDelegationRepository.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/PoolDelegationRepository.java new file mode 100644 index 0000000000..4296222855 --- /dev/null +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/model/repository/PoolDelegationRepository.java @@ -0,0 +1,16 @@ +package org.cardanofoundation.rosetta.api.block.model.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import org.cardanofoundation.rosetta.api.block.model.entity.PoolDelegationEntity; +import org.cardanofoundation.rosetta.api.block.model.entity.PoolDelegationId; + +@Repository +public interface PoolDelegationRepository extends JpaRepository { + + List findByTxHashIn(List txHashes); + +} diff --git a/api/src/main/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImpl.java b/api/src/main/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImpl.java index 3691e57057..4df234c784 100644 --- a/api/src/main/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImpl.java +++ b/api/src/main/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImpl.java @@ -1,29 +1,9 @@ package org.cardanofoundation.rosetta.api.block.service; -import java.time.Clock; -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.StructuredTaskScope; -import java.util.concurrent.StructuredTaskScope.ShutdownOnFailure; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; -import java.util.stream.Stream; import jakarta.validation.constraints.NotNull; -import javax.annotation.PostConstruct; - import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - import org.cardanofoundation.rosetta.api.account.mapper.AddressUtxoEntityToUtxo; import org.cardanofoundation.rosetta.api.account.model.domain.Utxo; import org.cardanofoundation.rosetta.api.account.model.entity.AddressUtxoEntity; @@ -36,6 +16,23 @@ import org.cardanofoundation.rosetta.api.block.model.entity.*; import org.cardanofoundation.rosetta.api.block.model.repository.*; import org.cardanofoundation.rosetta.common.exception.ExceptionFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import java.time.Clock; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.StructuredTaskScope; +import java.util.concurrent.StructuredTaskScope.ShutdownOnFailure; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Slf4j @Component @@ -47,7 +44,8 @@ public class LedgerBlockServiceImpl implements LedgerBlockService { private final BlockRepository blockRepository; private final TxRepository txRepository; private final StakeRegistrationRepository stakeRegistrationRepository; - private final DelegationRepository delegationRepository; + private final PoolDelegationRepository poolDelegationRepository; + private final DrepVoteDelegationRepository drepVoteDelegationRepository; private final PoolRegistrationRepository poolRegistrationRepository; private final PoolRetirementRepository poolRetirementRepository; private final WithdrawalRepository withdrawalRepository; @@ -233,10 +231,11 @@ private TransactionInfo findByTxHash(List transactions) { try (ShutdownOnFailure scope = new ShutdownOnFailure()) { StructuredTaskScope.Subtask> utxos = scope.fork(() -> addressUtxoRepository.findByTxHashIn(utxHashes)); StructuredTaskScope.Subtask> sReg = scope.fork(() -> stakeRegistrationRepository.findByTxHashIn(txHashes)); - StructuredTaskScope.Subtask> delegations = scope.fork(() -> delegationRepository.findByTxHashIn(txHashes)); + StructuredTaskScope.Subtask> poolDelegations = scope.fork(() -> poolDelegationRepository.findByTxHashIn(txHashes)); StructuredTaskScope.Subtask> pReg = scope.fork(() -> poolRegistrationRepository.findByTxHashIn(txHashes)); StructuredTaskScope.Subtask> pRet = scope.fork(() -> poolRetirementRepository.findByTxHashIn(txHashes)); StructuredTaskScope.Subtask> withdrawals = scope.fork(() -> withdrawalRepository.findByTxHashIn(txHashes)); + StructuredTaskScope.Subtask> drepDelegations = scope.fork(() -> drepVoteDelegationRepository.findByTxHashInAndCertIndex(txHashes, 0L)); StructuredTaskScope.Subtask> invalidTxs = scope.fork(() -> invalidTransactionRepository.findByTxHashIn(txHashes)); scope.joinUntil(Instant.now(clock).plusSeconds(blockTransactionApiTimeoutSecs)); @@ -245,7 +244,8 @@ private TransactionInfo findByTxHash(List transactions) { return new TransactionInfo( utxos.get(), sReg.get(), - delegations.get(), + poolDelegations.get(), + drepDelegations.get(), pReg.get(), pRet.get(), withdrawals.get(), @@ -293,10 +293,10 @@ void populateTransaction(BlockTx transaction, .toList()); transaction.setStakePoolDelegations( - fetched.delegations + fetched.poolDelegations .stream() .filter(tx -> tx.getTxHash().equals(transaction.getHash())) - .map(transactionMapper::mapDelegationEntityToDelegation) + .map(transactionMapper::mapPoolDelegationEntityToDelegation) .toList()); transaction.setWithdrawals( @@ -319,8 +319,13 @@ void populateTransaction(BlockTx transaction, .filter(tx -> tx.getTxHash().equals(transaction.getHash())) .map(transactionMapper::mapEntityToPoolRetirement) .toList()); - // TODO dRep Vote Delegations - //transaction.setDRepDelegations(fetched.delegations + + transaction.setDRepDelegations( + fetched.drepDelegations + .stream() + .filter(tx -> tx.getTxHash().equals(transaction.getHash())) + .map(transactionMapper::mapDrepVoteDelegationEntityToDRepDelegation) + .toList()); // TODO governance votes //transaction.setGovernanceVotes(fetched.); @@ -345,12 +350,12 @@ private static Map getUtxoMapFromEntities(Transactio record TransactionInfo(List utxos, List stakeRegistrations, - List delegations, + List poolDelegations, + List drepDelegations, List poolRegistrations, List poolRetirements, List withdrawals, List invalidTransactions) { - } record UtxoKey(String txHash, Integer outputIndex) { diff --git a/api/src/test/java/org/cardanofoundation/rosetta/api/block/mapper/DRepTypeConversionTest.java b/api/src/test/java/org/cardanofoundation/rosetta/api/block/mapper/DRepTypeConversionTest.java new file mode 100644 index 0000000000..415a008aba --- /dev/null +++ b/api/src/test/java/org/cardanofoundation/rosetta/api/block/mapper/DRepTypeConversionTest.java @@ -0,0 +1,277 @@ +package org.cardanofoundation.rosetta.api.block.mapper; + +import org.cardanofoundation.rosetta.api.common.mapper.TokenRegistryMapperImpl; +import org.cardanofoundation.rosetta.common.mapper.DataMapper; +import org.cardanofoundation.rosetta.common.services.ProtocolParamService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for DRepType conversion logic in TransactionMapper. + * Tests the enum conversions between Yaci and cardano-client-lib DRepType enums. + * + * This test validates the critical business logic of converting between: + * - com.bloxbean.cardano.yaci.core.model.governance.DrepType (entity layer) + * - com.bloxbean.cardano.client.transaction.spec.governance.DRepType (domain layer) + */ +@ExtendWith(MockitoExtension.class) +class DRepTypeConversionTest { + + @Mock + private ProtocolParamService protocolParamService; + + private TransactionMapper transactionMapper; + + @BeforeEach + void setUp() { + // Create real instances with dependencies + DataMapper dataMapper = new DataMapper(new TokenRegistryMapperImpl()); + TransactionMapperUtils transactionMapperUtils = new TransactionMapperUtils(protocolParamService, dataMapper); + transactionMapper = new TransactionMapperImpl(transactionMapperUtils); + } + + @Nested + class YaciToClientConversionTests { + + @Test + void shouldConvertAddrKeyHash() { + // given + com.bloxbean.cardano.yaci.core.model.governance.DrepType yaciType = + com.bloxbean.cardano.yaci.core.model.governance.DrepType.ADDR_KEYHASH; + + // when + com.bloxbean.cardano.client.transaction.spec.governance.DRepType result = + transactionMapper.convertYaciDrepType(yaciType); + + // then + assertThat(result).isEqualTo(com.bloxbean.cardano.client.transaction.spec.governance.DRepType.ADDR_KEYHASH); + } + + @Test + void shouldConvertScriptHash() { + // given + com.bloxbean.cardano.yaci.core.model.governance.DrepType yaciType = + com.bloxbean.cardano.yaci.core.model.governance.DrepType.SCRIPTHASH; + + // when + com.bloxbean.cardano.client.transaction.spec.governance.DRepType result = + transactionMapper.convertYaciDrepType(yaciType); + + // then + assertThat(result).isEqualTo(com.bloxbean.cardano.client.transaction.spec.governance.DRepType.SCRIPTHASH); + } + + @Test + void shouldConvertAbstain() { + // given + com.bloxbean.cardano.yaci.core.model.governance.DrepType yaciType = + com.bloxbean.cardano.yaci.core.model.governance.DrepType.ABSTAIN; + + // when + com.bloxbean.cardano.client.transaction.spec.governance.DRepType result = + transactionMapper.convertYaciDrepType(yaciType); + + // then + assertThat(result).isEqualTo(com.bloxbean.cardano.client.transaction.spec.governance.DRepType.ABSTAIN); + } + + @Test + void shouldConvertNoConfidence() { + // given + com.bloxbean.cardano.yaci.core.model.governance.DrepType yaciType = + com.bloxbean.cardano.yaci.core.model.governance.DrepType.NO_CONFIDENCE; + + // when + com.bloxbean.cardano.client.transaction.spec.governance.DRepType result = + transactionMapper.convertYaciDrepType(yaciType); + + // then + assertThat(result).isEqualTo(com.bloxbean.cardano.client.transaction.spec.governance.DRepType.NO_CONFIDENCE); + } + + @Test + void shouldHandleNull() { + // when + com.bloxbean.cardano.client.transaction.spec.governance.DRepType result = + transactionMapper.convertYaciDrepType(null); + + // then + assertThat(result).isNull(); + } + + @Test + void shouldConvertAllYaciTypesSuccessfully() { + // Test all Yaci enum values convert without error + for (com.bloxbean.cardano.yaci.core.model.governance.DrepType yaciType : + com.bloxbean.cardano.yaci.core.model.governance.DrepType.values()) { + + // when + com.bloxbean.cardano.client.transaction.spec.governance.DRepType result = + transactionMapper.convertYaciDrepType(yaciType); + + // then + assertThat(result) + .as("Conversion of %s should not be null", yaciType.name()) + .isNotNull(); + assertThat(result.name()) + .as("Enum names should match for %s", yaciType.name()) + .isEqualTo(yaciType.name()); + } + } + } + + @Nested + class ClientToYaciConversionTests { + + @Test + void shouldConvertAddrKeyHash() { + // given + com.bloxbean.cardano.client.transaction.spec.governance.DRepType clientType = + com.bloxbean.cardano.client.transaction.spec.governance.DRepType.ADDR_KEYHASH; + + // when + com.bloxbean.cardano.yaci.core.model.governance.DrepType result = + transactionMapper.convertClientDrepType(clientType); + + // then + assertThat(result).isEqualTo(com.bloxbean.cardano.yaci.core.model.governance.DrepType.ADDR_KEYHASH); + } + + @Test + void shouldConvertScriptHash() { + // given + com.bloxbean.cardano.client.transaction.spec.governance.DRepType clientType = + com.bloxbean.cardano.client.transaction.spec.governance.DRepType.SCRIPTHASH; + + // when + com.bloxbean.cardano.yaci.core.model.governance.DrepType result = + transactionMapper.convertClientDrepType(clientType); + + // then + assertThat(result).isEqualTo(com.bloxbean.cardano.yaci.core.model.governance.DrepType.SCRIPTHASH); + } + + @Test + void shouldConvertAbstain() { + // given + com.bloxbean.cardano.client.transaction.spec.governance.DRepType clientType = + com.bloxbean.cardano.client.transaction.spec.governance.DRepType.ABSTAIN; + + // when + com.bloxbean.cardano.yaci.core.model.governance.DrepType result = + transactionMapper.convertClientDrepType(clientType); + + // then + assertThat(result).isEqualTo(com.bloxbean.cardano.yaci.core.model.governance.DrepType.ABSTAIN); + } + + @Test + void shouldConvertNoConfidence() { + // given + com.bloxbean.cardano.client.transaction.spec.governance.DRepType clientType = + com.bloxbean.cardano.client.transaction.spec.governance.DRepType.NO_CONFIDENCE; + + // when + com.bloxbean.cardano.yaci.core.model.governance.DrepType result = + transactionMapper.convertClientDrepType(clientType); + + // then + assertThat(result).isEqualTo(com.bloxbean.cardano.yaci.core.model.governance.DrepType.NO_CONFIDENCE); + } + + @Test + void shouldHandleNull() { + // when + com.bloxbean.cardano.yaci.core.model.governance.DrepType result = + transactionMapper.convertClientDrepType(null); + + // then + assertThat(result).isNull(); + } + + @Test + void shouldConvertAllClientTypesSuccessfully() { + // Test all Client enum values convert without error + for (com.bloxbean.cardano.client.transaction.spec.governance.DRepType clientType : + com.bloxbean.cardano.client.transaction.spec.governance.DRepType.values()) { + + // when + com.bloxbean.cardano.yaci.core.model.governance.DrepType result = + transactionMapper.convertClientDrepType(clientType); + + // then + assertThat(result) + .as("Conversion of %s should not be null", clientType.name()) + .isNotNull(); + assertThat(result.name()) + .as("Enum names should match for %s", clientType.name()) + .isEqualTo(clientType.name()); + } + } + } + + @Nested + class BidirectionalConversionTests { + + @Test + void shouldRoundTripYaciToClientAndBack() { + // Test complete round-trip conversion for all values + for (com.bloxbean.cardano.yaci.core.model.governance.DrepType original : + com.bloxbean.cardano.yaci.core.model.governance.DrepType.values()) { + + // when - convert Yaci -> Client -> Yaci + com.bloxbean.cardano.client.transaction.spec.governance.DRepType clientType = + transactionMapper.convertYaciDrepType(original); + com.bloxbean.cardano.yaci.core.model.governance.DrepType result = + transactionMapper.convertClientDrepType(clientType); + + // then + assertThat(result) + .as("Round-trip conversion for %s", original.name()) + .isEqualTo(original); + } + } + + @Test + void shouldRoundTripClientToYaciAndBack() { + // Test complete round-trip conversion for all values + for (com.bloxbean.cardano.client.transaction.spec.governance.DRepType original : + com.bloxbean.cardano.client.transaction.spec.governance.DRepType.values()) { + + // when - convert Client -> Yaci -> Client + com.bloxbean.cardano.yaci.core.model.governance.DrepType yaciType = + transactionMapper.convertClientDrepType(original); + com.bloxbean.cardano.client.transaction.spec.governance.DRepType result = + transactionMapper.convertYaciDrepType(yaciType); + + // then + assertThat(result) + .as("Round-trip conversion for %s", original.name()) + .isEqualTo(original); + } + } + + @Test + void shouldMaintainEnumNameConsistency() { + // Verify that enum names are identical across both types + com.bloxbean.cardano.yaci.core.model.governance.DrepType[] yaciValues = + com.bloxbean.cardano.yaci.core.model.governance.DrepType.values(); + com.bloxbean.cardano.client.transaction.spec.governance.DRepType[] clientValues = + com.bloxbean.cardano.client.transaction.spec.governance.DRepType.values(); + + assertThat(yaciValues).hasSameSizeAs(clientValues); + + for (int i = 0; i < yaciValues.length; i++) { + assertThat(yaciValues[i].name()) + .as("Enum name at index %d", i) + .isEqualTo(clientValues[i].name()); + } + } + } +} diff --git a/api/src/test/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImplIntTest.java b/api/src/test/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImplIntTest.java index f4cfbd5d28..80e25c8d0b 100644 --- a/api/src/test/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImplIntTest.java +++ b/api/src/test/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImplIntTest.java @@ -121,7 +121,7 @@ void findTransactionsByBlock_Test_empty_tx() { } @Test - void findTransactionsByBlock_Test_delegation_tx() { + void findTransactionsByBlock_Test_pool_delegation_tx() { //given TransactionBlockDetails tx = generatedDataMap.get(POOL_DELEGATION_TRANSACTION.getName()); //when @@ -141,12 +141,12 @@ void findTransactionsByBlock_Test_delegation_tx() { assertThat(blockTx.getStakePoolDelegations().getFirst().getAddress()) .isEqualTo(STAKE_ADDRESS_WITH_EARNED_REWARDS); - List delegations = entityManager - .createQuery("FROM DelegationEntity b where b.txHash=:hash", DelegationEntity.class) + List poolDelegations = entityManager + .createQuery("FROM PoolDelegationEntity b where b.txHash=:hash", PoolDelegationEntity.class) .setParameter("hash", tx.txHash()) .getResultList(); - assertThat(delegations).isNotNull().hasSize(1); - DelegationEntity expected = delegations.getFirst(); + assertThat(poolDelegations).isNotNull().hasSize(1); + PoolDelegationEntity expected = poolDelegations.getFirst(); assertThat(expected.getAddress()).isEqualTo(STAKE_ADDRESS_WITH_EARNED_REWARDS); StakePoolDelegation actual = blockTx.getStakePoolDelegations().getFirst(); diff --git a/api/src/test/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImplTest.java b/api/src/test/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImplTest.java index 5c0b93fe05..12daad7e44 100644 --- a/api/src/test/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImplTest.java +++ b/api/src/test/java/org/cardanofoundation/rosetta/api/block/service/LedgerBlockServiceImplTest.java @@ -55,7 +55,10 @@ class LedgerBlockServiceImplTest { private StakeRegistrationRepository stakeRegistrationRepository; @Mock - private DelegationRepository delegationRepository; + private PoolDelegationRepository poolDelegationRepository; + + @Mock + private DrepVoteDelegationRepository drepVoteDelegationRepository; @Mock private PoolRegistrationRepository poolRegistrationRepository; @@ -89,6 +92,7 @@ void populateTransaction_marksTransactionAsInvalid_ifFoundInInvalidTransactionRe Collections.emptyList(), // utxos Collections.emptyList(), // stakeRegistrations Collections.emptyList(), // delegations + Collections.emptyList(), // drepDelegations Collections.emptyList(), // poolRegistrations Collections.emptyList(), // poolRetirements Collections.emptyList(), // withdrawals @@ -112,6 +116,7 @@ void populateTransaction_doesNotMarkTransactionAsInvalid_ifNotFoundInInvalidTran Collections.emptyList(), // utxos Collections.emptyList(), // stakeRegistrations Collections.emptyList(), // delegations + Collections.emptyList(), // drepDelegations Collections.emptyList(), // poolRegistrations Collections.emptyList(), // poolRetirements Collections.emptyList(), // withdrawals @@ -149,6 +154,7 @@ void populateTransaction_populatesStakeRegistrations() { Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), Collections.emptyList() ); @@ -165,21 +171,22 @@ void populateTransaction_populatesDelegations() { transaction.setOutputs(Collections.emptyList()); val utxoMap = new HashMap(); - DelegationEntity entity1 = new DelegationEntity(); + PoolDelegationEntity entity1 = new PoolDelegationEntity(); entity1.setTxHash("txHash1"); - DelegationEntity entity2 = new DelegationEntity(); + PoolDelegationEntity entity2 = new PoolDelegationEntity(); entity2.setTxHash("txHash2"); - List delegations = List.of(entity1, entity2); + List poolDelegations = List.of(entity1, entity2); - when(transactionMapper.mapDelegationEntityToDelegation(entity1)) + when(transactionMapper.mapPoolDelegationEntityToDelegation(entity1)) .thenReturn(new StakePoolDelegation()); val transactionInfo = new LedgerBlockServiceImpl.TransactionInfo( Collections.emptyList(), Collections.emptyList(), - delegations, + poolDelegations, + Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), @@ -216,6 +223,7 @@ void populateTransaction_populatesWithdrawals() { Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), withdrawals, Collections.emptyList() ); @@ -247,6 +255,7 @@ void populateTransaction_populatesPoolRegistrations() { Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), poolRegistrations, Collections.emptyList(), Collections.emptyList(), @@ -281,6 +290,7 @@ void populateTransaction_populatesPoolRetirements() { Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), poolRetirements, Collections.emptyList(), Collections.emptyList() diff --git a/test-data-generator/src/main/java/org/cardanofoundation/rosetta/testgenerator/common/TestTransactionNames.java b/test-data-generator/src/main/java/org/cardanofoundation/rosetta/testgenerator/common/TestTransactionNames.java index 49deeb3794..76acc19c90 100644 --- a/test-data-generator/src/main/java/org/cardanofoundation/rosetta/testgenerator/common/TestTransactionNames.java +++ b/test-data-generator/src/main/java/org/cardanofoundation/rosetta/testgenerator/common/TestTransactionNames.java @@ -14,7 +14,11 @@ public enum TestTransactionNames { // Transaction names for PoolTransactions POOL_REGISTRATION_TRANSACTION("pool_registration"), POOL_DELEGATION_TRANSACTION("pool_delegation"), - POOL_RETIREMENT_TRANSACTION("pool_retirement"); + POOL_RETIREMENT_TRANSACTION("pool_retirement"), + + // Transaction names for GovernanceTransactions + DREP_REGISTER("drep_register"), + DREP_VOTE_DELEGATION("drep_vote_delegation"); private final String name; diff --git a/test-data-generator/src/main/java/org/cardanofoundation/rosetta/testgenerator/transactions/impl/GovernanceTransactions.java b/test-data-generator/src/main/java/org/cardanofoundation/rosetta/testgenerator/transactions/impl/GovernanceTransactions.java new file mode 100644 index 0000000000..a872642496 --- /dev/null +++ b/test-data-generator/src/main/java/org/cardanofoundation/rosetta/testgenerator/transactions/impl/GovernanceTransactions.java @@ -0,0 +1,152 @@ +package org.cardanofoundation.rosetta.testgenerator.transactions.impl; + +import com.bloxbean.cardano.client.account.Account; +import com.bloxbean.cardano.client.api.model.Amount; +import com.bloxbean.cardano.client.api.model.Result; +import com.bloxbean.cardano.client.backend.model.Block; +import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.function.helper.SignerProviders; +import com.bloxbean.cardano.client.quicktx.Tx; +import com.bloxbean.cardano.client.transaction.spec.governance.Anchor; +import com.bloxbean.cardano.client.transaction.spec.governance.DRep; +import com.bloxbean.cardano.client.util.HexUtil; +import lombok.extern.slf4j.Slf4j; +import org.cardanofoundation.rosetta.testgenerator.common.BaseFunctions; +import org.cardanofoundation.rosetta.testgenerator.common.TestConstants; +import org.cardanofoundation.rosetta.testgenerator.common.TestTransactionNames; +import org.cardanofoundation.rosetta.testgenerator.common.TransactionBlockDetails; +import org.cardanofoundation.rosetta.testgenerator.transactions.TransactionRunner; + +import java.math.BigInteger; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import static org.cardanofoundation.rosetta.testgenerator.common.BaseFunctions.checkIfUtxoAvailable; +import static org.cardanofoundation.rosetta.testgenerator.common.BaseFunctions.quickTxBuilder; + +@Slf4j +public class GovernanceTransactions implements TransactionRunner { + + private Account delegatingToAccount; + private String delegatingToAddress; + + private Account drepAccount; + private String drepAddr; + + @Override + public void init() { + delegatingToAccount = new Account(Networks.testnet(), TestConstants.SENDER_1_MNEMONIC); + delegatingToAddress = delegatingToAccount.baseAddress(); + + log.info("(delegating to a dRep) address: {}", delegatingToAddress); + + drepAccount = new Account(Networks.testnet(), TestConstants.SENDER_2_MNEMONIC); + drepAddr = drepAccount.baseAddress(); + + log.info("(dRep) address: {}", drepAddr); + } + + @Override + public Map runTransactions() { + Map generatedDataMap = HashMap.newHashMap(3); + + generatedDataMap.put(TestTransactionNames.STAKE_KEY_REGISTRATION_TRANSACTION.getName() + "_2", + registerDelegatingToStakeAddress()); + + generatedDataMap.put(TestTransactionNames.DREP_REGISTER.getName(), + registerDRep()); + + generatedDataMap.put(TestTransactionNames.DREP_VOTE_DELEGATION.getName(), + delegateToADRep()); + + return generatedDataMap; + } + + private TransactionBlockDetails registerDelegatingToStakeAddress() { + Tx registerStakeAddress1Tx = new Tx() + .registerStakeAddress(delegatingToAddress) + .payToAddress(delegatingToAddress, Amount.lovelace(BigInteger.valueOf(2000000L))) + .from(delegatingToAddress); + + Result result = quickTxBuilder.compose(registerStakeAddress1Tx) + .withSigner(SignerProviders.signerFrom(delegatingToAccount)) + .completeAndWait(Duration.ofMinutes(1)); + + if (result.isSuccessful()) { + String txHash = result.getValue(); + checkIfUtxoAvailable(result.getValue(), delegatingToAddress); + Block value1 = BaseFunctions.getBlock(txHash); + String hash = value1.getHash(); + + log.info("Block hash:%s".formatted(hash)); + + return new TransactionBlockDetails(txHash, hash, value1.getHeight()); + } + + throw new RuntimeException("Error in registering stake address, reason: " + result.getResponse()); + } + + public TransactionBlockDetails registerDRep() { + log.info("dRep registration..."); + + var anchor = new Anchor("https://pages.bloxbean.com/cardano-stake/bloxbean-pool.json", + HexUtil.decodeHexString("bafef700c0039a2efb056a665b3a8bcd94f8670b88d659f7f3db68340f6f0937")); + + Tx drepRegTx = new Tx() + .registerDRep(drepAccount, anchor) + .from(drepAddr); + + Result result = quickTxBuilder.compose(drepRegTx) + .withSigner(SignerProviders.signerFrom(drepAccount)) + .withSigner(SignerProviders.signerFrom(drepAccount.drepHdKeyPair())) + .complete(); + + if (result.isSuccessful()) { + BaseFunctions.checkIfUtxoAvailable(result.getValue(), drepAddr); + + String txHash = result.getValue(); + checkIfUtxoAvailable(result.getValue(), drepAddr); + Block value1 = BaseFunctions.getBlock(txHash); + String hash = value1.getHash(); + + log.info("Block hash:%s".formatted(hash)); + + return new TransactionBlockDetails(txHash, hash, value1.getHeight()); + } + + throw new RuntimeException("Error in registering dRep, error:" + result.getResponse()); + } + + public TransactionBlockDetails delegateToADRep() { + log.info("Delegating to a dRep..."); + + // Create DRep using the stake credential hash + // The drepId contains the credential hash that we need + String drepId = drepAccount.drepId(); + DRep drep = DRep.addrKeyHash(drepId); + + Tx tx = new Tx() + .delegateVotingPowerTo(delegatingToAddress, drep) + .from(drepAddr); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.stakeKeySignerFrom(delegatingToAccount)) + .withSigner(SignerProviders.signerFrom(drepAccount)) + .completeAndWait(); + + if (result.isSuccessful()) { + String txHash = result.getValue(); + checkIfUtxoAvailable(result.getValue(), drepAddr); + Block value1 = BaseFunctions.getBlock(txHash); + String hash = value1.getHash(); + + log.info("Block hash:%s".formatted(hash)); + + return new TransactionBlockDetails(txHash, hash, value1.getHeight()); + } + + throw new RuntimeException("error in delegating to a dRep, reason:" + result.getResponse()); + } + +} diff --git a/yaci-indexer/src/main/resources/application.properties b/yaci-indexer/src/main/resources/application.properties index ebfc0300c8..bec1e39890 100644 --- a/yaci-indexer/src/main/resources/application.properties +++ b/yaci-indexer/src/main/resources/application.properties @@ -119,3 +119,6 @@ store.epoch.endpoints.epoch.local.enabled=true store.continue-on-parse-error=${CONTINUE_PARSING_ON_ERROR:false} jobs.peer-discovery.enabled=${PEER_DISCOVERY:false} + +# disable local state for n2c governance for now +store.governance.n2c-gov-state-enabled=false \ No newline at end of file diff --git a/yaci-indexer/src/test/resources/application-test-integration.properties b/yaci-indexer/src/test/resources/application-test-integration.properties index aa1c398465..fbcfe3915b 100644 --- a/yaci-indexer/src/test/resources/application-test-integration.properties +++ b/yaci-indexer/src/test/resources/application-test-integration.properties @@ -16,4 +16,6 @@ store.cardano.return-tx-body-cbor=true store.utxo.pruning-interval=3600 store.utxo.pruning-safe-blocks=${REMOVE_SPENT_UTXOS_LAST_BLOCKS_GRACE_COUNT:129600} -store.utxo.pruning-batch-size=${REMOVE_SPENT_UTXOS_BATCH_SIZE:3000} \ No newline at end of file +store.utxo.pruning-batch-size=${REMOVE_SPENT_UTXOS_BATCH_SIZE:3000} + +store.governance.n2c-gov-state-enabled=false \ No newline at end of file