diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index 6b3a64c0..beb73734 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -20,9 +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.*; import org.qortal.data.network.OnlineAccountData; import org.qortal.data.network.OnlineAccountLevel; import org.qortal.data.transaction.PublicizeTransactionData; @@ -52,6 +50,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") @@ -627,4 +626,160 @@ public class AddressesResource { } } -} + @GET + @Path("/sponsorship/{address}") + @Operation( + summary = "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", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SponsorshipReport.class)) + ) + } + ) + @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) + 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, realRewardShareRecipients); + // 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, excluding the recipients that get real reward shares", + responses = { + @ApiResponse( + description = "the statistics", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SponsorshipReport.class)) + ) + } + ) + @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) + public SponsorshipReport getSponsorshipReportForSponsor( + @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 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(), realRewardShareRecipients); + + // 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("/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, + @QueryParam("realRewardShareRecipient") String[] realRewardShareRecipients ) { + if (!Crypto.isValidAddress(address)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + try (final Repository repository = RepositoryManager.getRepository()) { + + // 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); + + // 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); + } + } + + @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/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/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/data/account/MintershipReport.java b/src/main/java/org/qortal/data/account/MintershipReport.java new file mode 100644 index 00000000..e36a981b --- /dev/null +++ b/src/main/java/org/qortal/data/account/MintershipReport.java @@ -0,0 +1,156 @@ +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 MintershipReport { + + private String address; + + private int level; + + private int blocksMinted; + + private int adjustments; + + private int penalties; + + private boolean transfer; + + private String name; + + private int sponseeCount; + + private int balance; + + 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 MintershipReport() { + } + + 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.name = name; + this.sponseeCount = sponseeCount; + this.balance = balance; + 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 getName() { + return name; + } + + public int getSponseeCount() { + return sponseeCount; + } + + public int getBalance() { + return balance; + } + + 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 + + ", name='" + name + '\'' + + ", sponseeCount=" + sponseeCount + + ", balance=" + balance + + ", 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/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 bdad187b..f68fe8eb 100644 --- a/src/main/java/org/qortal/repository/AccountRepository.java +++ b/src/main/java/org/qortal/repository/AccountRepository.java @@ -3,7 +3,9 @@ package org.qortal.repository; 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 { @@ -131,7 +133,42 @@ public interface AccountRepository { /** Returns all account balances for given assetID, optionally excluding zero balances. */ public List getAssetBalances(long assetId, Boolean excludeZero) throws DataException; - /** How to order results when fetching asset balances. */ + public SponsorshipReport getSponsorshipReport(String address, String[] realRewardShareRecipients) throws DataException; + + /** + * Get Sponsorship Report + * + * @param address the account address + * @param addressFetcher fetches the addresses that this method will aggregate + * @return the report + * @throws DataException + */ + public SponsorshipReport getMintershipReport(String address, Function> addressFetcher) throws DataException; + + /** + * Get Sponsee Addresses + * + * @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, String[] realRewardShareRecipients) 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; + + 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/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/HSQLDBAccountRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBAccountRepository.java index 7aef66ce..6b945aa7 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,20 +10,28 @@ 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.function.Function; import java.util.stream.Collectors; 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 +1157,389 @@ public class HSQLDBAccountRepository implements AccountRepository { } } -} + @Override + public SponsorshipReport getSponsorshipReport(String address, String[] realRewardShareRecipients) throws DataException { + + List sponsees = getSponseeAddresses(address, realRewardShareRecipients); + + return getMintershipReport(address, account -> sponsees); + } + + @Override + public SponsorshipReport getMintershipReport(String account, Function> addressFetcher) 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); + boolean transferPrivs = accountResultSet.getBoolean(6); + + 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); + } + else { + return produceSponsorShipReport(account, level, blocksMinted, adjustments, penalties, sponseeAddresses, transferPrivs); + } + } + 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, String[] realRewardShareRecipients) 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; + + // 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; + + 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); + } + } + + @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 + * + * @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( + String address, + int level, + int blocksMinted, + int blocksMintedAdjustment, + int blocksMintedPenalty, + List sponseeAddresses, + boolean transferPrivs) throws SQLException, DataException { + + int sponseeCount = sponseeAddresses.size(); + + // get the registered names of the sponsees + ResultSet namesResultSet = getNamesResultSet(sponseeAddresses, 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); + int avgBalance = avgBalanceResultSet.getInt(1); + + // count the arbitrary and transfer asset transactions for all sponsees + ResultSet txTypeResultSet = getTxTypeResultSet(sponseeAddresses, sponseeCount); + + int arbitraryCount; + int transferAssetCount; + int transferPrivsCount; + + if( txTypeResultSet != null) { + + Map countsByType = new HashMap<>(2); + + do{ + Integer type = txTypeResultSet.getInt(1); + + 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); + } + // no rows -> no counts + else { + arbitraryCount = 0; + transferAssetCount = 0; + transferPrivsCount = 0; + } + + ResultSet sellResultSet = getSellResultSet(sponseeAddresses, sponseeCount); + + int sellCount; + int sellAmount; + + // 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/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; + } + + return new SponsorshipReport( + address, + level, + blocksMinted, + blocksMintedAdjustment, + blocksMintedPenalty, + transferPrivs, + sponseeNames.toArray(new String[sponseeNames.size()]), + sponseeCount, + sponseeCount - sponseeNames.size(), + avgBalance, + arbitraryCount, + transferAssetCount, + transferPrivsCount, + sellCount, + sellAmount, + buyCount, + buyAmount); + } + + private ResultSet getBuyResultSet(List addresses, int addressCount) throws SQLException { + + 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(); + + 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); + + 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, 40) "); + 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, can't be null + * @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); + + do{ + String name = namesResultSet.getString(1); + + if( name != null ) { + names.add(name); + } + } while( namesResultSet.next() ); + + return names; + } + + private ResultSet getNamesResultSet(List sponseeAddresses, int sponseeCount) throws SQLException { + StringBuffer namesSql = new StringBuffer(); + namesSql.append("SELECT name FROM NAMES "); + namesSql.append("WHERE owner 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 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