Skip to content

feat: SPO voting (governance) #467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

## What the project is about?


This repository provides a lightweight java implementation of the [Rosetta API](https://github.com/coinbase/mesh-specifications). It uses [Yaci-Store](https://github.com/bloxbean/yaci-store) as an indexer
to fetch the data from a Cardano node.

Expand Down
1 change: 1 addition & 0 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@
<sourceFolder>src/gen/java/main</sourceFolder>
<useJakartaEe>true</useJakartaEe>
<skipValidateSpec>true</skipValidateSpec>
<useEnumCaseInsensitive>true</useEnumCaseInsensitive>
<interfaceOnly>true</interfaceOnly>
<!--suppress UnresolvedMavenProperty -->
<additionalModelTypeAnnotations>@lombok.Builder @lombok.NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ public interface TransactionMapper {
@Mapping(target = "metadata.drep", source = "model.drep", qualifiedByName = "convertDRepFromRosetta")
Operation mapDRepDelegationToOperation(DRepDelegation model, OperationStatus status, int index);

@Mapping(target = "status", source = "status.status")
@Mapping(target = "type", constant = Constants.OPERATION_TYPE_POOL_GOVERNANCE_VOTE)
@Mapping(target = "operationIdentifier", source = "index", qualifiedByName = "OperationIdentifier")
@Mapping(target = "metadata.poolGovernanceVoteParams.governanceAction", source = "governanceVote.govActionId", qualifiedByName = "convertGovActionIdToRosetta")
@Mapping(target = "metadata.poolGovernanceVoteParams.poolCredential", source = "governanceVote.voter", qualifiedByName = "convertGovVoterToRosetta")
@Mapping(target = "metadata.poolGovernanceVoteParams.vote", source = "governanceVote.vote", qualifiedByName = "convertGovVoteToRosetta")
@Mapping(target = "metadata.poolGovernanceVoteParams.voteRationale", source = "governanceVote.voteRationale", qualifiedByName = "convertGovAnchorFromRosetta")
Operation mapGovernanceVoteToOperation(GovernanceVote governanceVote, OperationStatus status, int index);

@Mapping(target = "type", constant = Constants.INPUT)
@Mapping(target = "coinChange.coinAction", source = "model", qualifiedByName = "getCoinSpentAction")
@Mapping(target = "metadata", source = "model.amounts", qualifiedByName = "mapAmountsToOperationMetadataInput")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@
import lombok.RequiredArgsConstructor;

import org.springframework.stereotype.Component;
import com.bloxbean.cardano.client.transaction.spec.governance.Anchor;
import com.bloxbean.cardano.client.transaction.spec.governance.Vote;
import com.bloxbean.cardano.client.transaction.spec.governance.Voter;
import com.bloxbean.cardano.client.transaction.spec.governance.actions.GovActionId;
import com.bloxbean.cardano.yaci.core.model.certs.CertificateType;
import org.mapstruct.Named;
import org.openapitools.client.model.*;

import org.cardanofoundation.rosetta.api.account.model.domain.Amt;
import org.cardanofoundation.rosetta.api.account.model.domain.Utxo;
import org.cardanofoundation.rosetta.api.block.model.domain.DRepDelegation;
import org.cardanofoundation.rosetta.api.block.model.domain.GovernanceVote;
import org.cardanofoundation.rosetta.api.block.model.domain.StakeRegistration;
import org.cardanofoundation.rosetta.common.enumeration.OperationType;
import org.cardanofoundation.rosetta.common.mapper.DataMapper;
Expand All @@ -29,6 +34,46 @@ public class TransactionMapperUtils {

final ProtocolParamService protocolParamService;

@Named("convertGovAnchorFromRosetta")
public GovVoteRationaleParams convertGovAnchorFromRosetta(Anchor anchor) {
return GovernanceVote.convertFromRosetta(anchor);
}

@Named("convertGovVoteRationaleToRosetta")
public Anchor convertGovVoteRationaleToRosetta(GovVoteRationaleParams params) {
return GovernanceVote.convertToRosetta(params);
}

@Named("convertGovVoteToRosetta")
public GovVoteParams convertGovVoteFromRosetta(Vote vote) {
return GovernanceVote.convertFromRosetta(vote);
}

@Named("convertGovVoteToRosetta")
public Vote convertGovVoteToRosetta(GovVoteParams voteParams) {
return GovernanceVote.convertToRosetta(voteParams);
}

@Named("convertGovActionIdToRosetta")
public GovActionParams convertGovActionIdFromRosetta(GovActionId govActionId) {
return GovernanceVote.convertFromRosetta(govActionId);
}

@Named("convertGovActionIdToRosetta")
public GovActionId convertGovActionIdToRosetta(GovActionParams govActionIdParams) {
return GovernanceVote.convertToRosetta(govActionIdParams);
}

@Named("convertGovVoterToRosetta")
public PublicKey convertGovVoterToRosetta(Voter voter) {
return GovernanceVote.convertToRosetta(voter);
}

@Named("convertGovPoolCredentialFromRosetta")
public Voter convertGovVoterFromRosetta(PublicKey publicKey) {
return GovernanceVote.convertFromRosetta(publicKey);
}

@Named("convertDRepToRosetta")
public DRepDelegation.DRep convertDRepToRosetta(DRepParams dRepParams) {
return DRepDelegation.DRep.convertDRepToRosetta(dRepParams);
Expand Down Expand Up @@ -72,6 +117,7 @@ public OperationMetadata mapToOperationMetaData(boolean spent, List<Amt> amounts
.build()
)
);

return Objects.isNull(operationMetadata.getTokenBundle()) ? null : operationMetadata;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class BlockTx {
protected List<StakeRegistration> stakeRegistrations;
protected List<StakePoolDelegation> stakePoolDelegations;
protected List<DRepDelegation> dRepDelegations;
protected List<GovernanceVote> governanceVotes;
protected List<PoolRegistration> poolRegistrations;
protected List<PoolRetirement> poolRetirements;
protected List<Withdrawal> withdrawals;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package org.cardanofoundation.rosetta.api.block.model.domain;

import java.util.Optional;
import javax.annotation.Nullable;

import lombok.*;

import com.bloxbean.cardano.client.address.Credential;
import com.bloxbean.cardano.client.address.CredentialType;
import com.bloxbean.cardano.client.transaction.spec.governance.Anchor;
import com.bloxbean.cardano.client.transaction.spec.governance.Vote;
import com.bloxbean.cardano.client.transaction.spec.governance.Voter;
import com.bloxbean.cardano.client.transaction.spec.governance.actions.GovActionId;
import com.bloxbean.cardano.client.util.HexUtil;
import org.openapitools.client.model.*;

import org.cardanofoundation.rosetta.common.exception.ExceptionFactory;

import static com.bloxbean.cardano.client.transaction.spec.governance.VoterType.STAKING_POOL_KEY_HASH;
import static com.bloxbean.cardano.client.util.HexUtil.encodeHexString;
import static org.openapitools.client.model.CurveType.EDWARDS25519;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class GovernanceVote {

private GovActionId govActionId;
private Vote vote;
private Voter voter;

@Nullable
private Anchor voteRationale;

public static GovernanceVote convertToRosetta(PoolGovernanceVoteParams voteParams) {
GovernanceVoteBuilder governanceVoteBuilder = GovernanceVote.builder()
.govActionId(convertToRosetta(voteParams.getGovernanceAction()))
.voter(convertFromRosetta(voteParams.getPoolCredential())) // for now only support pool credential
.vote(convertToRosetta(voteParams.getVote()));

Optional.ofNullable(voteParams.getVoteRationale()).ifPresent(govVoteRationaleParams -> {
governanceVoteBuilder.voteRationale(convertToRosetta(govVoteRationaleParams));
});

return governanceVoteBuilder.build();
}

public static PoolGovernanceVoteParams convertFromRosetta(GovernanceVote governanceVote) {
PoolGovernanceVoteParams.PoolGovernanceVoteParamsBuilder builder = PoolGovernanceVoteParams.builder();

builder.governanceAction(convertFromRosetta(governanceVote.getGovActionId()));
builder.poolCredential(convertToRosetta(governanceVote.getVoter()));
builder.vote(convertFromRosetta(governanceVote.getVote()));

Optional.ofNullable(governanceVote.getVoteRationale()).ifPresent(anchor -> {
builder.voteRationale(convertFromRosetta(anchor));
});

return builder.build();
}

public static GovVoteRationaleParams convertFromRosetta(Anchor anchor) {
return GovVoteRationaleParams.builder()
.url(anchor.getAnchorUrl())
.dataHash(encodeHexString(anchor.getAnchorDataHash()))
.build();
}

public static Anchor convertToRosetta(GovVoteRationaleParams govAnchorParams) {
return Anchor.builder()
.anchorUrl(govAnchorParams.getUrl())
.anchorDataHash(HexUtil.decodeHexString(govAnchorParams.getDataHash()))
.build();
}

public static GovVoteParams convertFromRosetta(Vote vote) {
return switch (vote) {
case YES -> GovVoteParams.YES;
case NO -> GovVoteParams.NO;
case ABSTAIN -> GovVoteParams.ABSTAIN;
};
}

public static GovActionId convertToRosetta(GovActionParams govActionIdParams) {
return GovActionId.builder()
.govActionIndex(govActionIdParams.getIndex())
.transactionId(govActionIdParams.getTxId())
.build();
}

public static GovActionParams convertFromRosetta(GovActionId govActionId) {
return GovActionParams.builder()
.index(govActionId.getGovActionIndex())
.txId(govActionId.getTransactionId())
.build();
}

public static Voter convertFromRosetta(PublicKey poolCredential) {
Credential credential = Credential.fromKey(poolCredential.getHexBytes());

return new Voter(STAKING_POOL_KEY_HASH, credential);
}

public static PublicKey convertToRosetta(Voter voter) {
if (voter.getType() != STAKING_POOL_KEY_HASH) {
throw ExceptionFactory.governanceOnlyPoolVotingPossible();
}

if (voter.getCredential().getType() != CredentialType.Key) {
throw ExceptionFactory.governanceKeyHashOnlySupported();
}

byte[] credentialBytes = voter.getCredential().getBytes();

return new PublicKey(encodeHexString(credentialBytes), EDWARDS25519);
}

private static String convertFromRosetta(Credential credential) {
return encodeHexString(credential.getBytes());
}

public static Vote convertToRosetta(GovVoteParams voteParams) {
return switch (voteParams) {
case GovVoteParams.YES -> Vote.YES;
case GovVoteParams.NO -> Vote.NO;
case GovVoteParams.ABSTAIN -> Vote.ABSTAIN;
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -20,14 +21,16 @@
@NoArgsConstructor
public class ProcessOperations {

private ArrayList<TransactionInput> transactionInputs = new ArrayList<>();
private ArrayList<TransactionOutput> transactionOutputs = new ArrayList<>();
private ArrayList<Certificate> certificates = new ArrayList<>();
private ArrayList<Withdrawal> withdrawals = new ArrayList<>();
private ArrayList<String> addresses = new ArrayList<>();
private ArrayList<BigInteger> inputAmounts = new ArrayList<>();
private ArrayList<BigInteger> outputAmounts = new ArrayList<>();
private ArrayList<BigInteger> withdrawalAmounts = new ArrayList<>();
private List<TransactionInput> transactionInputs = new ArrayList<>();
private List<TransactionOutput> transactionOutputs = new ArrayList<>();
private List<Certificate> certificates = new ArrayList<>();
private List<Withdrawal> withdrawals = new ArrayList<>();
private List<String> addresses = new ArrayList<>();
private List<BigInteger> inputAmounts = new ArrayList<>();
private List<BigInteger> outputAmounts = new ArrayList<>();
private List<BigInteger> withdrawalAmounts = new ArrayList<>();
private List<GovernanceVote> governanceVotes = new ArrayList<>();

private double stakeKeyRegistrationsCount = 0.0;
private double stakeKeyDeRegistrationsCount = 0.0;
private double poolRegistrationsCount = 0.0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.cardanofoundation.rosetta.api.block.model.domain;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import lombok.AllArgsConstructor;
Expand All @@ -20,10 +20,11 @@
@Setter
public class ProcessOperationsReturn {

private ArrayList<TransactionInput> transactionInputs;
private ArrayList<TransactionOutput> transactionOutputs;
private ArrayList<Certificate> certificates;
private ArrayList<Withdrawal> withdrawals;
private List<TransactionInput> transactionInputs;
private List<TransactionOutput> transactionOutputs;
private List<Certificate> certificates;
private List<GovernanceVote> governanceVotes;
private List<Withdrawal> withdrawals;
private Set<String> addresses;
private Long fee;
private AuxiliaryData voteRegistrationMetadata;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,12 @@ void populateTransaction(BlockTx transaction,
.filter(tx -> tx.getTxHash().equals(transaction.getHash()))
.map(transactionMapper::mapEntityToPoolRetirement)
.toList());
// TODO dRep Vote Delegations
//transaction.setDRepDelegations(fetched.delegations

// TODO governance votes
//transaction.setGovernanceVotes(fetched.);

}

private void populateUtxo(Utxo utxo, Map<UtxoKey, AddressUtxoEntity> utxoMap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ public enum AddressType {
ENTERPRISE("Enterprise"),
BASE("Base"),
REWARD("Reward"),
POOL_KEY_HASH("Pool_Hash");
POOL_KEY_HASH("Pool_Hash"),
POOL_KEY_KASH_BECH_32("Pool_Hash_Bech32");

private final String value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ List<SigningPayload> constructPayloadsForTransactionBody(String transactionBodyH

String extractTransactionIfNeeded(String txWithExtraData);

String getCardanoAddress(AddressType addressType, PublicKey stakingCredential,
PublicKey publicKey, NetworkEnum networkEnum);
String getCardanoAddress(AddressType addressType,
PublicKey stakingCredential,
PublicKey publicKey,
NetworkEnum networkEnum);

Map<String, Double> getDepositsSumMap(DepositParameters depositParameters, ProcessOperations result, double refundsSum);

Expand Down
Loading