3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-14 11:15:49 +00:00

added 2 endpoints providing sponsorship analytics

This commit is contained in:
kennycud 2024-09-23 08:04:44 -07:00
parent f14bc86b39
commit d976904a8e
4 changed files with 580 additions and 1 deletions

View File

@ -23,6 +23,7 @@ import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.AccountPenaltyData;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.account.SponsorshipReport;
import org.qortal.data.network.OnlineAccountData;
import org.qortal.data.network.OnlineAccountLevel;
import org.qortal.data.transaction.PublicizeTransactionData;
@ -52,6 +53,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Path("/addresses")
@ -630,4 +632,70 @@ public class AddressesResource {
}
}
@GET
@Path("/sponsorship/{address}")
@Operation(
summary = "Returns sponsorship statistics for an account",
description = "Returns sponsorship statistics for an account",
responses = {
@ApiResponse(
description = "the statistics",
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = SponsorshipReport.class)))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public SponsorshipReport getSponsorshipReport(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(address);
// Not found?
if (report == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
return report;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/sponsorship/{address}/sponsor")
@Operation(
summary = "Returns sponsorship statistics for an account's sponsor",
description = "Returns sponsorship statistics for an account's sponsor",
responses = {
@ApiResponse(
description = "the statistics",
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = SponsorshipReport.class)))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public SponsorshipReport getSponsorshipReportForSponsor(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
// get sponsor
Optional<String> sponsor = repository.getAccountRepository().getSponsor(address);
// if there is not sponsor, throw error
if(sponsor.isEmpty()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// get report for sponsor
SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(sponsor.get());
// Not found?
if (report == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
return report;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@ -0,0 +1,148 @@
package org.qortal.data.account;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.Arrays;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class SponsorshipReport {
private String address;
private int level;
private int blocksMinted;
private int adjustments;
private int penalties;
private String[] names;
private int sponseeCount;
private int nonRegisteredCount;
private int avgBalance;
private int arbitraryCount;
private int transferAssetCount;
private int sellCount;
private int sellAmount;
private int buyCount;
private int buyAmount;
// Constructors
// For JAXB
protected SponsorshipReport() {
}
public SponsorshipReport(String address, int level, int blocksMinted, int adjustments, int penalties, String[] names, int sponseeCount, int nonRegisteredCount, int avgBalance, int arbitraryCount, int transferAssetCount, int sellCount, int sellAmount, int buyCount, int buyAmount) {
this.address = address;
this.level = level;
this.blocksMinted = blocksMinted;
this.adjustments = adjustments;
this.penalties = penalties;
this.names = names;
this.sponseeCount = sponseeCount;
this.nonRegisteredCount = nonRegisteredCount;
this.avgBalance = avgBalance;
this.arbitraryCount = arbitraryCount;
this.transferAssetCount = transferAssetCount;
this.sellCount = sellCount;
this.sellAmount = sellAmount;
this.buyCount = buyCount;
this.buyAmount = buyAmount;
}
// Getters / setters
public String getAddress() {
return address;
}
public int getLevel() {
return level;
}
public int getBlocksMinted() {
return blocksMinted;
}
public int getAdjustments() {
return adjustments;
}
public int getPenalties() {
return penalties;
}
public String[] getNames() {
return names;
}
public int getSponseeCount() {
return sponseeCount;
}
public int getNonRegisteredCount() {
return nonRegisteredCount;
}
public int getAvgBalance() {
return avgBalance;
}
public int getArbitraryCount() {
return arbitraryCount;
}
public int getTransferAssetCount() {
return transferAssetCount;
}
public int getSellCount() {
return sellCount;
}
public int getSellAmount() {
return sellAmount;
}
public int getBuyCount() {
return buyCount;
}
public int getBuyAmount() {
return buyAmount;
}
@Override
public String toString() {
return "SponsorshipReport{" +
"address='" + address + '\'' +
", level=" + level +
", blocksMinted=" + blocksMinted +
", adjustments=" + adjustments +
", penalties=" + penalties +
", names=" + Arrays.toString(names) +
", sponseeCount=" + sponseeCount +
", nonRegisteredCount=" + nonRegisteredCount +
", avgBalance=" + avgBalance +
", arbitraryCount=" + arbitraryCount +
", transferAssetCount=" + transferAssetCount +
", sellCount=" + sellCount +
", sellAmount=" + sellAmount +
", buyCount=" + buyCount +
", buyAmount=" + buyAmount +
'}';
}
}

View File

@ -3,6 +3,7 @@ package org.qortal.repository;
import org.qortal.data.account.*;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public interface AccountRepository {
@ -131,6 +132,39 @@ public interface AccountRepository {
/** Returns all account balances for given assetID, optionally excluding zero balances. */
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException;
/**
* Get Sponsorship Report
*
* @param address the sponsor's account address
*
* @return the report
*
* @throws DataException
*/
public SponsorshipReport getSponsorshipReport(String address) throws DataException;
/**
* Get Sponsee Addresses
*
* @param account the sponsor's account address
*
* @return the sponsee addresses
*
* @throws DataException
*/
public List<String> getSponseeAddresses(String account) throws DataException;
/**
* Get Sponsor
*
* @param address the address of the account
*
* @return the address of accounts sponsor, empty if not sponsored
*
* @throws DataException
*/
public Optional<String> getSponsor(String address) throws DataException;
/** How to order results when fetching asset balances. */
public enum BalanceOrdering {
/** assetID first, then balance, then account address */

View File

@ -1,5 +1,7 @@
package org.qortal.repository.hsqldb;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.asset.Asset;
import org.qortal.data.account.*;
import org.qortal.repository.AccountRepository;
@ -8,7 +10,11 @@ import org.qortal.repository.DataException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@ -16,12 +22,15 @@ import static org.qortal.utils.Amounts.prettyAmount;
public class HSQLDBAccountRepository implements AccountRepository {
public static final String SELL = "sell";
public static final String BUY = "buy";
protected HSQLDBRepository repository;
public HSQLDBAccountRepository(HSQLDBRepository repository) {
this.repository = repository;
}
protected static final Logger LOGGER = LogManager.getLogger(HSQLDBAccountRepository.class);
// General account
@Override
@ -1147,4 +1156,324 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
}
@Override
public SponsorshipReport getSponsorshipReport(String account) throws DataException {
try {
ResultSet accountResultSet = getAccountResultSet(account);
if( accountResultSet == null ) throw new DataException("Unable to fetch account info from repository");
int level = accountResultSet.getInt(2);
int blocksMinted = accountResultSet.getInt(3);
int adjustments = accountResultSet.getInt(4);
int penalties = accountResultSet.getInt(5);
List<String> sponseeAddresses = getSponseeAddresses(account);
if( sponseeAddresses.isEmpty() ){
return new SponsorshipReport(account, level, blocksMinted, adjustments, penalties, new String[0], 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
else {
return produceSponsorShipReport(account, level, blocksMinted, adjustments, penalties, sponseeAddresses);
}
}
catch (Exception e) {
LOGGER.error(e.getMessage(), e);
throw new DataException("Unable to fetch account info from repository", e);
}
}
@Override
public List<String> getSponseeAddresses(String account) throws DataException {
StringBuffer sponseeSql = new StringBuffer();
sponseeSql.append( "SELECT DISTINCT t.recipient sponsees " );
sponseeSql.append( "FROM REWARDSHARETRANSACTIONS t ");
sponseeSql.append( "INNER JOIN ACCOUNTS a on t.minter_public_key = a.public_key ");
sponseeSql.append( "WHERE account = ? and t.recipient != a.account");
try {
ResultSet sponseeResultSet = this.repository.checkedExecute(sponseeSql.toString(), account);
List<String> sponseeAddresses;
if( sponseeResultSet == null ) {
sponseeAddresses = new ArrayList<>(0);
}
else {
sponseeAddresses = new ArrayList<>();
do {
sponseeAddresses.add(sponseeResultSet.getString(1));
} while (sponseeResultSet.next());
}
return sponseeAddresses;
}
catch (SQLException e) {
throw new DataException("can't get sponsees from blockchain data", e);
}
}
@Override
public Optional<String> getSponsor(String address) throws DataException {
StringBuffer sponsorSql = new StringBuffer();
sponsorSql.append( "SELECT DISTINCT account, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty ");
sponsorSql.append( "FROM REWARDSHARETRANSACTIONS t ");
sponsorSql.append( "INNER JOIN ACCOUNTS a on a.public_key = t.minter_public_key ");
sponsorSql.append( "WHERE recipient = ? and recipient != account ");
try {
ResultSet sponseeResultSet = this.repository.checkedExecute(sponsorSql.toString(), address);
if( sponseeResultSet == null ){
return Optional.empty();
}
else {
return Optional.ofNullable( sponseeResultSet.getString(1));
}
} catch (SQLException e) {
throw new DataException("can't get sponsor from blockchain data", e);
}
}
/**
* Produce Sponsorship Report
*
* @param address the account address for the sponsor
* @param level the sponsor's level
* @param blocksMinted the blocks minted by the sponsor
* @param blocksMintedAdjustment
* @param blocksMintedPenalty
* @param sponseeAddresses
*
* @return the report
*
* @throws SQLException
*/
private SponsorshipReport produceSponsorShipReport(
String address,
int level,
int blocksMinted,
int blocksMintedAdjustment,
int blocksMintedPenalty,
List<String> sponseeAddresses) throws SQLException {
int sponseeCount = sponseeAddresses.size();
// get the registered nanmes of the sponsees
ResultSet namesResultSet = getNamesResultSet(sponseeAddresses, sponseeCount);
List<String> sponseeNames = getNames(namesResultSet, sponseeCount);
// get the average balance of the sponsees
ResultSet avgBalanceResultSet = getAverageBalanceResultSet(sponseeAddresses, sponseeCount);
int avgBalance = avgBalanceResultSet.getInt(1);
// count the arbitrary and transfer asset transactions for all sponsees
ResultSet txTypeResultSet = getTxTypeResultSet(sponseeAddresses, sponseeCount);
int arbitraryCount = 0;
int transferAssetCount = 0;
if( txTypeResultSet != null) {
int txType = txTypeResultSet.getInt(1);
// if arbitrary transaction type, then get the count and move to the next result
if (txType == 10) {
arbitraryCount = txTypeResultSet.getInt(2);
// if there is another result, then get
if (txTypeResultSet.next())
txType = txTypeResultSet.getInt(1);
}
// if asset transfer type, then get the count and move to the next result
if (txType == 12) {
transferAssetCount = txTypeResultSet.getInt(2);
txTypeResultSet.next();
}
}
// count up the each the buy and sell foreign coin exchanges for all sponsees
// also sum up the balances of these exchanges
ResultSet buySellResultSet = getBuySellResultSet(sponseeAddresses, sponseeCount);
// if there are results, then fill in the buy/sell amount/counts
if( buySellResultSet != null ) {
Map<String, Integer> countsByDirection = new HashMap<>(2);
Map<String, Integer> amountsByDirection = new HashMap<>(2);
do{
String direction = buySellResultSet.getString(1).trim();
if( direction != null ) {
countsByDirection.put(direction, buySellResultSet.getInt(2));
amountsByDirection.put(direction, buySellResultSet.getInt(3));
}
} while( buySellResultSet.next());
int sellCount = countsByDirection.getOrDefault(SELL, 0);
int sellAmount = amountsByDirection.getOrDefault(SELL, 0);
int buyCount = countsByDirection.getOrDefault(BUY, 0);
int buyAmount = amountsByDirection.getOrDefault(BUY, 0);
return new SponsorshipReport(
address,
level,
blocksMinted,
blocksMintedAdjustment,
blocksMintedPenalty,
sponseeNames.toArray(new String[sponseeNames.size()]),
sponseeCount,
sponseeCount - sponseeNames.size(),
avgBalance,
arbitraryCount,
transferAssetCount,
sellCount,
sellAmount,
buyCount,
buyAmount);
}
// otherwise use zeros for the counts and amounts
return new SponsorshipReport(
address,
level,
blocksMinted,
blocksMintedAdjustment,
blocksMintedPenalty,
sponseeNames.toArray(new String[sponseeNames.size()]),
sponseeCount,
sponseeCount - sponseeNames.size(),
avgBalance,
arbitraryCount,
transferAssetCount,
0,
0,
0,
0);
}
private ResultSet getBuySellResultSet(List<String> sponseeAddresses, int sponseeCount) throws SQLException {
StringBuffer buySellSql = new StringBuffer();
buySellSql.append("SELECT ");
buySellSql.append("CASE ");
buySellSql.append(" WHEN participant = account THEN 'sell' ");
buySellSql.append(" WHEN participant != account THEN 'buy' ");
buySellSql.append("END AS direction, ");
buySellSql.append(" COUNT(*) as transactions, sum(tx.amount)/100000000 as amount ");
buySellSql.append("FROM TRANSACTIONPARTICIPANTS ");
buySellSql.append("INNER JOIN ATTRANSACTIONS tx using (signature) ");
buySellSql.append("INNER JOIN ATS ats using (at_address) ");
buySellSql.append("INNER JOIN ACCOUNTS a on ats.creator = a.public_key ");
buySellSql.append("WHERE participant in ( ");
buySellSql.append(String.join(", ", Collections.nCopies(sponseeCount, "?")));
buySellSql.append(") ");
buySellSql.append("GROUP BY ");
buySellSql.append("CASE ");
buySellSql.append(" WHEN participant = account THEN 'sell' ");
buySellSql.append(" WHEN participant != account THEN 'buy' ");
buySellSql.append("END; ");
String[] sponsees = sponseeAddresses.toArray(new String[sponseeCount]);
ResultSet buySellResultSet = this.repository.checkedExecute(buySellSql.toString(), sponsees);
return buySellResultSet;
}
private ResultSet getAccountResultSet(String account) throws SQLException {
StringBuffer accountSql = new StringBuffer();
accountSql.append( "SELECT DISTINCT account, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty ");
accountSql.append( "FROM ACCOUNTS ");
accountSql.append( "WHERE account = ? ");
ResultSet accountResultSet = this.repository.checkedExecute( accountSql.toString(), account);
return accountResultSet;
}
private ResultSet getTxTypeResultSet(List<String> sponseeAddresses, int sponseeCount) throws SQLException {
StringBuffer txTypeTotalsSql = new StringBuffer();
// Transaction Types, int values
// ARBITRARY = 10
// TRANSFER_ASSET = 12
// txTypeTotalsSql.append("
txTypeTotalsSql.append("SELECT type, count(*) ");
txTypeTotalsSql.append("FROM TRANSACTIONPARTICIPANTS ");
txTypeTotalsSql.append("INNER JOIN TRANSACTIONS USING (signature) ");
txTypeTotalsSql.append("where participant in ( ");
txTypeTotalsSql.append(String.join(", ", Collections.nCopies(sponseeCount, "?")));
txTypeTotalsSql.append(") and type in (10, 12) ");
txTypeTotalsSql.append("group by type order by type");
String[] sponsees = sponseeAddresses.toArray(new String[sponseeCount]);
ResultSet txTypeResultSet = this.repository.checkedExecute(txTypeTotalsSql.toString(), sponsees);
return txTypeResultSet;
}
private ResultSet getAverageBalanceResultSet(List<String> sponseeAddresses, int sponseeCount) throws SQLException {
StringBuffer avgBalanceSql = new StringBuffer();
avgBalanceSql.append("SELECT avg(balance)/100000000 FROM ACCOUNTBALANCES ");
avgBalanceSql.append("WHERE account in (");
avgBalanceSql.append(String.join(", ", Collections.nCopies(sponseeCount, "?")));
avgBalanceSql.append(") and ASSET_ID = 0");
String[] sponsees = sponseeAddresses.toArray(new String[sponseeCount]);
return this.repository.checkedExecute(avgBalanceSql.toString(), sponsees);
}
/**
* Get Names
*
* @param namesResultSet the result set to get the names from
* @param count the number of potential names
*
* @return the names
*
* @throws SQLException
*/
private static List<String> getNames(ResultSet namesResultSet, int count) throws SQLException {
List<String> names = new ArrayList<>(count);
int nonRegisteredCount = 0;
do{
String name = namesResultSet.getString(1);
if( name != null ) {
names.add(name);
}
else {
nonRegisteredCount++;
}
} while( namesResultSet.next() );
return names;
}
private ResultSet getNamesResultSet(List<String> sponseeAddresses, int sponseeCount) throws SQLException {
StringBuffer namesSql = new StringBuffer();
namesSql.append("SELECT r.name ");
namesSql.append("FROM ACCOUNTS a ");
namesSql.append("LEFT JOIN REGISTERNAMETRANSACTIONS r on r.registrant = a.public_key ");
namesSql.append("WHERE account in (");
namesSql.append(String.join(", ", Collections.nCopies(sponseeCount, "?")));
namesSql.append(")");
String[] sponsees = sponseeAddresses.toArray(new String[sponseeCount]);
ResultSet namesResultSet = this.repository.checkedExecute(namesSql.toString(), sponsees);
return namesResultSet;
}
}