Merge pull request #7092 from stejbac/speed-up-vote-result-view-load

Speed up Vote Result display, cycle list item selection & JSON export
This commit is contained in:
Alejandro García 2024-05-09 18:57:53 +00:00 committed by GitHub
commit 0b85e0615d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 63 additions and 122 deletions

View File

@ -48,7 +48,6 @@ import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
@ -60,14 +59,12 @@ import org.bitcoinj.wallet.SendRequest;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -95,6 +92,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
private final DaoStateService daoStateService;
private final UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService;
private final List<Transaction> walletTransactions = new ArrayList<>();
private Map<String, Transaction> walletTransactionsById;
private final CopyOnWriteArraySet<BsqBalanceListener> bsqBalanceListeners = new CopyOnWriteArraySet<>();
private final List<WalletTransactionsChangeListener> walletTransactionsChangeListeners = new ArrayList<>();
private boolean updateBsqWalletTransactionsPending;
@ -233,7 +231,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
private void updateBsqBalance() {
long ts = System.currentTimeMillis();
unverifiedBalance = Coin.valueOf(
getTransactions(false).stream()
walletTransactions.stream()
.filter(tx -> tx.getConfidence().getConfidenceType() == PENDING)
.mapToLong(tx -> {
// Sum up outputs into BSQ wallet and subtract the inputs using lockup or unlocking
@ -270,7 +268,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
.sum()
);
Set<String> confirmedTxIdSet = getTransactions(false).stream()
Set<String> confirmedTxIdSet = walletTransactions.stream()
.filter(tx -> tx.getConfidence().getConfidenceType() == BUILDING)
.map(Transaction::getTxId)
.map(Sha256Hash::toString)
@ -343,13 +341,15 @@ public class BsqWalletService extends WalletService implements DaoStateListener
// BSQ TransactionOutputs and Transactions
///////////////////////////////////////////////////////////////////////////////////////////
// not thread safe - call only from user thread
public List<Transaction> getClonedWalletTransactions() {
return new ArrayList<>(walletTransactions);
}
// not thread safe - call only from user thread
public Stream<Transaction> getPendingWalletTransactionsStream() {
return walletTransactions.stream()
.filter(transaction -> transaction.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING);
.filter(transaction -> transaction.getConfidence().getConfidenceType() == PENDING);
}
private void updateBsqWalletTransactions() {
@ -363,6 +363,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
UserThread.runAfter(() -> {
walletTransactions.clear();
walletTransactions.addAll(getTransactions(false));
walletTransactionsById = null;
walletTransactionsChangeListeners.forEach(WalletTransactionsChangeListener::onWalletTransactionsChange);
updateBsqBalance();
updateBsqWalletTransactionsPending = false;
@ -371,37 +372,6 @@ public class BsqWalletService extends WalletService implements DaoStateListener
}
}
private Set<Transaction> getBsqWalletTransactions() {
return getTransactions(false).stream()
.filter(transaction -> transaction.getConfidence().getConfidenceType() == PENDING ||
daoStateService.containsTx(transaction.getTxId().toString()))
.collect(Collectors.toSet());
}
public Set<Transaction> getUnverifiedBsqTransactions() {
Set<Transaction> bsqWalletTransactions = getBsqWalletTransactions();
Set<Transaction> walletTxs = new HashSet<>(getTransactions(false));
checkArgument(walletTxs.size() >= bsqWalletTransactions.size(),
"We cannot have more txsWithOutputsFoundInBsqTxo than walletTxs");
if (walletTxs.size() == bsqWalletTransactions.size()) {
// As expected
return new HashSet<>();
} else {
Map<String, Transaction> map = walletTxs.stream()
.collect(Collectors.toMap(t -> t.getTxId().toString(), Function.identity()));
Set<String> walletTxIds = walletTxs.stream()
.map(Transaction::getTxId).map(Sha256Hash::toString).collect(Collectors.toSet());
Set<String> bsqTxIds = bsqWalletTransactions.stream()
.map(Transaction::getTxId).map(Sha256Hash::toString).collect(Collectors.toSet());
walletTxIds.stream()
.filter(bsqTxIds::contains)
.forEach(map::remove);
return new HashSet<>(map.values());
}
}
@Override
public Coin getValueSentFromMeForTransaction(Transaction transaction) throws ScriptException {
Coin result = Coin.ZERO;
@ -414,7 +384,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
// We grab the parent tx of the connected output
final Transaction parentTransaction = connectedOutput.getParentTransaction();
final boolean isConfirmed = parentTransaction != null &&
parentTransaction.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING;
parentTransaction.getConfidence().getConfidenceType() == BUILDING;
if (connectedOutput.isMineOrWatched(wallet)) {
if (isConfirmed) {
// We lookup if we have a BSQ tx matching the parent tx
@ -455,7 +425,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
for (int i = 0; i < transaction.getOutputs().size(); i++) {
TransactionOutput output = transaction.getOutputs().get(i);
final boolean isConfirmed = output.getParentTransaction() != null &&
output.getParentTransaction().getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING;
output.getParentTransaction().getConfidence().getConfidenceType() == BUILDING;
if (output.isMineOrWatched(wallet)) {
if (isConfirmed) {
if (txOptional.isPresent()) {
@ -484,8 +454,13 @@ public class BsqWalletService extends WalletService implements DaoStateListener
return result;
}
// not thread safe - call only from user thread
public Optional<Transaction> isWalletTransaction(String txId) {
return walletTransactions.stream().filter(e -> e.getTxId().toString().equals(txId)).findAny();
if (walletTransactionsById == null) {
walletTransactionsById = walletTransactions.stream()
.collect(Collectors.toMap(tx -> tx.getTxId().toString(), tx -> tx));
}
return Optional.ofNullable(walletTransactionsById.get(txId));
}

View File

@ -72,7 +72,6 @@ import org.bitcoinj.wallet.listeners.WalletReorganizeEventListener;
import javax.inject.Inject;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Multiset;
@ -88,7 +87,6 @@ import org.bouncycastle.crypto.params.KeyParameter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
@ -107,6 +105,9 @@ import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.BUILDING;
import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.DEAD;
import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.PENDING;
/**
* Abstract base class for BTC and BSQ wallet. Provides all non-trade specific functionality.
@ -124,7 +125,6 @@ public abstract class WalletService {
private final WalletChangeEventListener cacheInvalidationListener;
private final AtomicReference<Multiset<Address>> txOutputAddressCache = new AtomicReference<>();
private final AtomicReference<SetMultimap<Address, Transaction>> addressToMatchingTxSetCache = new AtomicReference<>();
private final AtomicReference<Map<Sha256Hash, Transaction>> txByIdCache = new AtomicReference<>();
@Getter
protected Wallet wallet;
@Getter
@ -150,7 +150,6 @@ public abstract class WalletService {
cacheInvalidationListener = wallet -> {
txOutputAddressCache.set(null);
addressToMatchingTxSetCache.set(null);
txByIdCache.set(null);
};
}
@ -337,8 +336,9 @@ public abstract class WalletService {
txIn = partialTx.getInput(index);
if (txIn.getConnectedOutput() != null) {
// If we don't have a sig we don't do the check to avoid error reports of failed sig checks
final List<ScriptChunk> chunks = txIn.getConnectedOutput().getScriptPubKey().getChunks();
if (!chunks.isEmpty() && chunks.get(0).data != null && chunks.get(0).data.length > 0) {
List<ScriptChunk> chunks = txIn.getConnectedOutput().getScriptPubKey().getChunks();
byte[] pushData;
if (!chunks.isEmpty() && (pushData = chunks.get(0).data) != null && pushData.length > 0) {
try {
// We assume if it's already signed, it's hopefully got a SIGHASH type that will not invalidate when
// we sign missing pieces (to check this would require either assuming any signatures are signing
@ -460,9 +460,8 @@ public abstract class WalletService {
transactionConfidenceList.addAll(transactions.stream()
.map(tx -> getTransactionConfidence(tx, address))
.filter(Objects::nonNull)
.filter(con -> con.getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING ||
(con.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING &&
con.getAppearedAtChainHeight() > targetHeight))
.filter(con -> con.getConfidenceType() == PENDING ||
(con.getConfidenceType() == BUILDING && con.getAppearedAtChainHeight() > targetHeight))
.collect(Collectors.toList()));
}
return getMostRecentConfidence(transactionConfidenceList);
@ -486,23 +485,16 @@ public abstract class WalletService {
@Nullable
public TransactionConfidence getConfidenceForTxId(@Nullable String txId) {
if (wallet != null && txId != null && !txId.isEmpty()) {
Transaction tx = getTxByIdMap().get(Sha256Hash.wrap(txId));
if (tx != null) {
return tx.getConfidence();
Sha256Hash hash = Sha256Hash.wrap(txId);
Transaction tx = getTransaction(hash);
TransactionConfidence confidence;
if (tx != null && (confidence = tx.getConfidence()).getConfidenceType() != DEAD) {
return confidence;
}
}
return null;
}
private Map<Sha256Hash, Transaction> getTxByIdMap() {
return txByIdCache.updateAndGet(map -> map != null ? map : computeTxByIdMap());
}
private Map<Sha256Hash, Transaction> computeTxByIdMap() {
return wallet.getTransactions(false).stream()
.collect(ImmutableMap.toImmutableMap(Transaction::getTxId, tx -> tx));
}
@Nullable
private TransactionConfidence getTransactionConfidence(Transaction tx, Address address) {
List<TransactionConfidence> transactionConfidenceList = getOutputsWithConnectedOutputs(tx).stream()
@ -538,11 +530,9 @@ public abstract class WalletService {
TransactionConfidence transactionConfidence = null;
for (TransactionConfidence confidence : transactionConfidenceList) {
if (confidence != null) {
if (transactionConfidence == null ||
confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.PENDING) ||
(confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING) &&
transactionConfidence.getConfidenceType().equals(
TransactionConfidence.ConfidenceType.BUILDING) &&
if (transactionConfidence == null || confidence.getConfidenceType() == PENDING ||
(confidence.getConfidenceType() == BUILDING &&
transactionConfidence.getConfidenceType() == BUILDING &&
confidence.getDepthInBlocks() < transactionConfidence.getDepthInBlocks())) {
transactionConfidence = confidence;
}
@ -632,7 +622,7 @@ public abstract class WalletService {
for (TransactionOutput transactionOutput : proposedTransaction.getOutputs()) {
if (transactionOutput.getValue().isLessThan(Restrictions.getMinNonDustOutput())) {
dust = dust.add(transactionOutput.getValue());
log.info("Dust TXO = {}", transactionOutput.toString());
log.info("Dust TXO = {}", transactionOutput);
}
}
return dust;
@ -662,13 +652,13 @@ public abstract class WalletService {
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() {
@Override
public void onSuccess(Transaction result) {
log.info("emptyBtcWallet onSuccess Transaction=" + result);
log.info("emptyBtcWallet onSuccess Transaction={}", result);
resultHandler.handleResult();
}
@Override
public void onFailure(@NotNull Throwable t) {
log.error("emptyBtcWallet onFailure " + t.toString());
log.error("emptyBtcWallet onFailure " + t);
errorMessageHandler.handleErrorMessage(t.getMessage());
}
}, MoreExecutors.directExecutor());
@ -688,7 +678,7 @@ public abstract class WalletService {
}
public int getBestChainHeight() {
final BlockChain chain = walletsSetup.getChain();
BlockChain chain = walletsSetup.getChain();
return isWalletReady() && chain != null ? chain.getBestChainHeight() : 0;
}
@ -714,13 +704,13 @@ public abstract class WalletService {
}
public void addNewBestBlockListener(NewBestBlockListener listener) {
final BlockChain chain = walletsSetup.getChain();
BlockChain chain = walletsSetup.getChain();
if (isWalletReady() && chain != null)
chain.addNewBestBlockListener(listener);
}
public void removeNewBestBlockListener(NewBestBlockListener listener) {
final BlockChain chain = walletsSetup.getChain();
BlockChain chain = walletsSetup.getChain();
if (isWalletReady() && chain != null)
chain.removeNewBestBlockListener(listener);
}
@ -865,7 +855,7 @@ public abstract class WalletService {
/**
* @param serializedTransaction The serialized transaction to be added to the wallet
* @return The transaction we added to the wallet, which is different as the one we passed as argument!
* @throws VerificationException
* @throws VerificationException if the transaction could not be parsed or fails sanity checks
*/
public static Transaction maybeAddTxToWallet(byte[] serializedTransaction,
Wallet wallet,

View File

@ -40,7 +40,6 @@ import org.bitcoinj.core.Coin;
import de.jensd.fx.fontawesome.AwesomeIcon;
import javafx.scene.control.Label;
import javafx.scene.control.TableRow;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
@ -51,18 +50,14 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
public class ProposalListItem {
@Getter
private EvaluatedProposal evaluatedProposal;
private final EvaluatedProposal evaluatedProposal;
@Getter
private final Proposal proposal;
private final Vote vote;
private final boolean isMyBallotIncluded;
private final BsqFormatter bsqFormatter;
private TableRow tableRow;
ProposalListItem(EvaluatedProposal evaluatedProposal, Ballot ballot, boolean isMyBallotIncluded,
BsqFormatter bsqFormatter) {
this.evaluatedProposal = evaluatedProposal;
@ -104,17 +99,6 @@ public class ProposalListItem {
return myVoteIcon;
}
public void setTableRow(TableRow tableRow) {
this.tableRow = tableRow;
}
public void resetTableRow() {
if (tableRow != null) {
tableRow.setStyle(null);
tableRow.requestLayout();
}
}
public String getProposalOwnerName() {
return evaluatedProposal.getProposal().getName();
}

View File

@ -148,6 +148,7 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
private ProposalListItem selectedProposalListItem;
private boolean isVoteIncludedInResult;
private final Set<Cycle> cyclesAdded = new HashSet<>();
private Map<String, Ballot> ballotByProposalTxIdMap;
private boolean hasCalculatedResult = false;
@ -205,7 +206,7 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
if (daoStateService.isParseBlockChainComplete()) {
checkForResultPhase(daoStateService.getChainHeight());
fillCycleList();
fillCycleListAndBallotMap();
}
exportButton.setOnAction(event -> {
@ -243,7 +244,7 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
@Override
public void onParseBlockCompleteAfterBatchProcessing(Block block) {
checkForResultPhase(daoStateService.getChainHeight());
fillCycleList();
fillCycleListAndBallotMap();
}
private void checkForResultPhase(int chainHeight) {
@ -283,16 +284,9 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
resultsOfCycle = item.getResultsOfCycle();
// Check if my vote is included in result
isVoteIncludedInResult = false;
resultsOfCycle.getEvaluatedProposals().forEach(evProposal -> resultsOfCycle.getDecryptedVotesForCycle()
.forEach(decryptedBallotsWithMerits -> {
// Iterate through all included votes to see if any of those are ours
if (!isVoteIncludedInResult) {
isVoteIncludedInResult = bsqWalletService.isWalletTransaction(decryptedBallotsWithMerits
.getVoteRevealTxId()).isPresent();
}
}));
isVoteIncludedInResult = resultsOfCycle.getDecryptedVotesForCycle().stream()
.anyMatch(decryptedBallotsWithMerits -> bsqWalletService.isWalletTransaction(decryptedBallotsWithMerits
.getVoteRevealTxId()).isPresent());
maybeShowVoteResultErrors(item.getResultsOfCycle().getCycle());
createProposalsTable();
@ -359,11 +353,8 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
if (selectedProposalListItem != null) {
EvaluatedProposal evaluatedProposal = selectedProposalListItem.getEvaluatedProposal();
Optional<Ballot> optionalBallot = daoFacade.getAllValidBallots().stream()
.filter(ballot -> ballot.getTxId().equals(evaluatedProposal.getProposalTxId()))
.findAny();
Ballot ballot = ballotByProposalTxIdMap.get(evaluatedProposal.getProposalTxId());
Ballot ballot = optionalBallot.orElse(null);
voteListItemList.clear();
resultsOfCycle.getEvaluatedProposals().stream()
.filter(evProposal -> evProposal.getProposal().equals(selectedProposalListItem.getEvaluatedProposal().getProposal()))
@ -389,16 +380,19 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
// Fill lists: Cycle
///////////////////////////////////////////////////////////////////////////////////////////
private void fillCycleList() {
private void fillCycleListAndBallotMap() {
// At data creation we delay a bit so that the UI has a chance to display the placeholder.
if (cyclesAdded.isEmpty()) {
UserThread.runAfter(this::doFillCycleList, 50, TimeUnit.MILLISECONDS);
UserThread.runAfter(this::doFillCycleListAndBallotMap, 50, TimeUnit.MILLISECONDS);
} else {
doFillCycleList();
doFillCycleListAndBallotMap();
}
}
private void doFillCycleList() {
private void doFillCycleListAndBallotMap() {
ballotByProposalTxIdMap = daoFacade.getAllValidBallots().stream()
.collect(Collectors.toMap(Ballot::getTxId, ballot -> ballot));
// Creating our data structure is a bit expensive so we ensure to only create the CycleListItems once.
daoStateService.getCycles().stream()
.filter(cycle -> !cyclesAdded.contains(cycle))
@ -412,7 +406,7 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
.collect(Collectors.toList());
AtomicLong stakeAndMerit = new AtomicLong();
List<DecryptedBallotsWithMerits> decryptedVotesForCycle = daoStateService.getDecryptedBallotsWithMeritsList().stream()
List<DecryptedBallotsWithMerits> decryptedVotesForCycle = daoStateService.getDecryptedBallotsWithMeritsList().parallelStream()
.filter(decryptedBallotsWithMerits -> cycleService.isTxInCycle(cycle, decryptedBallotsWithMerits.getBlindVoteTxId()))
.filter(decryptedBallotsWithMerits -> cycleService.isTxInCycle(cycle, decryptedBallotsWithMerits.getVoteRevealTxId()))
.peek(decryptedBallotsWithMerits -> stakeAndMerit.getAndAdd(decryptedBallotsWithMerits.getStake() + decryptedBallotsWithMerits.getMerit(daoStateService)))
@ -464,8 +458,6 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
cyclesTableView.setItems(sortedCycleListItemList);
sortedCycleListItemList.comparatorProperty().bind(cyclesTableView.comparatorProperty());
}
@ -495,11 +487,8 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
proposalsTableView.setItems(sortedProposalList);
sortedProposalList.comparatorProperty().bind(proposalsTableView.comparatorProperty());
proposalList.forEach(ProposalListItem::resetTableRow);
proposalList.clear();
Map<String, Ballot> ballotByProposalTxIdMap = daoFacade.getAllValidBallots().stream()
.collect(Collectors.toMap(Ballot::getTxId, ballot -> ballot));
proposalList.setAll(resultsOfCycle.getEvaluatedProposals().stream()
.filter(evaluatedProposal -> {
boolean containsKey = ballotByProposalTxIdMap.containsKey(evaluatedProposal.getProposalTxId());
@ -893,6 +882,12 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
cycleJson.addProperty("totalAcceptedVotes", cycleListItem.getResultsOfCycle().getNumAcceptedVotes());
cycleJson.addProperty("totalRejectedVotes", cycleListItem.getResultsOfCycle().getNumRejectedVotes());
List<DecryptedBallotsWithMerits> decryptedVotesForCycle = cycleListItem.getResultsOfCycle().getDecryptedVotesForCycle();
// Make sure the votes are sorted so we can easier compare json files from different users
decryptedVotesForCycle.sort(Comparator.comparing(DecryptedBallotsWithMerits::getBlindVoteTxId));
Map<String, Long> meritStakeMap = decryptedVotesForCycle.stream()
.collect(Collectors.toMap(DecryptedBallotsWithMerits::getBlindVoteTxId, d -> d.getMerit(daoStateService)));
JsonArray proposalsArray = new JsonArray();
List<EvaluatedProposal> evaluatedProposals = cycleListItem.getResultsOfCycle().getEvaluatedProposals();
evaluatedProposals.sort(Comparator.comparingLong(o -> o.getProposal().getCreationDate()));
@ -984,18 +979,15 @@ public class VoteResultView extends ActivatableView<GridPane, Void> implements D
evaluatedProposals.stream()
.filter(evaluatedProposal -> evaluatedProposal.getProposal().equals(proposal))
.forEach(evaluatedProposal -> {
List<DecryptedBallotsWithMerits> decryptedVotesForCycle = cycleListItem.getResultsOfCycle().getDecryptedVotesForCycle();
// Make sure the votes are sorted so we can easier compare json files from different users
decryptedVotesForCycle.sort(Comparator.comparing(DecryptedBallotsWithMerits::getBlindVoteTxId));
decryptedVotesForCycle.forEach(decryptedBallotsWithMerits -> {
JsonObject voteJson = new JsonObject();
// Domain data of decryptedBallotsWithMerits
voteJson.addProperty("hashOfBlindVoteList", Utilities.bytesAsHexString(decryptedBallotsWithMerits.getHashOfBlindVoteList()));
voteJson.addProperty("blindVoteTxId", decryptedBallotsWithMerits.getBlindVoteTxId());
voteJson.addProperty("voteRevealTxId", decryptedBallotsWithMerits.getVoteRevealTxId());
voteJson.addProperty("stake", decryptedBallotsWithMerits.getStake());
voteJson.addProperty("voteWeight", decryptedBallotsWithMerits.getMerit(daoStateService));
long stake = decryptedBallotsWithMerits.getStake();
voteJson.addProperty("stake", stake);
voteJson.addProperty("voteWeight", stake + meritStakeMap.get(decryptedBallotsWithMerits.getBlindVoteTxId()));
String voteResult = decryptedBallotsWithMerits.getVote(evaluatedProp.getProposalTxId())
.map(vote -> vote.isAccepted() ? "Accepted" : "Rejected")
.orElse("Ignored");