From f14bc86b399075e96c280b45d0aef88481954d99 Mon Sep 17 00:00:00 2001 From: kennycud Date: Sun, 15 Sep 2024 16:10:30 -0700 Subject: [PATCH 01/14] implemented arbitrary resources simple search --- .../api/resource/ArbitraryResource.java | 43 ++++++ .../repository/ArbitraryRepository.java | 11 ++ .../hsqldb/HSQLDBArbitraryRepository.java | 122 ++++++++++++++++++ 3 files changed, 176 insertions(+) diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java index 99fc0020..754c3467 100644 --- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java +++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java @@ -227,6 +227,49 @@ public class ArbitraryResource { } } + @GET + @Path("/resources/searchsimple") + @Operation( + summary = "Search arbitrary resources available on chain, optionally filtered by service.", + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryResourceData.class)) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public List searchResourcesSimple( + @QueryParam("service") Service service, + @Parameter(description = "Identifier (searches identifier field only)") @QueryParam("identifier") String identifier, + @Parameter(description = "Name (searches name field only)") @QueryParam("name") List names, + @Parameter(description = "Prefix only (if true, only the beginning of fields are matched)") @QueryParam("prefix") Boolean prefixOnly, + @Parameter(description = "Case insensitive (ignore leter case on search)") @QueryParam("caseInsensitive") Boolean caseInsensitive, + @Parameter(description = "Creation date before timestamp") @QueryParam("before") Long before, + @Parameter(description = "Creation date after timestamp") @QueryParam("after") Long after, + @Parameter(ref = "limit") @QueryParam("limit") Integer limit, + @Parameter(ref = "offset") @QueryParam("offset") Integer offset, + @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) { + + try (final Repository repository = RepositoryManager.getRepository()) { + + boolean usePrefixOnly = Boolean.TRUE.equals(prefixOnly); + boolean ignoreCase = Boolean.TRUE.equals(caseInsensitive); + + List resources = repository.getArbitraryRepository() + .searchArbitraryResourcesSimple(service, identifier, names, usePrefixOnly, + before, after, limit, offset, reverse, ignoreCase); + + if (resources == null) { + return new ArrayList<>(); + } + + return resources; + + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + @GET @Path("/resource/status/{service}/{name}") @Operation( diff --git a/src/main/java/org/qortal/repository/ArbitraryRepository.java b/src/main/java/org/qortal/repository/ArbitraryRepository.java index 175f1daf..1c0e84e2 100644 --- a/src/main/java/org/qortal/repository/ArbitraryRepository.java +++ b/src/main/java/org/qortal/repository/ArbitraryRepository.java @@ -44,6 +44,17 @@ public interface ArbitraryRepository { public List searchArbitraryResources(Service service, String query, String identifier, List names, String title, String description, boolean prefixOnly, List namesFilter, boolean defaultResource, SearchMode mode, Integer minLevel, Boolean followedOnly, Boolean excludeBlocked, Boolean includeMetadata, Boolean includeStatus, Long before, Long after, Integer limit, Integer offset, Boolean reverse) throws DataException; + List searchArbitraryResourcesSimple( + Service service, + String identifier, + List names, + boolean prefixOnly, + Long before, + Long after, + Integer limit, + Integer offset, + Boolean reverse, + Boolean caseInsensitive) throws DataException; // Arbitrary resources cache save/load diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java index c49074c5..e30d61bf 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java @@ -954,6 +954,128 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository { } } + @Override + public List searchArbitraryResourcesSimple( + Service service, + String identifier, + List names, + boolean prefixOnly, + Long before, + Long after, + Integer limit, + Integer offset, + Boolean reverse, + Boolean caseInsensitive) throws DataException { + StringBuilder sql = new StringBuilder(512); + List bindParams = new ArrayList<>(); + + sql.append("SELECT name, service, identifier, size, status, created_when, updated_when "); + sql.append("FROM ArbitraryResourcesCache "); + sql.append("WHERE name IS NOT NULL"); + + if (service != null) { + sql.append(" AND service = ?"); + bindParams.add(service.value); + } + + // Handle identifier matches + if (identifier != null) { + if(caseInsensitive || prefixOnly) { + // Search anywhere in the identifier, unless "prefixOnly" has been requested + String queryWildcard = getQueryWildcard(identifier, prefixOnly, caseInsensitive); + sql.append(caseInsensitive ? " AND LCASE(identifier) LIKE ?" : " AND identifier LIKE ?"); + bindParams.add(queryWildcard); + } + else { + sql.append(" AND identifier = ?"); + bindParams.add(identifier); + } + } + + // Handle name searches + if (names != null && !names.isEmpty()) { + sql.append(" AND ("); + + if( caseInsensitive || prefixOnly ) { + for (int i = 0; i < names.size(); ++i) { + // Search anywhere in the name, unless "prefixOnly" has been requested + String queryWildcard = getQueryWildcard(names.get(i), prefixOnly, caseInsensitive); + if (i > 0) sql.append(" OR "); + sql.append(caseInsensitive ? "LCASE(name) LIKE ?" : "name LIKE ?"); + bindParams.add(queryWildcard); + } + } + else { + for (int i = 0; i < names.size(); ++i) { + if (i > 0) sql.append(" OR "); + sql.append("name = ?"); + bindParams.add(names.get(i)); + } + } + + sql.append(")"); + } + + // Timestamp range + if (before != null) { + sql.append(" AND created_when < ?"); + bindParams.add(before); + } + if (after != null) { + sql.append(" AND created_when > ?"); + bindParams.add(after); + } + + sql.append(" ORDER BY created_when"); + + if (reverse != null && reverse) { + sql.append(" DESC"); + } + + HSQLDBRepository.limitOffsetSql(sql, limit, offset); + + List arbitraryResources = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) { + if (resultSet == null) + return arbitraryResources; + + do { + String nameResult = resultSet.getString(1); + Service serviceResult = Service.valueOf(resultSet.getInt(2)); + String identifierResult = resultSet.getString(3); + Integer sizeResult = resultSet.getInt(4); + Integer status = resultSet.getInt(5); + Long created = resultSet.getLong(6); + Long updated = resultSet.getLong(7); + + if (Objects.equals(identifierResult, "default")) { + // Map "default" back to null. This is optional but probably less confusing than returning "default". + identifierResult = null; + } + + ArbitraryResourceData arbitraryResourceData = new ArbitraryResourceData(); + arbitraryResourceData.name = nameResult; + arbitraryResourceData.service = serviceResult; + arbitraryResourceData.identifier = identifierResult; + arbitraryResourceData.size = sizeResult; + arbitraryResourceData.created = created; + arbitraryResourceData.updated = (updated == 0) ? null : updated; + + arbitraryResources.add(arbitraryResourceData); + } while (resultSet.next()); + + return arbitraryResources; + } catch (SQLException e) { + throw new DataException("Unable to fetch simple arbitrary resources from repository", e); + } + } + + private static String getQueryWildcard(String value, boolean prefixOnly, boolean caseInsensitive) { + String valueToUse = caseInsensitive ? value.toLowerCase() : value; + return prefixOnly ? String.format("%s%%", valueToUse) : valueToUse; + } + // Arbitrary resources cache save/load From d976904a8e771b1dd4c7214ffe11af3e7214d79b Mon Sep 17 00:00:00 2001 From: kennycud Date: Mon, 23 Sep 2024 08:04:44 -0700 Subject: [PATCH 02/14] added 2 endpoints providing sponsorship analytics --- .../api/resource/AddressesResource.java | 68 ++++ .../data/account/SponsorshipReport.java | 148 ++++++++ .../qortal/repository/AccountRepository.java | 34 ++ .../hsqldb/HSQLDBAccountRepository.java | 331 +++++++++++++++++- 4 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/qortal/data/account/SponsorshipReport.java diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index 66d8412c..349dd89d 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -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 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); + } + } } diff --git a/src/main/java/org/qortal/data/account/SponsorshipReport.java b/src/main/java/org/qortal/data/account/SponsorshipReport.java new file mode 100644 index 00000000..47470b6a --- /dev/null +++ b/src/main/java/org/qortal/data/account/SponsorshipReport.java @@ -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 + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/repository/AccountRepository.java b/src/main/java/org/qortal/repository/AccountRepository.java index bdad187b..d1ade684 100644 --- a/src/main/java/org/qortal/repository/AccountRepository.java +++ b/src/main/java/org/qortal/repository/AccountRepository.java @@ -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 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 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 getSponsor(String address) throws DataException; + /** How to order results when fetching asset balances. */ public enum BalanceOrdering { /** assetID first, then balance, then account address */ diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index 7aef66ce..374c3a99 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -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 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 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 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 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 sponseeAddresses) throws SQLException { + + int sponseeCount = sponseeAddresses.size(); + + // get the registered nanmes of the sponsees + ResultSet namesResultSet = getNamesResultSet(sponseeAddresses, sponseeCount); + List 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 countsByDirection = new HashMap<>(2); + Map 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 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 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 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 getNames(ResultSet namesResultSet, int count) throws SQLException { + + List 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 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; + } +} \ No newline at end of file From a530b64ae74b963f21be49610aa08414532f8817 Mon Sep 17 00:00:00 2001 From: kennycud Date: Mon, 23 Sep 2024 16:24:24 -0700 Subject: [PATCH 03/14] added transfer privs inquiries to sponsorship analytics --- .../data/account/SponsorshipReport.java | 18 ++- .../hsqldb/HSQLDBAccountRepository.java | 103 +++++++++--------- 2 files changed, 67 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/qortal/data/account/SponsorshipReport.java b/src/main/java/org/qortal/data/account/SponsorshipReport.java index 47470b6a..b08051ef 100644 --- a/src/main/java/org/qortal/data/account/SponsorshipReport.java +++ b/src/main/java/org/qortal/data/account/SponsorshipReport.java @@ -18,6 +18,8 @@ public class SponsorshipReport { private int penalties; + private boolean transfer; + private String[] names; private int sponseeCount; @@ -30,6 +32,8 @@ public class SponsorshipReport { private int transferAssetCount; + private int transferPrivsCount; + private int sellCount; private int sellAmount; @@ -44,18 +48,20 @@ public class SponsorshipReport { 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) { + public SponsorshipReport(String address, int level, int blocksMinted, int adjustments, int penalties, boolean transfer, String[] names, int sponseeCount, int nonRegisteredCount, int avgBalance, int arbitraryCount, int transferAssetCount, int transferPrivsCount, int sellCount, int sellAmount, int buyCount, int buyAmount) { this.address = address; this.level = level; this.blocksMinted = blocksMinted; this.adjustments = adjustments; this.penalties = penalties; + this.transfer = transfer; this.names = names; this.sponseeCount = sponseeCount; this.nonRegisteredCount = nonRegisteredCount; this.avgBalance = avgBalance; this.arbitraryCount = arbitraryCount; this.transferAssetCount = transferAssetCount; + this.transferPrivsCount = transferPrivsCount; this.sellCount = sellCount; this.sellAmount = sellAmount; this.buyCount = buyCount; @@ -85,6 +91,10 @@ public class SponsorshipReport { return penalties; } + public boolean isTransfer() { + return transfer; + } + public String[] getNames() { return names; } @@ -109,6 +119,10 @@ public class SponsorshipReport { return transferAssetCount; } + public int getTransferPrivsCount() { + return transferPrivsCount; + } + public int getSellCount() { return sellCount; } @@ -133,12 +147,14 @@ public class SponsorshipReport { ", blocksMinted=" + blocksMinted + ", adjustments=" + adjustments + ", penalties=" + penalties + + ", transfer=" + transfer + ", names=" + Arrays.toString(names) + ", sponseeCount=" + sponseeCount + ", nonRegisteredCount=" + nonRegisteredCount + ", avgBalance=" + avgBalance + ", arbitraryCount=" + arbitraryCount + ", transferAssetCount=" + transferAssetCount + + ", transferPrivsCount=" + transferPrivsCount + ", sellCount=" + sellCount + ", sellAmount=" + sellAmount + ", buyCount=" + buyCount + diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index 374c3a99..ede1ddf9 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -1168,14 +1168,15 @@ public class HSQLDBAccountRepository implements AccountRepository { int blocksMinted = accountResultSet.getInt(3); int adjustments = accountResultSet.getInt(4); int penalties = accountResultSet.getInt(5); + boolean transferPrivs = accountResultSet.getBoolean(6); List 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); + return new SponsorshipReport(account, level, blocksMinted, adjustments, penalties, transferPrivs, new String[0], 0, 0,0, 0, 0, 0, 0, 0, 0, 0); } else { - return produceSponsorShipReport(account, level, blocksMinted, adjustments, penalties, sponseeAddresses); + return produceSponsorShipReport(account, level, blocksMinted, adjustments, penalties, sponseeAddresses, transferPrivs); } } catch (Exception e) { @@ -1243,15 +1244,14 @@ public class HSQLDBAccountRepository implements AccountRepository { /** * 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 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 - * + * @param transferPrivs true if this account was involved in a TRANSFER_PRIVS transaction * @return the report - * * @throws SQLException */ private SponsorshipReport produceSponsorShipReport( @@ -1260,7 +1260,8 @@ public class HSQLDBAccountRepository implements AccountRepository { int blocksMinted, int blocksMintedAdjustment, int blocksMintedPenalty, - List sponseeAddresses) throws SQLException { + List sponseeAddresses, + boolean transferPrivs) throws SQLException, DataException { int sponseeCount = sponseeAddresses.size(); @@ -1275,32 +1276,41 @@ public class HSQLDBAccountRepository implements AccountRepository { // count the arbitrary and transfer asset transactions for all sponsees ResultSet txTypeResultSet = getTxTypeResultSet(sponseeAddresses, sponseeCount); - int arbitraryCount = 0; - int transferAssetCount = 0; + int arbitraryCount; + int transferAssetCount; + int transferPrivsCount; 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); + Map countsByType = new HashMap<>(2); - // if there is another result, then get - if (txTypeResultSet.next()) - txType = txTypeResultSet.getInt(1); - } + do{ + Integer type = 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(); - } + if( type != null ) { + countsByType.put(type, txTypeResultSet.getInt(2)); + } + } while( txTypeResultSet.next()); + + arbitraryCount = countsByType.getOrDefault(10, 0); + transferAssetCount = countsByType.getOrDefault(12, 0); + transferPrivsCount = countsByType.getOrDefault(40, 0); } + else { + throw new DataException("trouble fetching counts for transaction types"); + } + // 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); + int sellCount; + int sellAmount; + + int buyCount; + int buyAmount; + // if there are results, then fill in the buy/sell amount/counts if( buySellResultSet != null ) { @@ -1317,31 +1327,15 @@ public class HSQLDBAccountRepository implements AccountRepository { } 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); + sellCount = countsByDirection.getOrDefault(SELL, 0); + sellAmount = amountsByDirection.getOrDefault(SELL, 0); + buyCount = countsByDirection.getOrDefault(BUY, 0); + buyAmount = amountsByDirection.getOrDefault(BUY, 0); + } + else { + throw new DataException("trouble fetching counts for buy/sell transactions"); } - // otherwise use zeros for the counts and amounts return new SponsorshipReport( address, @@ -1349,16 +1343,18 @@ public class HSQLDBAccountRepository implements AccountRepository { blocksMinted, blocksMintedAdjustment, blocksMintedPenalty, + transferPrivs, sponseeNames.toArray(new String[sponseeNames.size()]), sponseeCount, sponseeCount - sponseeNames.size(), avgBalance, arbitraryCount, transferAssetCount, - 0, - 0, - 0, - 0); + transferPrivsCount, + sellCount, + sellAmount, + buyCount, + buyAmount); } private ResultSet getBuySellResultSet(List sponseeAddresses, int sponseeCount) throws SQLException { @@ -1392,8 +1388,9 @@ public class HSQLDBAccountRepository implements AccountRepository { StringBuffer accountSql = new StringBuffer(); - accountSql.append( "SELECT DISTINCT account, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty "); - accountSql.append( "FROM ACCOUNTS "); + accountSql.append( "SELECT DISTINCT a.account, a.level, a.blocks_minted, a.blocks_minted_adjustment, a.blocks_minted_penalty, tx.sender IS NOT NULL as transfer "); + accountSql.append( "FROM ACCOUNTS a "); + accountSql.append( "LEFT JOIN TRANSFERPRIVSTRANSACTIONS tx on a.public_key = tx.sender or a.account = tx.recipient "); accountSql.append( "WHERE account = ? "); ResultSet accountResultSet = this.repository.checkedExecute( accountSql.toString(), account); @@ -1413,7 +1410,7 @@ public class HSQLDBAccountRepository implements AccountRepository { 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(") and type in (10, 12, 40) "); txTypeTotalsSql.append("group by type order by type"); String[] sponsees = sponseeAddresses.toArray(new String[sponseeCount]); From 5a691762ed5c6736c6adf84d727555423c044b49 Mon Sep 17 00:00:00 2001 From: kennycud Date: Mon, 23 Sep 2024 17:07:04 -0700 Subject: [PATCH 04/14] bug fix, no rows should return zero counts instead of throwing an exception --- .../repository/hsqldb/HSQLDBAccountRepository.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index ede1ddf9..d33434d1 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -1296,8 +1296,11 @@ public class HSQLDBAccountRepository implements AccountRepository { transferAssetCount = countsByType.getOrDefault(12, 0); transferPrivsCount = countsByType.getOrDefault(40, 0); } + // no rows -> no counts else { - throw new DataException("trouble fetching counts for transaction types"); + arbitraryCount = 0; + transferAssetCount = 0; + transferPrivsCount = 0; } @@ -1333,8 +1336,13 @@ public class HSQLDBAccountRepository implements AccountRepository { buyCount = countsByDirection.getOrDefault(BUY, 0); buyAmount = amountsByDirection.getOrDefault(BUY, 0); } + // no rows -> no counts else { - throw new DataException("trouble fetching counts for buy/sell transactions"); + sellCount = 0; + sellAmount = 0; + + buyCount = 0; + buyAmount = 0; } return new SponsorshipReport( From aba4c6000ffce108ec356caa6ff53e1fb75c97c4 Mon Sep 17 00:00:00 2001 From: crowetic Date: Fri, 27 Sep 2024 18:00:32 -0700 Subject: [PATCH 05/14] added new '/addresses/levels' API call that will pull an array of accounts with their levels, based on an input number. Accounts and levels at or above the input number will be pulled and displayed along with their level. Thanks to @kennycud! --- .../api/resource/AddressesResource.java | 36 +++++++++++++--- .../data/account/AddressLevelPairing.java | 43 +++++++++++++++++++ .../qortal/repository/AccountRepository.java | 4 +- .../hsqldb/HSQLDBAccountRepository.java | 34 +++++++++++++++ 4 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/qortal/data/account/AddressLevelPairing.java diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index 349dd89d..2f2dd529 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -20,10 +20,7 @@ import org.qortal.asset.Asset; import org.qortal.controller.LiteNode; import org.qortal.controller.OnlineAccountsManager; 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.account.*; import org.qortal.data.network.OnlineAccountData; import org.qortal.data.network.OnlineAccountLevel; import org.qortal.data.transaction.PublicizeTransactionData; @@ -668,7 +665,7 @@ public class AddressesResource { description = "Returns sponsorship statistics for an account's sponsor", responses = { @ApiResponse( - description = "the statistics", + description = "statistics", content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = SponsorshipReport.class))) ) } @@ -698,4 +695,31 @@ public class AddressesResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } } -} + + + @GET + @Path("/levels/{minLevel}") + @Operation( + summary = "Return accounts with levels greater than or equal to input", + responses = { + @ApiResponse( + description = "online accounts", + content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AddressLevelPairing.class))) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + + public List getAddressLevelPairings(@PathParam("minLevel") int minLevel) { + + try (final Repository repository = RepositoryManager.getRepository()) { + + // get the level address pairings + List pairings = repository.getAccountRepository().getAddressLevelPairings(minLevel); + + return pairings; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/data/account/AddressLevelPairing.java b/src/main/java/org/qortal/data/account/AddressLevelPairing.java new file mode 100644 index 00000000..f6156c0b --- /dev/null +++ b/src/main/java/org/qortal/data/account/AddressLevelPairing.java @@ -0,0 +1,43 @@ +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 AddressLevelPairing { + + private String address; + + private int level; + + // Constructors + + // For JAXB + protected AddressLevelPairing() { + } + + public AddressLevelPairing(String address, int level) { + this.address = address; + this.level = level; + } + + // Getters / setters + + + public String getAddress() { + return address; + } + + public int getLevel() { + return level; + } + @Override + public String toString() { + return "SponsorshipReport{" + + "address='" + address + '\'' + + ", level=" + level + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/repository/AccountRepository.java b/src/main/java/org/qortal/repository/AccountRepository.java index d1ade684..eec8d736 100644 --- a/src/main/java/org/qortal/repository/AccountRepository.java +++ b/src/main/java/org/qortal/repository/AccountRepository.java @@ -165,7 +165,9 @@ public interface AccountRepository { */ public Optional getSponsor(String address) throws DataException; - /** How to order results when fetching asset balances. */ + public List getAddressLevelPairings(int minLevel) throws DataException; + + /** How to order results when fetching asset balances. */ public enum BalanceOrdering { /** assetID first, then balance, then account address */ ASSET_BALANCE_ACCOUNT, diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index d33434d1..bf71c77f 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -1,5 +1,6 @@ package org.qortal.repository.hsqldb; +import cash.z.wallet.sdk.rpc.Service; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.asset.Asset; @@ -1241,6 +1242,39 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + @Override + public List getAddressLevelPairings(int minLevel) throws DataException { + + StringBuffer accLevelSql = new StringBuffer(51); + + accLevelSql.append( "SELECT account,level FROM ACCOUNTS WHERE level >= ?" ); + + try { + ResultSet accountLevelResultSet = this.repository.checkedExecute(accLevelSql.toString(),minLevel); + + List addressLevelPairings; + + if( accountLevelResultSet == null ) { + addressLevelPairings = new ArrayList<>(0); + } + else { + addressLevelPairings = new ArrayList<>(); + + do { + AddressLevelPairing pairing + = new AddressLevelPairing( + accountLevelResultSet.getString(1), + accountLevelResultSet.getInt(2) + ); + addressLevelPairings.add(pairing); + } while (accountLevelResultSet.next()); + } + return addressLevelPairings; + } catch (SQLException e) { + throw new DataException("Can't get addresses for this level from blockchain data", e); + } + } + /** * Produce Sponsorship Report * From bbf2787ba4ec919a7a6d73f254c172107c15da36 Mon Sep 17 00:00:00 2001 From: kennycud Date: Fri, 27 Sep 2024 19:25:46 -0700 Subject: [PATCH 06/14] removed import that was put in by accident on a merge --- .../hsqldb/HSQLDBAccountRepository.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index d33434d1..f97e5817 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -1241,6 +1241,39 @@ public class HSQLDBAccountRepository implements AccountRepository { } } + @Override + public List getAddressLevelPairings(int minLevel) throws DataException { + + StringBuffer accLevelSql = new StringBuffer(51); + + accLevelSql.append( "SELECT account,level FROM ACCOUNTS WHERE level >= ?" ); + + try { + ResultSet accountLevelResultSet = this.repository.checkedExecute(accLevelSql.toString(),minLevel); + + List addressLevelPairings; + + if( accountLevelResultSet == null ) { + addressLevelPairings = new ArrayList<>(0); + } + else { + addressLevelPairings = new ArrayList<>(); + + do { + AddressLevelPairing pairing + = new AddressLevelPairing( + accountLevelResultSet.getString(1), + accountLevelResultSet.getInt(2) + ); + addressLevelPairings.add(pairing); + } while (accountLevelResultSet.next()); + } + return addressLevelPairings; + } catch (SQLException e) { + throw new DataException("Can't get addresses for this level from blockchain data", e); + } + } + /** * Produce Sponsorship Report * From 639e1df531bb7082be87917d019a342c4f62f69f Mon Sep 17 00:00:00 2001 From: kennycud Date: Sat, 28 Sep 2024 12:52:55 -0700 Subject: [PATCH 07/14] in sponsorship reports, exclude the recipients that get real reward shares --- .../api/resource/AddressesResource.java | 16 ++++++---- .../qortal/repository/AccountRepository.java | 14 ++++---- .../hsqldb/HSQLDBAccountRepository.java | 32 ++++++++++++++++--- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index 2f2dd529..de9de821 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -633,7 +633,7 @@ public class AddressesResource { @Path("/sponsorship/{address}") @Operation( summary = "Returns sponsorship statistics for an account", - description = "Returns sponsorship statistics for an account", + description = "Returns sponsorship statistics for an account, excluding the recipients that get real reward shares", responses = { @ApiResponse( description = "the statistics", @@ -642,12 +642,14 @@ public class AddressesResource { } ) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) - public SponsorshipReport getSponsorshipReport(@PathParam("address") String address) { + public SponsorshipReport getSponsorshipReport( + @PathParam("address") String address, + @QueryParam(("realRewardShareRecipient")) String[] realRewardShareRecipients) { if (!Crypto.isValidAddress(address)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); try (final Repository repository = RepositoryManager.getRepository()) { - SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(address); + SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(address, realRewardShareRecipients); // Not found? if (report == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); @@ -662,7 +664,7 @@ public class AddressesResource { @Path("/sponsorship/{address}/sponsor") @Operation( summary = "Returns sponsorship statistics for an account's sponsor", - description = "Returns sponsorship statistics for an account's sponsor", + description = "Returns sponsorship statistics for an account's sponsor, excluding the recipients that get real reward shares", responses = { @ApiResponse( description = "statistics", @@ -671,7 +673,9 @@ public class AddressesResource { } ) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) - public SponsorshipReport getSponsorshipReportForSponsor(@PathParam("address") String address) { + public SponsorshipReport getSponsorshipReportForSponsor( + @PathParam("address") String address, + @QueryParam("realRewardShareRecipient") String[] realRewardShareRecipients) { if (!Crypto.isValidAddress(address)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); @@ -684,7 +688,7 @@ public class AddressesResource { if(sponsor.isEmpty()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); // get report for sponsor - SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(sponsor.get()); + SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(sponsor.get(), realRewardShareRecipients); // Not found? if (report == null) diff --git a/src/main/java/org/qortal/repository/AccountRepository.java b/src/main/java/org/qortal/repository/AccountRepository.java index eec8d736..f0631d95 100644 --- a/src/main/java/org/qortal/repository/AccountRepository.java +++ b/src/main/java/org/qortal/repository/AccountRepository.java @@ -135,24 +135,22 @@ public interface AccountRepository { /** * Get Sponsorship Report * - * @param address the sponsor's account address - * + * @param address the sponsor's account address + * @param realRewardShareRecipients the recipients that get real reward shares, not sponsorship * @return the report - * * @throws DataException */ - public SponsorshipReport getSponsorshipReport(String address) throws DataException; + public SponsorshipReport getSponsorshipReport(String address, String[] realRewardShareRecipients) throws DataException; /** * Get Sponsee Addresses * - * @param account the sponsor's account address - * + * @param account the sponsor's account address + * @param realRewardShareRecipients the recipients that get real reward shares, not sponsorship * @return the sponsee addresses - * * @throws DataException */ - public List getSponseeAddresses(String account) throws DataException; + public List getSponseeAddresses(String account, String[] realRewardShareRecipients) throws DataException; /** * Get Sponsor diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index bf71c77f..9a3ddd9a 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -1,6 +1,5 @@ package org.qortal.repository.hsqldb; -import cash.z.wallet.sdk.rpc.Service; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.asset.Asset; @@ -1158,7 +1157,7 @@ public class HSQLDBAccountRepository implements AccountRepository { } @Override - public SponsorshipReport getSponsorshipReport(String account) throws DataException { + public SponsorshipReport getSponsorshipReport(String account, String[] realRewardShareRecipients) throws DataException { try { ResultSet accountResultSet = getAccountResultSet(account); @@ -1171,7 +1170,7 @@ public class HSQLDBAccountRepository implements AccountRepository { int penalties = accountResultSet.getInt(5); boolean transferPrivs = accountResultSet.getBoolean(6); - List sponseeAddresses = getSponseeAddresses(account); + List sponseeAddresses = getSponseeAddresses(account, realRewardShareRecipients); if( sponseeAddresses.isEmpty() ){ return new SponsorshipReport(account, level, blocksMinted, adjustments, penalties, transferPrivs, new String[0], 0, 0,0, 0, 0, 0, 0, 0, 0, 0); @@ -1187,7 +1186,7 @@ public class HSQLDBAccountRepository implements AccountRepository { } @Override - public List getSponseeAddresses(String account) throws DataException { + public List getSponseeAddresses(String account, String[] realRewardShareRecipients) throws DataException { StringBuffer sponseeSql = new StringBuffer(); sponseeSql.append( "SELECT DISTINCT t.recipient sponsees " ); @@ -1196,7 +1195,30 @@ public class HSQLDBAccountRepository implements AccountRepository { sponseeSql.append( "WHERE account = ? and t.recipient != a.account"); try { - ResultSet sponseeResultSet = this.repository.checkedExecute(sponseeSql.toString(), account); + ResultSet sponseeResultSet; + + // if there are real reward share recipeints to exclude + if (realRewardShareRecipients != null && realRewardShareRecipients.length > 0) { + + // add constraint to where clause + sponseeSql.append(" and t.recipient NOT IN ("); + sponseeSql.append(String.join(", ", Collections.nCopies(realRewardShareRecipients.length, "?"))); + sponseeSql.append(")"); + + // Create a new array to hold both + String[] combinedArray = new String[realRewardShareRecipients.length + 1]; + + // Add the single string to the first position + combinedArray[0] = account; + + // Copy the elements from realRewardShareRecipients to the combinedArray starting from index 1 + System.arraycopy(realRewardShareRecipients, 0, combinedArray, 1, realRewardShareRecipients.length); + + sponseeResultSet = this.repository.checkedExecute(sponseeSql.toString(), combinedArray); + } + else { + sponseeResultSet = this.repository.checkedExecute(sponseeSql.toString(), account); + } List sponseeAddresses; From 68a2e65fc7fa6b1fd3531c34ad3e74791d32e799 Mon Sep 17 00:00:00 2001 From: kennycud Date: Sun, 29 Sep 2024 05:50:30 -0700 Subject: [PATCH 08/14] added MintershipReport endpoint, restructured the sponsorship report logic, so the MintershipReport could take advantage of the common logic, fixed some of the annotations in the sponsorship report endpoints --- .../api/resource/AddressesResource.java | 45 ++++++++++++++++--- ...rshipReport.java => MintershipReport.java} | 8 ++-- .../qortal/repository/AccountRepository.java | 9 ++-- .../hsqldb/HSQLDBAccountRepository.java | 19 +++++--- 4 files changed, 62 insertions(+), 19 deletions(-) rename src/main/java/org/qortal/data/account/{SponsorshipReport.java => MintershipReport.java} (87%) diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index de9de821..6a716f97 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -637,19 +637,19 @@ public class AddressesResource { responses = { @ApiResponse( description = "the statistics", - content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = SponsorshipReport.class))) + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = MintershipReport.class)) ) } ) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) - public SponsorshipReport getSponsorshipReport( + public MintershipReport getSponsorshipReport( @PathParam("address") String address, @QueryParam(("realRewardShareRecipient")) String[] realRewardShareRecipients) { if (!Crypto.isValidAddress(address)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); try (final Repository repository = RepositoryManager.getRepository()) { - SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(address, realRewardShareRecipients); + MintershipReport report = repository.getAccountRepository().getSponsorshipReport(address, realRewardShareRecipients); // Not found? if (report == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); @@ -667,13 +667,13 @@ public class AddressesResource { description = "Returns sponsorship statistics for an account's sponsor, excluding the recipients that get real reward shares", responses = { @ApiResponse( - description = "statistics", - content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = SponsorshipReport.class))) + description = "the statistics", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = MintershipReport.class)) ) } ) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) - public SponsorshipReport getSponsorshipReportForSponsor( + public MintershipReport getSponsorshipReportForSponsor( @PathParam("address") String address, @QueryParam("realRewardShareRecipient") String[] realRewardShareRecipients) { if (!Crypto.isValidAddress(address)) @@ -688,7 +688,7 @@ public class AddressesResource { if(sponsor.isEmpty()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); // get report for sponsor - SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(sponsor.get(), realRewardShareRecipients); + MintershipReport report = repository.getAccountRepository().getSponsorshipReport(sponsor.get(), realRewardShareRecipients); // Not found? if (report == null) @@ -700,6 +700,37 @@ public class AddressesResource { } } + @GET + @Path("/mintership/{address}") + @Operation( + summary = "Returns mintership statistics for an account", + description = "Returns mintership statistics for an account", + responses = { + @ApiResponse( + description = "the statistics", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = MintershipReport.class)) + ) + } + ) + @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) + public MintershipReport getMintershipReport(@PathParam("address") String address ) { + if (!Crypto.isValidAddress(address)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + try (final Repository repository = RepositoryManager.getRepository()) { + + // get report for sponsor + MintershipReport report = repository.getAccountRepository().getMintershipReport(address, account -> List.of(account)); + + // 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("/levels/{minLevel}") diff --git a/src/main/java/org/qortal/data/account/SponsorshipReport.java b/src/main/java/org/qortal/data/account/MintershipReport.java similarity index 87% rename from src/main/java/org/qortal/data/account/SponsorshipReport.java rename to src/main/java/org/qortal/data/account/MintershipReport.java index b08051ef..c1f2674e 100644 --- a/src/main/java/org/qortal/data/account/SponsorshipReport.java +++ b/src/main/java/org/qortal/data/account/MintershipReport.java @@ -6,7 +6,7 @@ import java.util.Arrays; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) -public class SponsorshipReport { +public class MintershipReport { private String address; @@ -45,10 +45,10 @@ public class SponsorshipReport { // Constructors // For JAXB - protected SponsorshipReport() { + protected MintershipReport() { } - public SponsorshipReport(String address, int level, int blocksMinted, int adjustments, int penalties, boolean transfer, String[] names, int sponseeCount, int nonRegisteredCount, int avgBalance, int arbitraryCount, int transferAssetCount, int transferPrivsCount, int sellCount, int sellAmount, int buyCount, int buyAmount) { + public MintershipReport(String address, int level, int blocksMinted, int adjustments, int penalties, boolean transfer, String[] names, int sponseeCount, int nonRegisteredCount, int avgBalance, int arbitraryCount, int transferAssetCount, int transferPrivsCount, int sellCount, int sellAmount, int buyCount, int buyAmount) { this.address = address; this.level = level; this.blocksMinted = blocksMinted; @@ -141,7 +141,7 @@ public class SponsorshipReport { @Override public String toString() { - return "SponsorshipReport{" + + return "MintershipReport{" + "address='" + address + '\'' + ", level=" + level + ", blocksMinted=" + blocksMinted + diff --git a/src/main/java/org/qortal/repository/AccountRepository.java b/src/main/java/org/qortal/repository/AccountRepository.java index f0631d95..8d1390f4 100644 --- a/src/main/java/org/qortal/repository/AccountRepository.java +++ b/src/main/java/org/qortal/repository/AccountRepository.java @@ -5,6 +5,7 @@ import org.qortal.data.account.*; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Function; public interface AccountRepository { @@ -132,15 +133,17 @@ public interface AccountRepository { /** Returns all account balances for given assetID, optionally excluding zero balances. */ public List getAssetBalances(long assetId, Boolean excludeZero) throws DataException; + public MintershipReport getSponsorshipReport(String address, String[] realRewardShareRecipients) throws DataException; + /** * Get Sponsorship Report * - * @param address the sponsor's account address - * @param realRewardShareRecipients the recipients that get real reward shares, not sponsorship + * @param address the account address + * @param addressFetcher fetches the addresses that this method will aggregate * @return the report * @throws DataException */ - public SponsorshipReport getSponsorshipReport(String address, String[] realRewardShareRecipients) throws DataException; + public MintershipReport getMintershipReport(String address, Function> addressFetcher) throws DataException; /** * Get Sponsee Addresses diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index 9a3ddd9a..5ea8e3b0 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import static org.qortal.utils.Amounts.prettyAmount; @@ -1157,7 +1158,15 @@ public class HSQLDBAccountRepository implements AccountRepository { } @Override - public SponsorshipReport getSponsorshipReport(String account, String[] realRewardShareRecipients) throws DataException { + public MintershipReport getSponsorshipReport(String address, String[] realRewardShareRecipients) throws DataException { + + List sponsees = getSponseeAddresses(address, realRewardShareRecipients); + + return getMintershipReport(address, account -> sponsees); + } + + @Override + public MintershipReport getMintershipReport(String account, Function> addressFetcher) throws DataException { try { ResultSet accountResultSet = getAccountResultSet(account); @@ -1170,10 +1179,10 @@ public class HSQLDBAccountRepository implements AccountRepository { int penalties = accountResultSet.getInt(5); boolean transferPrivs = accountResultSet.getBoolean(6); - List sponseeAddresses = getSponseeAddresses(account, realRewardShareRecipients); + List sponseeAddresses = addressFetcher.apply(account); if( sponseeAddresses.isEmpty() ){ - return new SponsorshipReport(account, level, blocksMinted, adjustments, penalties, transferPrivs, new String[0], 0, 0,0, 0, 0, 0, 0, 0, 0, 0); + return new MintershipReport(account, level, blocksMinted, adjustments, penalties, transferPrivs, new String[0], 0, 0,0, 0, 0, 0, 0, 0, 0, 0); } else { return produceSponsorShipReport(account, level, blocksMinted, adjustments, penalties, sponseeAddresses, transferPrivs); @@ -1310,7 +1319,7 @@ public class HSQLDBAccountRepository implements AccountRepository { * @return the report * @throws SQLException */ - private SponsorshipReport produceSponsorShipReport( + private MintershipReport produceSponsorShipReport( String address, int level, int blocksMinted, @@ -1401,7 +1410,7 @@ public class HSQLDBAccountRepository implements AccountRepository { buyAmount = 0; } - return new SponsorshipReport( + return new MintershipReport( address, level, blocksMinted, From 2b83c4bbf3f45522ce7dbd1dd22c90b3a674c776 Mon Sep 17 00:00:00 2001 From: kennycud Date: Sun, 29 Sep 2024 08:37:16 -0700 Subject: [PATCH 09/14] fixed the query to get registered names --- .../repository/hsqldb/HSQLDBAccountRepository.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index 5ea8e3b0..0e43271f 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -1516,18 +1516,12 @@ public class HSQLDBAccountRepository implements AccountRepository { List 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; @@ -1535,10 +1529,8 @@ public class HSQLDBAccountRepository implements AccountRepository { private ResultSet getNamesResultSet(List 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("SELECT name FROM NAMES "); + namesSql.append("WHERE owner in ("); namesSql.append(String.join(", ", Collections.nCopies(sponseeCount, "?"))); namesSql.append(")"); From 39da7edf5acb436e70a0cf0a2aa4c2626358b50c Mon Sep 17 00:00:00 2001 From: kennycud Date: Sun, 29 Sep 2024 19:02:06 -0700 Subject: [PATCH 10/14] handled null pointer case caused by the last update --- .../repository/hsqldb/HSQLDBAccountRepository.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index 0e43271f..38a9acbf 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -1330,9 +1330,17 @@ public class HSQLDBAccountRepository implements AccountRepository { int sponseeCount = sponseeAddresses.size(); - // get the registered nanmes of the sponsees + // get the registered names of the sponsees ResultSet namesResultSet = getNamesResultSet(sponseeAddresses, sponseeCount); - List sponseeNames = getNames(namesResultSet, sponseeCount); + + List sponseeNames; + + if( namesResultSet != null ) { + sponseeNames = getNames(namesResultSet, sponseeCount); + } + else { + sponseeNames = new ArrayList<>(0); + } // get the average balance of the sponsees ResultSet avgBalanceResultSet = getAverageBalanceResultSet(sponseeAddresses, sponseeCount); @@ -1505,7 +1513,7 @@ public class HSQLDBAccountRepository implements AccountRepository { /** * Get Names * - * @param namesResultSet the result set to get the names from + * @param namesResultSet the result set to get the names from, can't be null * @param count the number of potential names * * @return the names From 3162a83dc115eff9877ed8934cc2ce3d69ffbd9e Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Mon, 30 Sep 2024 09:08:28 -0400 Subject: [PATCH 11/14] Added bootstrap4.qortal.org to settings --- src/main/java/org/qortal/settings/Settings.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index f18ccd88..14252840 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -272,7 +272,8 @@ public class Settings { private String[] bootstrapHosts = new String[] { "http://bootstrap.qortal.org", "http://bootstrap2.qortal.org", - "http://bootstrap3.qortal.org" + "http://bootstrap3.qortal.org", + "http://bootstrap4.qortal.org" }; // Auto-update sources From f105af696d95184d66e82f820d697fb1dbd4c03d Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Mon, 30 Sep 2024 09:25:07 -0400 Subject: [PATCH 12/14] removed API restriction from pubkey conversion --- src/main/java/org/qortal/api/resource/AddressesResource.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index 66d8412c..6b3a64c0 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -327,11 +327,8 @@ public class AddressesResource { ) } ) - @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.NON_PRODUCTION, ApiError.REPOSITORY_ISSUE}) + @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.REPOSITORY_ISSUE}) public String fromPublicKey(@PathParam("publickey") String publicKey58) { - if (Settings.getInstance().isApiRestricted()) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); - // Decode public key byte[] publicKey; try { From d9ad0bd663c1b1d8d92c3ea6e3fec146ee9b13ed Mon Sep 17 00:00:00 2001 From: kennycud Date: Thu, 3 Oct 2024 12:42:44 -0700 Subject: [PATCH 13/14] sponsorship endpoints now return a different report type than the mintership endpoint --- .../api/resource/AddressesResource.java | 48 +++-- .../qortal/data/account/MintershipReport.java | 30 ++-- .../data/account/SponsorshipReport.java | 164 ++++++++++++++++++ .../qortal/repository/AccountRepository.java | 4 +- .../hsqldb/HSQLDBAccountRepository.java | 10 +- 5 files changed, 220 insertions(+), 36 deletions(-) create mode 100644 src/main/java/org/qortal/data/account/SponsorshipReport.java diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index 6a716f97..d0937fde 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -637,19 +637,19 @@ public class AddressesResource { responses = { @ApiResponse( description = "the statistics", - content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = MintershipReport.class)) + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SponsorshipReport.class)) ) } ) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) - public MintershipReport getSponsorshipReport( + public SponsorshipReport getSponsorshipReport( @PathParam("address") String address, @QueryParam(("realRewardShareRecipient")) String[] realRewardShareRecipients) { if (!Crypto.isValidAddress(address)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); try (final Repository repository = RepositoryManager.getRepository()) { - MintershipReport report = repository.getAccountRepository().getSponsorshipReport(address, realRewardShareRecipients); + SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(address, realRewardShareRecipients); // Not found? if (report == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); @@ -668,12 +668,12 @@ public class AddressesResource { responses = { @ApiResponse( description = "the statistics", - content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = MintershipReport.class)) + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SponsorshipReport.class)) ) } ) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) - public MintershipReport getSponsorshipReportForSponsor( + public SponsorshipReport getSponsorshipReportForSponsor( @PathParam("address") String address, @QueryParam("realRewardShareRecipient") String[] realRewardShareRecipients) { if (!Crypto.isValidAddress(address)) @@ -688,7 +688,7 @@ public class AddressesResource { if(sponsor.isEmpty()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); // get report for sponsor - MintershipReport report = repository.getAccountRepository().getSponsorshipReport(sponsor.get(), realRewardShareRecipients); + SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(sponsor.get(), realRewardShareRecipients); // Not found? if (report == null) @@ -713,20 +713,48 @@ public class AddressesResource { } ) @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) - public MintershipReport getMintershipReport(@PathParam("address") String address ) { + public MintershipReport getMintershipReport(@PathParam("address") String address, + @QueryParam("realRewardShareRecipient") String[] realRewardShareRecipients ) { if (!Crypto.isValidAddress(address)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); try (final Repository repository = RepositoryManager.getRepository()) { - // get report for sponsor - MintershipReport report = repository.getAccountRepository().getMintershipReport(address, account -> List.of(account)); + // get sponsorship report for minter, fetch a list of one minter + SponsorshipReport report = repository.getAccountRepository().getMintershipReport(address, account -> List.of(account)); // Not found? if (report == null) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); - return report; + // since the report is for one minter, must get sponsee count separately + int sponseeCount = repository.getAccountRepository().getSponseeAddresses(address, realRewardShareRecipients).size(); + + // since the report is for one minter, must get the first name from a array of names that should be size 1 + String name = report.getNames().length > 0 ? report.getNames()[0] : null; + + // transform sponsorship report to mintership report + MintershipReport mintershipReport + = new MintershipReport( + report.getAddress(), + report.getLevel(), + report.getBlocksMinted(), + report.getAdjustments(), + report.getPenalties(), + report.isTransfer(), + name, + sponseeCount, + report.getAvgBalance(), + report.getArbitraryCount(), + report.getTransferAssetCount(), + report.getTransferPrivsCount(), + report.getSellCount(), + report.getSellAmount(), + report.getBuyCount(), + report.getBuyAmount() + ); + + return mintershipReport; } catch (DataException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); } diff --git a/src/main/java/org/qortal/data/account/MintershipReport.java b/src/main/java/org/qortal/data/account/MintershipReport.java index c1f2674e..e36a981b 100644 --- a/src/main/java/org/qortal/data/account/MintershipReport.java +++ b/src/main/java/org/qortal/data/account/MintershipReport.java @@ -20,13 +20,11 @@ public class MintershipReport { private boolean transfer; - private String[] names; + private String name; private int sponseeCount; - private int nonRegisteredCount; - - private int avgBalance; + private int balance; private int arbitraryCount; @@ -48,17 +46,16 @@ public class MintershipReport { protected MintershipReport() { } - public MintershipReport(String address, int level, int blocksMinted, int adjustments, int penalties, boolean transfer, String[] names, int sponseeCount, int nonRegisteredCount, int avgBalance, int arbitraryCount, int transferAssetCount, int transferPrivsCount, int sellCount, int sellAmount, int buyCount, int buyAmount) { + public MintershipReport(String address, int level, int blocksMinted, int adjustments, int penalties, boolean transfer, String name, int sponseeCount, int balance, int arbitraryCount, int transferAssetCount, int transferPrivsCount, int sellCount, int sellAmount, int buyCount, int buyAmount) { this.address = address; this.level = level; this.blocksMinted = blocksMinted; this.adjustments = adjustments; this.penalties = penalties; this.transfer = transfer; - this.names = names; + this.name = name; this.sponseeCount = sponseeCount; - this.nonRegisteredCount = nonRegisteredCount; - this.avgBalance = avgBalance; + this.balance = balance; this.arbitraryCount = arbitraryCount; this.transferAssetCount = transferAssetCount; this.transferPrivsCount = transferPrivsCount; @@ -95,20 +92,16 @@ public class MintershipReport { return transfer; } - public String[] getNames() { - return names; + public String getName() { + return name; } public int getSponseeCount() { return sponseeCount; } - public int getNonRegisteredCount() { - return nonRegisteredCount; - } - - public int getAvgBalance() { - return avgBalance; + public int getBalance() { + return balance; } public int getArbitraryCount() { @@ -148,10 +141,9 @@ public class MintershipReport { ", adjustments=" + adjustments + ", penalties=" + penalties + ", transfer=" + transfer + - ", names=" + Arrays.toString(names) + + ", name='" + name + '\'' + ", sponseeCount=" + sponseeCount + - ", nonRegisteredCount=" + nonRegisteredCount + - ", avgBalance=" + avgBalance + + ", balance=" + balance + ", arbitraryCount=" + arbitraryCount + ", transferAssetCount=" + transferAssetCount + ", transferPrivsCount=" + transferPrivsCount + diff --git a/src/main/java/org/qortal/data/account/SponsorshipReport.java b/src/main/java/org/qortal/data/account/SponsorshipReport.java new file mode 100644 index 00000000..7b518363 --- /dev/null +++ b/src/main/java/org/qortal/data/account/SponsorshipReport.java @@ -0,0 +1,164 @@ +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 boolean transfer; + + private String[] names; + + private int sponseeCount; + + private int nonRegisteredCount; + + private int avgBalance; + + private int arbitraryCount; + + private int transferAssetCount; + + private int transferPrivsCount; + + 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, boolean transfer, String[] names, int sponseeCount, int nonRegisteredCount, int avgBalance, int arbitraryCount, int transferAssetCount, int transferPrivsCount, int sellCount, int sellAmount, int buyCount, int buyAmount) { + this.address = address; + this.level = level; + this.blocksMinted = blocksMinted; + this.adjustments = adjustments; + this.penalties = penalties; + this.transfer = transfer; + this.names = names; + this.sponseeCount = sponseeCount; + this.nonRegisteredCount = nonRegisteredCount; + this.avgBalance = avgBalance; + this.arbitraryCount = arbitraryCount; + this.transferAssetCount = transferAssetCount; + this.transferPrivsCount = transferPrivsCount; + 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 boolean isTransfer() { + return transfer; + } + + 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 getTransferPrivsCount() { + return transferPrivsCount; + } + + 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 "MintershipReport{" + + "address='" + address + '\'' + + ", level=" + level + + ", blocksMinted=" + blocksMinted + + ", adjustments=" + adjustments + + ", penalties=" + penalties + + ", transfer=" + transfer + + ", names=" + Arrays.toString(names) + + ", sponseeCount=" + sponseeCount + + ", nonRegisteredCount=" + nonRegisteredCount + + ", avgBalance=" + avgBalance + + ", arbitraryCount=" + arbitraryCount + + ", transferAssetCount=" + transferAssetCount + + ", transferPrivsCount=" + transferPrivsCount + + ", sellCount=" + sellCount + + ", sellAmount=" + sellAmount + + ", buyCount=" + buyCount + + ", buyAmount=" + buyAmount + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/repository/AccountRepository.java b/src/main/java/org/qortal/repository/AccountRepository.java index 8d1390f4..f68fe8eb 100644 --- a/src/main/java/org/qortal/repository/AccountRepository.java +++ b/src/main/java/org/qortal/repository/AccountRepository.java @@ -133,7 +133,7 @@ public interface AccountRepository { /** Returns all account balances for given assetID, optionally excluding zero balances. */ public List getAssetBalances(long assetId, Boolean excludeZero) throws DataException; - public MintershipReport getSponsorshipReport(String address, String[] realRewardShareRecipients) throws DataException; + public SponsorshipReport getSponsorshipReport(String address, String[] realRewardShareRecipients) throws DataException; /** * Get Sponsorship Report @@ -143,7 +143,7 @@ public interface AccountRepository { * @return the report * @throws DataException */ - public MintershipReport getMintershipReport(String address, Function> addressFetcher) throws DataException; + public SponsorshipReport getMintershipReport(String address, Function> addressFetcher) throws DataException; /** * Get Sponsee Addresses diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index 38a9acbf..a7548438 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -1158,7 +1158,7 @@ public class HSQLDBAccountRepository implements AccountRepository { } @Override - public MintershipReport getSponsorshipReport(String address, String[] realRewardShareRecipients) throws DataException { + public SponsorshipReport getSponsorshipReport(String address, String[] realRewardShareRecipients) throws DataException { List sponsees = getSponseeAddresses(address, realRewardShareRecipients); @@ -1166,7 +1166,7 @@ public class HSQLDBAccountRepository implements AccountRepository { } @Override - public MintershipReport getMintershipReport(String account, Function> addressFetcher) throws DataException { + public SponsorshipReport getMintershipReport(String account, Function> addressFetcher) throws DataException { try { ResultSet accountResultSet = getAccountResultSet(account); @@ -1182,7 +1182,7 @@ public class HSQLDBAccountRepository implements AccountRepository { List sponseeAddresses = addressFetcher.apply(account); if( sponseeAddresses.isEmpty() ){ - return new MintershipReport(account, level, blocksMinted, adjustments, penalties, transferPrivs, new String[0], 0, 0,0, 0, 0, 0, 0, 0, 0, 0); + return new SponsorshipReport(account, level, blocksMinted, adjustments, penalties, transferPrivs, new String[0], 0, 0,0, 0, 0, 0, 0, 0, 0, 0); } else { return produceSponsorShipReport(account, level, blocksMinted, adjustments, penalties, sponseeAddresses, transferPrivs); @@ -1319,7 +1319,7 @@ public class HSQLDBAccountRepository implements AccountRepository { * @return the report * @throws SQLException */ - private MintershipReport produceSponsorShipReport( + private SponsorshipReport produceSponsorShipReport( String address, int level, int blocksMinted, @@ -1418,7 +1418,7 @@ public class HSQLDBAccountRepository implements AccountRepository { buyAmount = 0; } - return new MintershipReport( + return new SponsorshipReport( address, level, blocksMinted, From 7c9d82b780f0a2c73e6d1fd65a280f24c5884f63 Mon Sep 17 00:00:00 2001 From: kennycud Date: Sat, 5 Oct 2024 12:47:40 -0700 Subject: [PATCH 14/14] fixed the incorrect sell amounts by replacing the complex buy sell query with 2 queries, one for buys and one for sells --- .../hsqldb/HSQLDBAccountRepository.java | 98 +++++++++---------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index a7548438..6b945aa7 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java @@ -1376,44 +1376,34 @@ public class HSQLDBAccountRepository implements AccountRepository { transferPrivsCount = 0; } - - // 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); + ResultSet sellResultSet = getSellResultSet(sponseeAddresses, sponseeCount); int sellCount; int sellAmount; - int buyCount; - int buyAmount; - - // if there are results, then fill in the buy/sell amount/counts - if( buySellResultSet != null ) { - - Map countsByDirection = new HashMap<>(2); - Map 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()); - - - sellCount = countsByDirection.getOrDefault(SELL, 0); - sellAmount = amountsByDirection.getOrDefault(SELL, 0); - - buyCount = countsByDirection.getOrDefault(BUY, 0); - buyAmount = amountsByDirection.getOrDefault(BUY, 0); + // if there are sell results, then fill in the sell amount/counts + if( sellResultSet != null ) { + sellCount = sellResultSet.getInt(1); + sellAmount = sellResultSet.getInt(2); } - // no rows -> no counts + // no rows -> no counts/amounts else { sellCount = 0; sellAmount = 0; + } + ResultSet buyResultSet = getBuyResultSet(sponseeAddresses, sponseeCount); + + int buyCount; + int buyAmount; + + // if there are buy results, then fill in the buy amount/counts + if( buyResultSet != null ) { + buyCount = buyResultSet.getInt(1); + buyAmount = buyResultSet.getInt(2); + } + // no rows -> no counts/amounts + else { buyCount = 0; buyAmount = 0; } @@ -1438,33 +1428,39 @@ public class HSQLDBAccountRepository implements AccountRepository { buyAmount); } - private ResultSet getBuySellResultSet(List 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; "); + private ResultSet getBuyResultSet(List addresses, int addressCount) throws SQLException { - String[] sponsees = sponseeAddresses.toArray(new String[sponseeCount]); - ResultSet buySellResultSet = this.repository.checkedExecute(buySellSql.toString(), sponsees); + StringBuffer sql = new StringBuffer(); + sql.append("SELECT COUNT(*) count, SUM(amount)/100000000 amount "); + sql.append("FROM ACCOUNTS a "); + sql.append("INNER JOIN ATTRANSACTIONS tx ON tx.recipient = a.account "); + sql.append("INNER JOIN ATS ats ON ats.at_address = tx.at_address "); + sql.append("WHERE a.account IN ( "); + sql.append(String.join(", ", Collections.nCopies(addressCount, "?"))); + sql.append(") "); + sql.append("AND a.account = tx.recipient AND a.public_key != ats.creator AND asset_id = 0 "); + String[] sponsees = addresses.toArray(new String[addressCount]); + ResultSet buySellResultSet = this.repository.checkedExecute(sql.toString(), sponsees); return buySellResultSet; } + private ResultSet getSellResultSet(List addresses, int addressCount) throws SQLException { + + StringBuffer sql = new StringBuffer(); + sql.append("SELECT COUNT(*) count, SUM(amount)/100000000 amount "); + sql.append("FROM ATS ats "); + sql.append("INNER JOIN ACCOUNTS a ON a.public_key = ats.creator "); + sql.append("INNER JOIN ATTRANSACTIONS tx ON tx.at_address = ats.at_address "); + sql.append("WHERE a.account IN ( "); + sql.append(String.join(", ", Collections.nCopies(addressCount, "?"))); + sql.append(") "); + sql.append("AND a.account != tx.recipient AND asset_id = 0 "); + String[] sponsees = addresses.toArray(new String[addressCount]); + + return this.repository.checkedExecute(sql.toString(), sponsees); + } + private ResultSet getAccountResultSet(String account) throws SQLException { StringBuffer accountSql = new StringBuffer();