3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-11 17:55:50 +00:00

Merge pull request #1 from Qortal/master

Merge Api calls
This commit is contained in:
AlphaX 2024-10-15 19:59:18 +02:00 committed by GitHub
commit 5c7ffce887
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1135 additions and 11 deletions

View File

@ -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")
@ -327,11 +326,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 {
@ -630,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<String> sponsor = repository.getAccountRepository().getSponsor(address);
// if there is not sponsor, throw error
if(sponsor.isEmpty()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// get report for sponsor
SponsorshipReport report = repository.getAccountRepository().getSponsorshipReport(sponsor.get(), 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<AddressLevelPairing> getAddressLevelPairings(@PathParam("minLevel") int minLevel) {
try (final Repository repository = RepositoryManager.getRepository()) {
// get the level address pairings
List<AddressLevelPairing> pairings = repository.getAccountRepository().getAddressLevelPairings(minLevel);
return pairings;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@ -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<ArbitraryResourceData> 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<String> 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<ArbitraryResourceData> 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(

View File

@ -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 +
'}';
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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<AccountBalanceData> 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<String, List<String>> 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<String> 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<String> getSponsor(String address) throws DataException;
public List<AddressLevelPairing> 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,

View File

@ -44,6 +44,17 @@ public interface ArbitraryRepository {
public List<ArbitraryResourceData> searchArbitraryResources(Service service, String query, String identifier, List<String> names, String title, String description, boolean prefixOnly, List<String> 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<ArbitraryResourceData> searchArbitraryResourcesSimple(
Service service,
String identifier,
List<String> names,
boolean prefixOnly,
Long before,
Long after,
Integer limit,
Integer offset,
Boolean reverse,
Boolean caseInsensitive) throws DataException;
// Arbitrary resources cache save/load

View File

@ -1,5 +1,7 @@
package org.qortal.repository.hsqldb;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.asset.Asset;
import org.qortal.data.account.*;
import org.qortal.repository.AccountRepository;
@ -8,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<String> sponsees = getSponseeAddresses(address, realRewardShareRecipients);
return getMintershipReport(address, account -> sponsees);
}
@Override
public SponsorshipReport getMintershipReport(String account, Function<String, List<String>> 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<String> 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<String> 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<String> sponseeAddresses;
if( sponseeResultSet == null ) {
sponseeAddresses = new ArrayList<>(0);
}
else {
sponseeAddresses = new ArrayList<>();
do {
sponseeAddresses.add(sponseeResultSet.getString(1));
} while (sponseeResultSet.next());
}
return sponseeAddresses;
}
catch (SQLException e) {
throw new DataException("can't get sponsees from blockchain data", e);
}
}
@Override
public Optional<String> getSponsor(String address) throws DataException {
StringBuffer sponsorSql = new StringBuffer();
sponsorSql.append( "SELECT DISTINCT account, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty ");
sponsorSql.append( "FROM REWARDSHARETRANSACTIONS t ");
sponsorSql.append( "INNER JOIN ACCOUNTS a on a.public_key = t.minter_public_key ");
sponsorSql.append( "WHERE recipient = ? and recipient != account ");
try {
ResultSet sponseeResultSet = this.repository.checkedExecute(sponsorSql.toString(), address);
if( sponseeResultSet == null ){
return Optional.empty();
}
else {
return Optional.ofNullable( sponseeResultSet.getString(1));
}
} catch (SQLException e) {
throw new DataException("can't get sponsor from blockchain data", e);
}
}
@Override
public List<AddressLevelPairing> 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<AddressLevelPairing> 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<String> sponseeAddresses,
boolean transferPrivs) throws SQLException, DataException {
int sponseeCount = sponseeAddresses.size();
// get the registered names of the sponsees
ResultSet namesResultSet = getNamesResultSet(sponseeAddresses, sponseeCount);
List<String> 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<Integer, Integer> 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<String> 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<String> 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<String> sponseeAddresses, int sponseeCount) throws SQLException {
StringBuffer txTypeTotalsSql = new StringBuffer();
// Transaction Types, int values
// ARBITRARY = 10
// TRANSFER_ASSET = 12
// txTypeTotalsSql.append("
txTypeTotalsSql.append("SELECT type, count(*) ");
txTypeTotalsSql.append("FROM TRANSACTIONPARTICIPANTS ");
txTypeTotalsSql.append("INNER JOIN TRANSACTIONS USING (signature) ");
txTypeTotalsSql.append("where participant in ( ");
txTypeTotalsSql.append(String.join(", ", Collections.nCopies(sponseeCount, "?")));
txTypeTotalsSql.append(") and type in (10, 12, 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<String> sponseeAddresses, int sponseeCount) throws SQLException {
StringBuffer avgBalanceSql = new StringBuffer();
avgBalanceSql.append("SELECT avg(balance)/100000000 FROM ACCOUNTBALANCES ");
avgBalanceSql.append("WHERE account in (");
avgBalanceSql.append(String.join(", ", Collections.nCopies(sponseeCount, "?")));
avgBalanceSql.append(") and ASSET_ID = 0");
String[] sponsees = sponseeAddresses.toArray(new String[sponseeCount]);
return this.repository.checkedExecute(avgBalanceSql.toString(), sponsees);
}
/**
* Get Names
*
* @param namesResultSet the result set to get the names from, can't be null
* @param count the number of potential names
*
* @return the names
*
* @throws SQLException
*/
private static List<String> getNames(ResultSet namesResultSet, int count) throws SQLException {
List<String> names = new ArrayList<>(count);
do{
String name = namesResultSet.getString(1);
if( name != null ) {
names.add(name);
}
} while( namesResultSet.next() );
return names;
}
private ResultSet getNamesResultSet(List<String> 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;
}
}

View File

@ -954,6 +954,128 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
}
}
@Override
public List<ArbitraryResourceData> searchArbitraryResourcesSimple(
Service service,
String identifier,
List<String> names,
boolean prefixOnly,
Long before,
Long after,
Integer limit,
Integer offset,
Boolean reverse,
Boolean caseInsensitive) throws DataException {
StringBuilder sql = new StringBuilder(512);
List<Object> 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<ArbitraryResourceData> 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

View File

@ -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