Skip to content

Commit

Permalink
feat: stake account balance.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mateusz Czeladka committed Feb 11, 2025
1 parent 28bcfa6 commit 3dab0f4
Show file tree
Hide file tree
Showing 31 changed files with 750 additions and 229 deletions.
1 change: 1 addition & 0 deletions .env.IntegrationTest
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ INDEXER_DOCKER_IMAGE_TAG=main
PRUNING_ENABLED=false

YACI_SPRING_PROFILES=postgres,n2c-socat
YACI_INDEXER_PORT=9095
# database profiles: h2, h2-testData, postgres
MEMPOOL_ENABLED=false

Expand Down
1 change: 1 addition & 0 deletions .env.docker-compose
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ INDEXER_DOCKER_IMAGE_TAG=main
PRUNING_ENABLED=false

YACI_SPRING_PROFILES=postgres,n2c-socket
YACI_INDEXER_PORT=9095
# database profiles: h2, h2-testData, postgres
MEMPOOL_ENABLED=false

Expand Down
1 change: 1 addition & 0 deletions .env.h2
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ PRUNING_ENABLED=false

YACI_SPRING_PROFILES=h2,n2c-socket
# database profiles: h2, h2-testData, postgres
YACI_INDEXER_PORT=9095
MEMPOOL_ENABLED=false

## Logger Config
Expand Down
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.model.domain.StakeAccountInfo;

public interface AddressBalanceMapper {

AddressBalance convertToAdaAddressBalance(StakeAccountInfo stakeAccountInfo, Long number);

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

import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Service;

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

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

@Service
@Slf4j
public class AddressBalanceMapperImpl implements AddressBalanceMapper {

@Override
public AddressBalance convertToAdaAddressBalance(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
Expand Up @@ -10,47 +10,43 @@
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.openapitools.client.model.*;

import org.cardanofoundation.rosetta.api.account.mapper.AccountMapper;
import org.cardanofoundation.rosetta.api.account.mapper.AddressBalanceMapper;
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.model.domain.StakeAccountInfo;
import org.cardanofoundation.rosetta.common.exception.ExceptionFactory;
import org.cardanofoundation.rosetta.common.util.CardanoAddressUtils;
import org.cardanofoundation.rosetta.common.util.Constants;

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;


@Service
@Slf4j
@RequiredArgsConstructor
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 AddressBalanceMapper balanceMapper;

@Override
public AccountBalanceResponse getAccountBalance(AccountBalanceRequest accountBalanceRequest) {


Long index = null;
String hash = null;
String accountAddress = accountBalanceRequest.getAccountIdentifier().getAddress();
Expand All @@ -65,7 +61,6 @@ public AccountBalanceResponse getAccountBalance(AccountBalanceRequest accountBal
}

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

}

@Override
Expand All @@ -87,36 +82,42 @@ 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) {
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.getStakeAccountRewards(address);

balances = List.of(balanceMapper.convertToAdaAddressBalance(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) {
Expand All @@ -135,7 +136,7 @@ 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 +153,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
Expand Up @@ -15,8 +15,6 @@ public interface LedgerAccountService {

List<AddressBalance> findBalanceByAddressAndBlock(String address, Long number);

List<AddressBalance> findBalanceByStakeAddressAndBlock(String address, Long number);

List<Utxo> findUtxoByAddressAndCurrency(String address, List<Currency> currencies);

}
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 @@ -36,15 +38,6 @@ public List<AddressBalance> findBalanceByAddressAndBlock(String address, Long nu
return mapAndGroupAddressUtxoEntityToAddressBalance(unspendUtxosByAddressAndBlock);
}

@Override
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);
}

private static List<AddressBalance> mapAndGroupAddressUtxoEntityToAddressBalance(
List<AddressUtxoEntity> unspendUtxosByAddressAndBlock) {
Map<String, AddressBalance> map = new HashMap<>();
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.model.domain.StakeAccountInfo;

public interface YaciHttpGateway {

StakeAccountInfo getStakeAccountRewards(String stakeAddress);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.cardanofoundation.rosetta.client;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import jakarta.annotation.PostConstruct;

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.cardanofoundation.rosetta.client.model.domain.StakeAccountInfo;
import org.cardanofoundation.rosetta.common.exception.ExceptionFactory;

@Service
@Slf4j
@RequiredArgsConstructor
public class YaciHttpGatewayImpl implements YaciHttpGateway {

private final HttpClient httpClient;
private final ObjectMapper objectMapper = new ObjectMapper();

@Value("${cardano.rosetta.YACI_HTTP_BASE_URL}")
protected String yaciBaseUrl;

@Value("${cardano.rosetta.HTTP_REQUEST_TIMEOUT_SECONDS}")
protected int httpRequestTimeoutSeconds;

@PostConstruct
public void init() {
log.info("YaciHttpGatewayImpl initialized with yaciBaseUrl: {}, httpRequestTimeoutSeconds: {}", yaciBaseUrl, httpRequestTimeoutSeconds);
}

@Override
public StakeAccountInfo getStakeAccountRewards(String stakeAddress) {
var getStakeAccountDetailsHttpRequest = HttpRequest.newBuilder()
.uri(URI.create(yaciBaseUrl + "/rosetta/account/by-stake-address/" + stakeAddress))
.GET()
.timeout(Duration.ofSeconds(httpRequestTimeoutSeconds))
.header("Content-Type", "application/json")
.build();

try {
HttpResponse<String> response = httpClient.send(getStakeAccountDetailsHttpRequest, HttpResponse.BodyHandlers.ofString());

int statusCode = response.statusCode();
String responseBody = response.body();

if (statusCode >= 200 && statusCode < 300) {
return objectMapper.readValue(responseBody, StakeAccountInfo.class);
} else if (statusCode == 400) {
throw ExceptionFactory.gatewayError(false);
} else if (statusCode == 500) {
throw ExceptionFactory.gatewayError(true);
} else {
throw ExceptionFactory.gatewayError(false);
}
} catch (IOException | InterruptedException e) {
log.error("Error during yaci-indexer HTTP request", e);

Thread.currentThread().interrupt();

throw ExceptionFactory.gatewayError(true);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.cardanofoundation.rosetta.client.model.domain;

import java.math.BigInteger;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class StakeAccountInfo {

private String stakeAddress;
private BigInteger withdrawableAmount;
private BigInteger controlledAmount;

}
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,9 @@ public static ApiException poolDepositMissingError() {
public static ApiException NotSupportedInOfflineMode() {
return new ApiException(RosettaErrorType.NOT_SUPPORTED_IN_OFFLINE_MODE.toRosettaError(false));
}

public static ApiException gatewayError(boolean retriable) {
return new ApiException(RosettaErrorType.GATEWAY_ERROR.toRosettaError(retriable));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ public enum RosettaErrorType {
MIN_POOL_COST_MISSING("body.metadata must have required property 'minPoolCost'", 5031),
PROTOCOL_MISSING("body.metadata must have required property 'protocol'", 5032),
POOL_DEPOSIT_MISSING("body.metadata must have required property 'poolDeposit'", 5033),
NOT_SUPPORTED_IN_OFFLINE_MODE("This operation is not supported in offline mode", 5034),;

NOT_SUPPORTED_IN_OFFLINE_MODE("This operation is not supported in offline mode", 5034),
GATEWAY_ERROR("Unable to get data from the downstream gateway", 5035);

final String message;
final int code;
Expand Down
Loading

0 comments on commit 3dab0f4

Please sign in to comment.