Skip to content

Commit

Permalink
Merge pull request #70 from bloxbean/account_balance
Browse files Browse the repository at this point in the history
feat(account/utxo): Run time account balance aggregation support
  • Loading branch information
satran004 authored Aug 4, 2023
2 parents ac6ce5f + fe4b09a commit 8ff23f5
Show file tree
Hide file tree
Showing 22 changed files with 410 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
@EnableTransactionManagement
public class AccountStoreConfiguration {

@Value("${store.account.history-cleanup-enabled:true}")
private boolean historyCleanupEnabled = true;
@Value("${store.account.history-cleanup-enabled:false}")
private boolean historyCleanupEnabled;

@Value("${store.account.balance-aggregation-enabled:true}")
private boolean balanceAggregationEnabled = true;
@Value("${store.account.balance-aggregation-enabled:false}")
private boolean balanceAggregationEnabled;

@Bean
@ConditionalOnMissingBean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package com.bloxbean.cardano.yaci.store.account.controller;

import com.bloxbean.cardano.yaci.store.account.AccountStoreConfiguration;
import com.bloxbean.cardano.yaci.store.account.domain.AddressBalance;
import com.bloxbean.cardano.yaci.store.account.domain.StakeAccountInfo;
import com.bloxbean.cardano.yaci.store.account.domain.StakeAccountRewardInfo;
import com.bloxbean.cardano.yaci.store.account.domain.StakeAddressBalance;
import com.bloxbean.cardano.yaci.store.account.service.AccountService;
import com.bloxbean.cardano.yaci.store.account.service.UtxoAccountService;
import com.bloxbean.cardano.yaci.store.account.storage.AccountBalanceStorage;
import com.bloxbean.cardano.yaci.store.common.util.Bech32Prefixes;
import com.bloxbean.cardano.yaci.store.utxo.domain.Amount;
import io.swagger.v3.oas.annotations.Operation;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

Expand All @@ -24,29 +31,50 @@
public class AccountController {
private final AccountBalanceStorage accountBalanceStorage;
private final AccountService accountService;
private final UtxoAccountService utxoAccountService;
private final AccountStoreConfiguration accountStoreConfiguration;

@GetMapping("/addresses/{address}/balance")
@Operation(description = "Get current balance at an address")
public List<AddressBalance> getAddressBalance(String address) {
if (!accountStoreConfiguration.isBalanceAggregationEnabled())
throw new UnsupportedOperationException("Address balance aggregation is not enabled");

return accountBalanceStorage.getAddressBalance(address);
}

@GetMapping("/accounts/{address}/balance")
@Operation(description = "Get current balance at a stake address")
public List<StakeAddressBalance> getStakeAddressBalance(String stakeAddr) {
if (!accountStoreConfiguration.isBalanceAggregationEnabled())
throw new UnsupportedOperationException("Address balance aggregation is not enabled");

return accountBalanceStorage.getStakeAddressBalance(stakeAddr);
}

@GetMapping("/accounts/{stakeAddress}")
@Operation(description = "Obtain information about a specific stake account")
public StakeAccountInfo getStakeAccountDetails(String stakeAddress) {
List<StakeAddressBalance> stakeAddressBalances = accountBalanceStorage.getStakeAddressBalance(stakeAddress);
public StakeAccountInfo getStakeAccountDetails(@PathVariable @NonNull String stakeAddress) {
if (!stakeAddress.startsWith(Bech32Prefixes.STAKE_ADDR_PREFIX))
throw new IllegalArgumentException("Invalid stake address");

BigInteger lovellaceBalance = BigInteger.ZERO;
if (stakeAddressBalances != null && stakeAddressBalances.size() > 0) {
lovellaceBalance = stakeAddressBalances.stream().filter(addressBalance -> addressBalance.getUnit().equals("lovelace"))
.findFirst()
.map(addressBalance -> addressBalance.getQuantity())
.orElse(BigInteger.ZERO);
if (accountStoreConfiguration.isBalanceAggregationEnabled()) {
List<StakeAddressBalance> stakeAddressBalances = accountBalanceStorage.getStakeAddressBalance(stakeAddress);
if (stakeAddressBalances != null && stakeAddressBalances.size() > 0) {
lovellaceBalance = stakeAddressBalances.stream().filter(addressBalance -> addressBalance.getUnit().equals("lovelace"))
.findFirst()
.map(addressBalance -> addressBalance.getQuantity())
.orElse(BigInteger.ZERO);
}
} else { //Do run time aggregation
List<Amount> amounts = utxoAccountService.getAmountsAtAddress(stakeAddress);
if (amounts != null && amounts.size() > 0) {
lovellaceBalance = amounts.stream().filter(amount -> amount.getUnit().equals("lovelace"))
.findFirst()
.map(amount -> amount.getQuantity())
.orElse(BigInteger.ZERO);
}
}

Optional<StakeAccountRewardInfo> stakeAccountInfo = accountService.getAccountInfo(stakeAddress);
Expand All @@ -60,4 +88,19 @@ public StakeAccountInfo getStakeAccountDetails(String stakeAddress) {

return stakeAccountDetails;
}

@GetMapping("/addresses/{address}/amounts")
@Operation(description = "Get amounts at an address. For stake address, only lovelace is returned")
public List<Amount> getAddressAmounts(@PathVariable @NonNull String address) {
List<Amount> amounts = utxoAccountService.getAmountsAtAddress(address);
if (amounts == null || amounts.size() == 0)
return Collections.emptyList();

if (address.startsWith(Bech32Prefixes.STAKE_ADDR_PREFIX)) { //For stake address, return only lovelace
return amounts.stream().filter(amount -> amount.getUnit().equals("lovelace"))
.toList();
} else {
return amounts;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ private List<AddressBalance> handleAddressBalance(AddressUtxoEvent addressUtxoEv

//Update inputs
for (AddressUtxo input : inputs) {
if (input.getAmounts() == null) {
log.error("Input amounts are null for tx: " + txInputOutput.getTxHash());
log.error("Input: " + input);
}

for (Amt amount : input.getAmounts()) {
String key = getKey(input.getOwnerAddr(), amount.getUnit());
if (addressBalanceMap.get(key) != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.bloxbean.cardano.yaci.store.account.service;

import com.bloxbean.cardano.yaci.store.client.utxo.UtxoClient;
import com.bloxbean.cardano.yaci.store.common.domain.Utxo;
import com.bloxbean.cardano.yaci.store.utxo.domain.Amount;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

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

@Service
@RequiredArgsConstructor
@Slf4j
public class UtxoAccountService {
private final UtxoClient utxoClient;

public List<Amount> getAmountsAtAddress(String address) {
int page = 0;

Map<String, Amount> amountMap = new HashMap<>();
while (true) {
List<Utxo> utxos = utxoClient.getUtxoByAddress(address, page, 100);
if (utxos == null || utxos.size() == 0)
break;

utxos.stream()
.flatMap(utxo -> utxo.getAmount().stream())
.forEach(utxoAmt -> {
Amount existingAmount = amountMap.get(utxoAmt.getUnit());
if(existingAmount == null) {
Amount newAmount = new Amount(utxoAmt.getUnit(), utxoAmt.getQuantity());
amountMap.put(utxoAmt.getUnit(), newAmount);
} else {
BigInteger newQty = existingAmount.getQuantity().add(utxoAmt.getQuantity());
existingAmount.setQuantity(newQty);
}
});

page++;
}

return new ArrayList<>(amountMap.values());
}
}
9 changes: 7 additions & 2 deletions applications/utxo-indexer/src/main/resources/banner.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@

██ ██ █████ ██████ ██ ███████ ████████ ██████ ██████ ███████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
████ ███████ ██ ██ ███████ ██ ██ ██ ██████ █████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██████ ██ ███████ ██ ██████ ██ ██ ███████
A BloxBean Project


██ ██ ████████ ██ ██ ██████ ██ ███ ██ ██████ ███████ ██ ██ ███████ ██████
██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ █████ ███ █████ ██████
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██ ██ ██ ██████ ██ ██ ████ ██████ ███████ ██ ██ ███████ ██ ██

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.bloxbean.cardano.yaci.store.client.utxo;

import com.bloxbean.cardano.yaci.store.common.domain.AddressUtxo;
import com.bloxbean.cardano.yaci.store.common.domain.Utxo;
import com.bloxbean.cardano.yaci.store.common.domain.UtxoKey;
import lombok.extern.slf4j.Slf4j;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

@Slf4j
public class DummyUtxoClient implements UtxoClient {
public DummyUtxoClient() {
log.warn("Dummy Utxo Client Configured >>>>>>>>");
}

@Override
public List<AddressUtxo> getUtxosByIds(List<UtxoKey> utxoIds) {
return Collections.emptyList();
}

@Override
public Optional<AddressUtxo> getUtxoById(UtxoKey utxoId) {
return Optional.empty();
}

@Override
public List<Utxo> getUtxoByAddress(String address, int page, int count) {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.bloxbean.cardano.yaci.store.client.utxo;

import com.bloxbean.cardano.yaci.store.common.domain.AddressUtxo;
import com.bloxbean.cardano.yaci.store.common.domain.Utxo;
import com.bloxbean.cardano.yaci.store.common.domain.UtxoKey;

import java.util.List;
Expand All @@ -9,4 +10,5 @@
public interface UtxoClient {
List<AddressUtxo> getUtxosByIds(List<UtxoKey> utxoIds);
Optional<AddressUtxo> getUtxoById(UtxoKey utxoId);
List<Utxo> getUtxoByAddress(String address, int page, int count);
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
package com.bloxbean.cardano.yaci.store.client.utxo;

import com.bloxbean.cardano.yaci.store.common.domain.AddressUtxo;
import com.bloxbean.cardano.yaci.store.common.domain.Utxo;
import com.bloxbean.cardano.yaci.store.common.domain.UtxoKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

@Component
@ConditionalOnMissingBean(name = "utxoClient")
@Slf4j
public class UtxoClientImpl implements UtxoClient {
private final RestTemplate restTemplate;

@Value("${server.port:8080}")
private int serverPort;
@Value("${store.utxo.base.url:#{null}}")
private String utxoStoreBaseUrl;

public UtxoClientImpl(RestTemplate restTemplate) {
public UtxoClientImpl(RestTemplate restTemplate, String utxoStoreBaseUrl) {
this.restTemplate = restTemplate;
log.info("Enabled Remote UtxoClient >>>");
this.utxoStoreBaseUrl = utxoStoreBaseUrl;
log.info("Enabled Remote UtxoClient >>>>>>>> " + utxoStoreBaseUrl);
}

@Override
Expand All @@ -41,6 +38,13 @@ public Optional<AddressUtxo> getUtxoById(UtxoKey utxoId) {
return Optional.ofNullable(utxo);
}

@Override
public List<Utxo> getUtxoByAddress(String address, int page, int count) {
String url = getBaseUrl() + "/addresses/" + address + "/utxos?page=" + page + "&count=" + count;
Utxo[] utxos = restTemplate.getForObject(url, Utxo[].class);
return Arrays.asList(utxos);
}

private String getBaseUrl() {
if (utxoStoreBaseUrl == null)
utxoStoreBaseUrl = "http://localhost:" + serverPort + "/api/v1";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.bloxbean.cardano.yaci.store.common.util;

public class Bech32Prefixes {
public static final String ADDR_PREFIX = "addr";
public static final String STAKE_ADDR_PREFIX = "stake";
public static final String ADDR_VKEY_HASH_PREFIX = "addr_vkh";
public static final String STAKE_ADDR_VKEY_HASH_PREFIX = "stake_vkh";
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class BlocksStoreProperties {
public static final class Blocks {
private boolean enabled = true;
private int epochCalculationInterval = 120; //in seconds
private boolean epochCalculationEnabled;
}

}
1 change: 1 addition & 0 deletions starters/spring-boot-starter/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dependencies {
api project(":components:common")
api project(':components:core')
api project(":components:events")
api project(":components:client")

api(libs.yaci){
exclude group: "com.bloxbean.cardano", module: "cardano-client-core"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import com.bloxbean.cardano.yaci.core.protocol.chainsync.messages.Point;
import com.bloxbean.cardano.yaci.helper.*;
import com.bloxbean.cardano.yaci.store.client.utxo.DummyUtxoClient;
import com.bloxbean.cardano.yaci.store.client.utxo.UtxoClient;
import com.bloxbean.cardano.yaci.store.client.utxo.UtxoClientImpl;
import com.bloxbean.cardano.yaci.store.core.StoreConfiguration;
import com.bloxbean.cardano.yaci.store.core.StoreProperties;
import com.bloxbean.cardano.yaci.store.core.service.ApplicationStartListener;
Expand Down Expand Up @@ -90,6 +93,15 @@ public ApplicationStartListener applicationStartListener(LocalClientProvider loc
return new ApplicationStartListener(localClientProvider);
}

@Bean
@ConditionalOnMissingBean(name = "utxoClient")
public UtxoClient utxoClient(RestTemplate restTemplate) {
if (properties.getUtxoClientUrl() != null && !properties.getUtxoClientUrl().isEmpty())
return new UtxoClientImpl(restTemplate, properties.getUtxoClientUrl().trim());
else
return new DummyUtxoClient();
}

@Bean
@ConditionalOnMissingBean
public RestTemplate restTemplate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class YaciStoreProperties {
private Cardano cardano = new Cardano();
private long eventPublisherId = 1;
private boolean syncAutoStart = true;
private String utxoClientUrl;

@Getter
@Setter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
Expand All @@ -19,6 +20,7 @@

@Slf4j
@Component
@ConditionalOnProperty(name = "store.blocks.epoch-calculation-enabled", havingValue = "true", matchIfMissing = false)
@RequiredArgsConstructor
public class EpochProcessor {

Expand Down
Loading

0 comments on commit 8ff23f5

Please sign in to comment.