Skip to content

Commit

Permalink
feat: withdrawable rewards should be available on Rosetta's account/b…
Browse files Browse the repository at this point in the history
…alance endpoint for a given stake address.
  • Loading branch information
Mateusz Czeladka committed Jan 31, 2025
1 parent 28bcfa6 commit 830ccda
Show file tree
Hide file tree
Showing 24 changed files with 632 additions and 270 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.cardanofoundation.rosetta.api.account.mapper;

import org.cardanofoundation.rosetta.api.account.model.domain.AddressBalance;
import org.cardanofoundation.rosetta.client.domain.StakeAccountInfo;

public interface StakeAccountInfoToAddressBalanceMapper {

AddressBalance convertToAddressBalance(StakeAccountInfo stakeAccountInfo, Long number);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.cardanofoundation.rosetta.api.account.mapper;

import lombok.extern.slf4j.Slf4j;
import org.cardanofoundation.rosetta.api.account.model.domain.AddressBalance;
import org.cardanofoundation.rosetta.client.domain.StakeAccountInfo;
import org.springframework.stereotype.Service;

import static org.cardanofoundation.rosetta.common.util.Constants.LOVELACE;

@Service
@Slf4j
public class StakeAccountInfoToAddressBalanceMapperImpl implements StakeAccountInfoToAddressBalanceMapper {

@Override
public AddressBalance convertToAddressBalance(StakeAccountInfo stakeAccountInfo, Long number) {
return AddressBalance.builder()
.address(stakeAccountInfo.getStakeAddress())
.unit(LOVELACE)
.quantity(stakeAccountInfo.getWithdrawableAmount())
.number(number)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -1,35 +1,30 @@
package org.cardanofoundation.rosetta.api.account.service;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Service;
import org.openapitools.client.model.AccountBalanceRequest;
import org.openapitools.client.model.AccountBalanceResponse;
import org.openapitools.client.model.AccountCoinsRequest;
import org.openapitools.client.model.AccountCoinsResponse;
import org.openapitools.client.model.Amount;
import org.openapitools.client.model.Currency;
import org.openapitools.client.model.CurrencyMetadata;
import org.openapitools.client.model.PartialBlockIdentifier;

import org.cardanofoundation.rosetta.api.account.mapper.AccountMapper;
import org.cardanofoundation.rosetta.api.account.mapper.StakeAccountInfoToAddressBalanceMapperImpl;
import org.cardanofoundation.rosetta.api.account.model.domain.AddressBalance;
import org.cardanofoundation.rosetta.api.account.model.domain.Utxo;
import org.cardanofoundation.rosetta.api.block.model.domain.BlockIdentifierExtended;
import org.cardanofoundation.rosetta.api.block.service.LedgerBlockService;
import org.cardanofoundation.rosetta.client.YaciHttpGateway;
import org.cardanofoundation.rosetta.client.domain.StakeAccountInfo;
import org.cardanofoundation.rosetta.common.exception.ExceptionFactory;
import org.cardanofoundation.rosetta.common.util.CardanoAddressUtils;
import org.cardanofoundation.rosetta.common.util.Constants;
import org.openapitools.client.model.*;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;

import static org.cardanofoundation.rosetta.common.exception.ExceptionFactory.invalidPolicyIdError;
import static org.cardanofoundation.rosetta.common.exception.ExceptionFactory.invalidTokenNameError;
import static org.cardanofoundation.rosetta.common.util.CardanoAddressUtils.isStakeAddress;
import static org.cardanofoundation.rosetta.common.util.Formatters.isEmptyHexString;


Expand All @@ -39,20 +34,21 @@
public class AccountServiceImpl implements AccountService {

private static final Pattern TOKEN_NAME_VALIDATION = Pattern.compile(
"^[0-9a-fA-F]{0," + Constants.ASSET_NAME_LENGTH + "}$");
"^[0-9a-fA-F]{0," + Constants.ASSET_NAME_LENGTH + "}$");
private static final Pattern POLICY_ID_VALIDATION = Pattern.compile(
"^[0-9a-fA-F]{" + Constants.POLICY_ID_LENGTH + "}$");
"^[0-9a-fA-F]{" + Constants.POLICY_ID_LENGTH + "}$");

private final LedgerAccountService ledgerAccountService;
private final LedgerBlockService ledgerBlockService;
private final AccountMapper accountMapper;
private final YaciHttpGateway yaciHttpGateway;
private final StakeAccountInfoToAddressBalanceMapperImpl stakeAccountInfoToAddressBalanceMapperService;

@Override
public AccountBalanceResponse getAccountBalance(AccountBalanceRequest accountBalanceRequest) {


Long index = null;
String hash = null;

String accountAddress = accountBalanceRequest.getAccountIdentifier().getAddress();
CardanoAddressUtils.verifyAddress(accountAddress);

Expand All @@ -65,7 +61,6 @@ public AccountBalanceResponse getAccountBalance(AccountBalanceRequest accountBal
}

return findBalanceDataByAddressAndBlock(accountAddress, index, hash, accountBalanceRequest.getCurrencies());

}

@Override
Expand All @@ -87,44 +82,51 @@ public AccountCoinsResponse getAccountCoins(AccountCoinsRequest accountCoinsRequ
BlockIdentifierExtended latestBlock = ledgerBlockService.findLatestBlockIdentifier();
log.debug("[accountCoins] Latest block is {}", latestBlock);
List<Utxo> utxos = ledgerAccountService.findUtxoByAddressAndCurrency(accountAddress,
currenciesRequested);
currenciesRequested);
log.debug("[accountCoins] found {} Utxos for Address {}", utxos.size(), accountAddress);
return accountMapper.mapToAccountCoinsResponse(latestBlock, utxos);
}

private AccountBalanceResponse findBalanceDataByAddressAndBlock(String address, Long number,
String hash, List<Currency> currencies) {

private AccountBalanceResponse findBalanceDataByAddressAndBlock(String address,
Long number,
String hash,
List<Currency> currencies) {
return findBlockOrLast(number, hash)
.map(blockDto -> {
log.info("Looking for utxos for address {} and block {}",
address,
blockDto.getHash());
List<AddressBalance> balances;
if(CardanoAddressUtils.isStakeAddress(address)) {
balances = ledgerAccountService.findBalanceByStakeAddressAndBlock(address, blockDto.getNumber());
} else {
balances = ledgerAccountService.findBalanceByAddressAndBlock(address, blockDto.getNumber());
}
AccountBalanceResponse accountBalanceResponse = accountMapper.mapToAccountBalanceResponse(
blockDto, balances);
if (Objects.nonNull(currencies) && !currencies.isEmpty()) {
validateCurrencies(currencies);
List<Amount> accountBalanceResponseAmounts = accountBalanceResponse.getBalances();
accountBalanceResponseAmounts.removeIf(b -> currencies.stream().noneMatch(c -> c.getSymbol().equals(b.getCurrency().getSymbol())));
accountBalanceResponse.setBalances(accountBalanceResponseAmounts);
}
return accountBalanceResponse;
})
.orElseThrow(ExceptionFactory::blockNotFoundException);
.map(blockDto -> {
log.info("Looking for utxos for address {} and block {}",
address,
blockDto.getHash()
);

List<AddressBalance> balances;
if (isStakeAddress(address)) {
StakeAccountInfo stakeAccountInfo = yaciHttpGateway.getStakeAccountInfo(address);

balances = List.of(stakeAccountInfoToAddressBalanceMapperService.convertToAddressBalance(stakeAccountInfo, blockDto.getNumber()));
} else {
balances = ledgerAccountService.findBalanceByAddressAndBlock(address, blockDto.getNumber());
}

AccountBalanceResponse accountBalanceResponse = accountMapper.mapToAccountBalanceResponse(blockDto, balances);

if (Objects.nonNull(currencies) && !currencies.isEmpty()) {
validateCurrencies(currencies);
List<Amount> accountBalanceResponseAmounts = accountBalanceResponse.getBalances();
accountBalanceResponseAmounts.removeIf(b -> currencies.stream().noneMatch(c -> c.getSymbol().equals(b.getCurrency().getSymbol())));
accountBalanceResponse.setBalances(accountBalanceResponseAmounts);
}

return accountBalanceResponse;
})
.orElseThrow(ExceptionFactory::blockNotFoundException);
}

private Optional<BlockIdentifierExtended> findBlockOrLast(Long number, String hash) {
if (number != null || hash != null) {
return ledgerBlockService.findBlockIdentifier(number, hash);
} else {
return Optional.of(ledgerBlockService.findLatestBlockIdentifier());
}

return Optional.of(ledgerBlockService.findLatestBlockIdentifier());
}

private void validateCurrencies(List<Currency> currencies) {
Expand All @@ -135,8 +137,9 @@ private void validateCurrencies(List<Currency> currencies) {
throw invalidTokenNameError("Given name is " + symbol);
}
if (!symbol.equals(Constants.ADA)
&& (metadata == null || !isPolicyIdValid(String.valueOf(metadata.getPolicyId())))) {
&& (metadata == null || !isPolicyIdValid(String.valueOf(metadata.getPolicyId())))) {
String policyId = metadata == null ? null : metadata.getPolicyId();

throw invalidPolicyIdError("Given policy id is " + policyId);
}
}
Expand All @@ -152,8 +155,8 @@ private boolean isPolicyIdValid(String policyId) {

private List<Currency> filterRequestedCurrencies(List<Currency> currencies) {
boolean isAdaAbsent = Optional.ofNullable(currencies)
.map(c -> c.stream().map(Currency::getSymbol).noneMatch(Constants.ADA::equals))
.orElse(false);
.map(c -> c.stream().map(Currency::getSymbol).noneMatch(Constants.ADA::equals))
.orElse(false);
return isAdaAbsent ? currencies : Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.cardanofoundation.rosetta.api.account.service;

import java.math.BigInteger;
import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -37,11 +39,16 @@ public List<AddressBalance> findBalanceByAddressAndBlock(String address, Long nu
}

@Override
public List<AddressBalance> findBalanceByStakeAddressAndBlock(String stakeAddress,
Long number) {
@Deprecated // dead code???, only used in tests
public List<AddressBalance> findBalanceByStakeAddressAndBlock(
String stakeAddress,
Long number) {

log.debug("Finding balance for Stakeaddress {} at block {}", stakeAddress, number);

List<AddressUtxoEntity> unspendUtxosByAddressAndBlock = addressUtxoRepository.findUnspentUtxosByStakeAddressAndBlock(
stakeAddress, number);

return mapAndGroupAddressUtxoEntityToAddressBalance(unspendUtxosByAddressAndBlock);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,13 @@
package org.cardanofoundation.rosetta.api.network.service;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
import com.bloxbean.cardano.client.common.model.Network;
import com.bloxbean.cardano.client.common.model.Networks;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.parser.OpenAPIV3Parser;
import org.openapitools.client.model.Allow;
import org.openapitools.client.model.Error;
import org.openapitools.client.model.MetadataRequest;
import org.openapitools.client.model.NetworkIdentifier;
import org.openapitools.client.model.NetworkListResponse;
import org.openapitools.client.model.NetworkOptionsResponse;
import org.openapitools.client.model.NetworkRequest;
import org.openapitools.client.model.NetworkStatusResponse;
import org.openapitools.client.model.OperationStatus;
import org.openapitools.client.model.Peer;
import org.openapitools.client.model.Version;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.cardanofoundation.rosetta.api.block.model.domain.BlockIdentifierExtended;
import org.cardanofoundation.rosetta.api.block.model.domain.NetworkStatus;
import org.cardanofoundation.rosetta.api.block.service.LedgerBlockService;
Expand All @@ -45,6 +19,16 @@
import org.cardanofoundation.rosetta.common.util.FileUtils;
import org.cardanofoundation.rosetta.common.util.RosettaConstants;
import org.cardanofoundation.rosetta.config.RosettaConfig;
import org.openapitools.client.model.Error;
import org.openapitools.client.model.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

@Service
@Slf4j
Expand All @@ -56,6 +40,7 @@ public class NetworkServiceImpl implements NetworkService {
private final NetworkMapper networkMapper;
private final TopologyConfigService topologyConfigService;
private final ResourceLoader resourceLoader;
private final ObjectMapper objectMapper;

private Integer cachedMagicNumber;

Expand Down Expand Up @@ -215,7 +200,7 @@ public Integer getNetworkMagic() {
private Map<String, Object> loadGenesisShelleyConfig() {
try {
String content = FileUtils.fileReader(genesisShelleyPath);
return new ObjectMapper().readValue(content, new TypeReference<Map<String, Object>>() {});
return objectMapper.readValue(content, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
throw ExceptionFactory.configNotFoundException(genesisShelleyPath);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
package org.cardanofoundation.rosetta.api.network.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.PostConstruct;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.openapitools.client.model.Peer;

import org.cardanofoundation.rosetta.api.network.model.Producer;
import org.cardanofoundation.rosetta.api.network.model.PublicRoot;
import org.cardanofoundation.rosetta.api.network.model.TopologyConfig;
import org.cardanofoundation.rosetta.common.exception.ExceptionFactory;
import org.cardanofoundation.rosetta.common.util.FileUtils;
import org.openapitools.client.model.Peer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

@Service
@Slf4j
Expand All @@ -29,6 +27,7 @@ public class TopologyConfigServiceImpl implements TopologyConfigService {
@Value("${cardano.rosetta.TOPOLOGY_FILEPATH}")
private String topologyFilepath;
private List<Peer> cachedPeers;
private final ObjectMapper objectMapper;

@PostConstruct
public void init() {
Expand Down Expand Up @@ -67,9 +66,8 @@ private List<Producer> getPublicRoots(List<PublicRoot> publicRoots) {

private TopologyConfig loadTopologyConfig() {
try {
ObjectMapper mapper = new ObjectMapper();
String content = FileUtils.fileReader(topologyFilepath);
return mapper.readValue(content, TopologyConfig.class);
return objectMapper.readValue(content, TopologyConfig.class);
} catch (IOException e) {
throw ExceptionFactory.configNotFoundException(topologyFilepath);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.cardanofoundation.rosetta.client;

import org.cardanofoundation.rosetta.client.domain.StakeAccountInfo;

public interface YaciHttpGateway {

StakeAccountInfo getStakeAccountInfo(String stakeAddress);

}
Loading

0 comments on commit 830ccda

Please sign in to comment.