forked from Qortal/qortal
API: transaction searching
Converted AddressesResource to full base64, removing base58. Narrowed range of API errors returnable while there. Added support for looking up public key of address. Added support for converting public key TO address. Added API endpoint for returning a range of block signatures, to aid block explorers. Added API support for fetching unconfirmed transactions. Added API endpoint for searching transactions to meet criteria like: - participating address (only recipients supported ATM) - block height range - transaction type - result count limit/offset --- Added storage of account's public key in repository along with supporting code in AccountData and Account business object to save public key where possible.
This commit is contained in:
parent
b5c02f49ce
commit
3829630b29
@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -23,12 +24,15 @@ import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import data.account.AccountBalanceData;
|
||||
import data.account.AccountData;
|
||||
import qora.account.Account;
|
||||
import qora.account.PublicKeyAccount;
|
||||
import qora.assets.Asset;
|
||||
import qora.crypto.Crypto;
|
||||
import repository.DataException;
|
||||
import repository.Repository;
|
||||
import repository.RepositoryManager;
|
||||
import utils.Base58;
|
||||
import transform.Transformer;
|
||||
|
||||
@Path("addresses")
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
|
||||
@ -36,7 +40,7 @@ import utils.Base58;
|
||||
@ExtensionProperty(name="path", value="/Api/AddressesResource")
|
||||
}
|
||||
)
|
||||
@Tag(name = "addresses")
|
||||
@Tag(name = "Addresses")
|
||||
public class AddressesResource {
|
||||
|
||||
@Context
|
||||
@ -56,7 +60,7 @@ public class AddressesResource {
|
||||
@Path("/lastreference/{address}")
|
||||
@Operation(
|
||||
summary = "Fetch reference for next transaction to be created by address",
|
||||
description = "Returns the 64-byte long base58-encoded signature of last transaction created by address, failing that: the first incoming transaction to address. Returns \"false\" if there is no transactions.",
|
||||
description = "Returns the base64-encoded signature of the last confirmed transaction created by address, failing that: the first incoming transaction to address. Returns \"false\" if there is no transactions.",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET lastreference:address"),
|
||||
@ -68,7 +72,7 @@ public class AddressesResource {
|
||||
},
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the base58-encoded transaction signature or \"false\"",
|
||||
description = "the base64-encoded transaction signature or \"false\"",
|
||||
content = @Content(schema = @Schema(implementation = String.class)),
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ -79,7 +83,7 @@ public class AddressesResource {
|
||||
}
|
||||
)
|
||||
public String getLastReference(
|
||||
@Parameter(description = "a base58-encoded address", required = true) @PathParam("address") String address
|
||||
@Parameter(description = "a base64-encoded address", required = true) @PathParam("address") String address
|
||||
) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
|
||||
@ -90,14 +94,14 @@ public class AddressesResource {
|
||||
lastReference = account.getLastReference();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
||||
if(lastReference == null || lastReference.length == 0) {
|
||||
return "false";
|
||||
} else {
|
||||
return Base58.encode(lastReference);
|
||||
return Base64.getEncoder().encodeToString(lastReference);
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +109,7 @@ public class AddressesResource {
|
||||
@Path("/lastreference/{address}/unconfirmed")
|
||||
@Operation(
|
||||
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions",
|
||||
description = "Returns the 64-byte long base58-encoded signature of last transaction, including unconfirmed, created by address, failing that: the first incoming transaction. Returns \\\"false\\\" if there is no transactions.",
|
||||
description = "Returns the base64-encoded signature of the last confirmed/unconfirmed transaction created by address, failing that: the first incoming transaction. Returns \\\"false\\\" if there is no transactions.",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET lastreference:address:unconfirmed"),
|
||||
@ -117,7 +121,7 @@ public class AddressesResource {
|
||||
},
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the base58-encoded transaction signature",
|
||||
description = "the base64-encoded transaction signature",
|
||||
content = @Content(schema = @Schema(implementation = String.class)),
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ -137,14 +141,14 @@ public class AddressesResource {
|
||||
lastReference = account.getUnconfirmedLastReference();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
||||
if(lastReference == null || lastReference.length == 0) {
|
||||
return "false";
|
||||
} else {
|
||||
return Base58.encode(lastReference);
|
||||
return Base64.getEncoder().encodeToString(lastReference);
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +183,8 @@ public class AddressesResource {
|
||||
@GET
|
||||
@Path("/generatingbalance/{address}")
|
||||
@Operation(
|
||||
description = "Return the generating balance of the given address.",
|
||||
summary = "Return the generating balance of the given address",
|
||||
description = "Returns the effective balance of the given address, used in Proof-of-Stake calculationgs when generating a new block.",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET generatingbalance:address"),
|
||||
@ -205,21 +210,20 @@ public class AddressesResource {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Account account = new Account(repository, address);
|
||||
return account.getGeneratingBalance();
|
||||
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/balance/{address}")
|
||||
@Operation(
|
||||
description = "Returns the confirmed balance of the given address.",
|
||||
summary = "Returns the confirmed balance of the given address",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET balance:address"),
|
||||
@ -245,19 +249,20 @@ public class AddressesResource {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Account account = new Account(repository, address);
|
||||
return account.getConfirmedBalance(Asset.QORA);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/assetbalance/{assetid}/{address}")
|
||||
@Operation(
|
||||
summary = "Asset-specific balance request",
|
||||
description = "Returns the confirmed balance of the given address for the given asset key.",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ -284,20 +289,21 @@ public class AddressesResource {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Account account = new Account(repository, address);
|
||||
return account.getConfirmedBalance(assetid);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/assets/{address}")
|
||||
@Operation(
|
||||
description = "Returns the list of assets for this address with balances.",
|
||||
summary = "All assets owned by this address",
|
||||
description = "Returns the list of assets for this address, with balances.",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET assets:address"),
|
||||
@ -323,19 +329,19 @@ public class AddressesResource {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getAccountRepository().getAllBalances(address);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/balance/{address}/{confirmations}")
|
||||
@Operation(
|
||||
description = "Calculates the balance of the given address after the given confirmations.",
|
||||
summary = "Calculates the balance of the given address for the given confirmations",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET balance:address:confirmations"),
|
||||
@ -364,7 +370,8 @@ public class AddressesResource {
|
||||
@GET
|
||||
@Path("/publickey/{address}")
|
||||
@Operation(
|
||||
description = "Returns the 32-byte long base58-encoded account publickey of the given address.",
|
||||
summary = "Address' public key",
|
||||
description = "Returns the base64-encoded account public key of the given address, or \"false\" if address not known or has no public key.",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET publickey:address"),
|
||||
@ -376,7 +383,7 @@ public class AddressesResource {
|
||||
},
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the publickey",
|
||||
description = "the public key",
|
||||
content = @Content(schema = @Schema(implementation = String.class)),
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ -387,7 +394,74 @@ public class AddressesResource {
|
||||
}
|
||||
)
|
||||
public String getPublicKey(@PathParam("address") String address) {
|
||||
throw new UnsupportedOperationException();
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
|
||||
if (accountData == null)
|
||||
return "false";
|
||||
|
||||
byte[] publicKey = accountData.getPublicKey();
|
||||
if (publicKey == null)
|
||||
return "false";
|
||||
|
||||
return Base64.getEncoder().encodeToString(publicKey);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("/convert/{publickey}")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Operation(
|
||||
summary = "Convert public key into address",
|
||||
description = "Returns account address based on supplied public key. Expects base64-encoded, 32-byte public key.",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET publickey:address"),
|
||||
@ExtensionProperty(name="description.key", value="operation:description")
|
||||
}),
|
||||
@Extension(properties = {
|
||||
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
|
||||
})
|
||||
},
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the address",
|
||||
content = @Content(schema = @Schema(implementation = String.class)),
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="description.key", value="success_response:description")
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
public String fromPublicKey(@PathParam("publickey") String publicKey) {
|
||||
// Decode public key
|
||||
byte[] publicKeyBytes;
|
||||
try {
|
||||
publicKeyBytes = Base64.getDecoder().decode(publicKey);
|
||||
} catch (NumberFormatException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_PUBLIC_KEY, e);
|
||||
}
|
||||
|
||||
// Correct size for public key?
|
||||
if (publicKeyBytes.length != Transformer.PUBLIC_KEY_LENGTH)
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_PUBLIC_KEY);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return Crypto.toAddress(publicKeyBytes);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import controller.Controller;
|
||||
@ExtensionProperty(name="path", value="/Api/AdminResource")
|
||||
}
|
||||
)
|
||||
@Tag(name = "admin")
|
||||
@Tag(name = "Admin")
|
||||
public class AdminResource {
|
||||
|
||||
@Context
|
||||
@ -34,7 +34,8 @@ public class AdminResource {
|
||||
@GET
|
||||
@Path("/dud")
|
||||
@Parameter(name = "blockSignature", description = "Block signature", schema = @Schema(type = "string", format = "byte", minLength = 84, maxLength=88))
|
||||
@Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return", schema = @Schema(type = "integer", defaultValue = "10"))
|
||||
@Parameter(in = ParameterIn.QUERY, name = "count", description = "Maximum number of entries to return", schema = @Schema(type = "integer", defaultValue = "10"))
|
||||
@Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return, 0 means unlimited", schema = @Schema(type = "integer", defaultValue = "10"))
|
||||
@Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results", schema = @Schema(type = "integer"))
|
||||
@Parameter(in = ParameterIn.QUERY, name = "includeTransactions", description = "Include associated transactions in results", schema = @Schema(type = "boolean"))
|
||||
public String globalParameters() {
|
||||
|
@ -50,13 +50,10 @@ public class AnnotationPostProcessor implements ReaderListener {
|
||||
|
||||
@Override
|
||||
public void beforeScan(Reader reader, OpenAPI openAPI) {
|
||||
LOGGER.info("beforeScan");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterScan(Reader reader, OpenAPI openAPI) {
|
||||
LOGGER.info("afterScan");
|
||||
|
||||
// Populate Components section with reusable parameters, like "limit" and "offset"
|
||||
// We take the reusable parameters from AdminResource.globalParameters path "/admin/dud"
|
||||
Components components = openAPI.getComponents();
|
||||
|
@ -9,10 +9,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@OpenAPIDefinition(
|
||||
info = @Info( title = "Qora API", description = "NOTE: byte-arrays currently returned as Base64 but this is likely to change to Base58" ),
|
||||
tags = {
|
||||
@Tag(name = "addresses"),
|
||||
@Tag(name = "admin"),
|
||||
@Tag(name = "blocks"),
|
||||
@Tag(name = "transactions")
|
||||
@Tag(name = "Addresses"),
|
||||
@Tag(name = "Admin"),
|
||||
@Tag(name = "Blocks"),
|
||||
@Tag(name = "Transactions"),
|
||||
@Tag(name = "Utilities")
|
||||
},
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
|
@ -33,6 +33,8 @@ public enum ApiError {
|
||||
FEE_LESS_REQUIRED(121, 422),
|
||||
WALLET_NOT_IN_SYNC(122, 422),
|
||||
INVALID_NETWORK_ADDRESS(123, 404),
|
||||
ADDRESS_NO_EXISTS(124, 404),
|
||||
INVALID_CRITERIA(125, 400),
|
||||
|
||||
//WALLET
|
||||
WALLET_NO_EXISTS(201, 404),
|
||||
|
@ -64,6 +64,8 @@ public class ApiErrorFactory {
|
||||
this.errorMessages.put(ApiError.FEE_LESS_REQUIRED, createErrorMessageEntry(ApiError.FEE_LESS_REQUIRED, "fee less required"));
|
||||
this.errorMessages.put(ApiError.WALLET_NOT_IN_SYNC, createErrorMessageEntry(ApiError.WALLET_NOT_IN_SYNC, "wallet needs to be synchronized"));
|
||||
this.errorMessages.put(ApiError.INVALID_NETWORK_ADDRESS, createErrorMessageEntry(ApiError.INVALID_NETWORK_ADDRESS, "invalid network address"));
|
||||
this.errorMessages.put(ApiError.ADDRESS_NO_EXISTS, createErrorMessageEntry(ApiError.ADDRESS_NO_EXISTS, "account address does not exist"));
|
||||
this.errorMessages.put(ApiError.INVALID_CRITERIA, createErrorMessageEntry(ApiError.INVALID_CRITERIA, "invalid search criteria"));
|
||||
|
||||
//WALLET
|
||||
this.errorMessages.put(ApiError.WALLET_NO_EXISTS, createErrorMessageEntry(ApiError.WALLET_NO_EXISTS, "wallet does not exist"));
|
||||
|
@ -12,7 +12,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@ -36,7 +38,7 @@ import repository.RepositoryManager;
|
||||
@ExtensionProperty(name="path", value="/Api/BlocksResource")
|
||||
}
|
||||
)
|
||||
@Tag(name = "blocks")
|
||||
@Tag(name = "Blocks")
|
||||
public class BlocksResource {
|
||||
|
||||
@Context
|
||||
@ -53,10 +55,10 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{signature}")
|
||||
@Path("/signature/{signature}")
|
||||
@Operation(
|
||||
summary = "Fetch block using base64 signature",
|
||||
description = "returns the block that matches the given signature",
|
||||
description = "Returns the block that matches the given signature",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET signature"),
|
||||
@ -101,7 +103,7 @@ public class BlocksResource {
|
||||
@Path("/first")
|
||||
@Operation(
|
||||
summary = "Fetch genesis block",
|
||||
description = "returns the genesis block",
|
||||
description = "Returns the genesis block",
|
||||
extensions = @Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET first"),
|
||||
@ExtensionProperty(name="description.key", value="operation:description")
|
||||
@ -133,7 +135,7 @@ public class BlocksResource {
|
||||
@Path("/last")
|
||||
@Operation(
|
||||
summary = "Fetch last/newest block in blockchain",
|
||||
description = "returns the last valid block",
|
||||
description = "Returns the last valid block",
|
||||
extensions = @Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET last"),
|
||||
@ExtensionProperty(name="description.key", value="operation:description")
|
||||
@ -165,7 +167,7 @@ public class BlocksResource {
|
||||
@Path("/child/{signature}")
|
||||
@Operation(
|
||||
summary = "Fetch child block using base64 signature of parent block",
|
||||
description = "returns the child block of the block that matches the given signature",
|
||||
description = "Returns the child block of the block that matches the given signature",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET child:signature"),
|
||||
@ -220,7 +222,8 @@ public class BlocksResource {
|
||||
@GET
|
||||
@Path("/generatingbalance")
|
||||
@Operation(
|
||||
description = "calculates the generating balance of the block that will follow the last block",
|
||||
summary = "Generating balance of next block",
|
||||
description = "Calculates the generating balance of the block that will follow the last block",
|
||||
extensions = @Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET generatingbalance"),
|
||||
@ExtensionProperty(name="description.key", value="operation:description")
|
||||
@ -252,7 +255,8 @@ public class BlocksResource {
|
||||
@GET
|
||||
@Path("/generatingbalance/{signature}")
|
||||
@Operation(
|
||||
description = "calculates the generating balance of the block that will follow the block that matches the signature",
|
||||
summary = "Generating balance of block after specific block",
|
||||
description = "Calculates the generating balance of the block that will follow the block that matches the signature",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET generatingbalance:signature"),
|
||||
@ -302,7 +306,8 @@ public class BlocksResource {
|
||||
@GET
|
||||
@Path("/time")
|
||||
@Operation(
|
||||
description = "calculates the time it should take for the network to generate the next block",
|
||||
summary = "Estimated time to forge next block",
|
||||
description = "Calculates the time it should take for the network to generate the next block",
|
||||
extensions = @Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET time"),
|
||||
@ExtensionProperty(name="description.key", value="operation:description")
|
||||
@ -333,7 +338,8 @@ public class BlocksResource {
|
||||
@GET
|
||||
@Path("/time/{generatingbalance}")
|
||||
@Operation(
|
||||
description = "calculates the time it should take for the network to generate blocks when the current generating balance in the network is the specified generating balance",
|
||||
summary = "Estimated time to forge block given generating balance",
|
||||
description = "Calculates the time it should take for the network to generate blocks based on specified generating balance",
|
||||
extensions = @Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET time:generatingbalance"),
|
||||
@ExtensionProperty(name="description.key", value="operation:description")
|
||||
@ -357,7 +363,8 @@ public class BlocksResource {
|
||||
@GET
|
||||
@Path("/height")
|
||||
@Operation(
|
||||
description = "returns the block height of the last block.",
|
||||
summary = "Current blockchain height",
|
||||
description = "Returns the block height of the last block.",
|
||||
extensions = @Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET height"),
|
||||
@ExtensionProperty(name="description.key", value="operation:description")
|
||||
@ -387,7 +394,8 @@ public class BlocksResource {
|
||||
@GET
|
||||
@Path("/height/{signature}")
|
||||
@Operation(
|
||||
description = "returns the block height of the block that matches the given signature",
|
||||
summary = "Height of specific block",
|
||||
description = "Returns the block height of the block that matches the given signature",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET height:signature"),
|
||||
@ -436,7 +444,8 @@ public class BlocksResource {
|
||||
@GET
|
||||
@Path("/byheight/{height}")
|
||||
@Operation(
|
||||
description = "returns the block whith given height",
|
||||
summary = "Fetch block using block height",
|
||||
description = "Returns the block with given height",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET byheight:height"),
|
||||
@ -458,7 +467,7 @@ public class BlocksResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public BlockWithTransactions getbyHeight(@PathParam("height") int height, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
|
||||
public BlockWithTransactions getByHeight(@PathParam("height") int height, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
return new BlockWithTransactions(repository, blockData, includeTransactions);
|
||||
@ -469,4 +478,53 @@ public class BlocksResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/range/{height}")
|
||||
@Operation(
|
||||
summary = "Fetch blocks starting with given height",
|
||||
description = "Returns blocks starting with given height.",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET byheight:height"),
|
||||
@ExtensionProperty(name="description.key", value="operation:description")
|
||||
}),
|
||||
@Extension(properties = {
|
||||
@ExtensionProperty(name="apiErrors", value="[\"BLOCK_NO_EXISTS\"]", parseValue = true),
|
||||
})
|
||||
},
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "blocks",
|
||||
content = @Content(schema = @Schema(implementation = BlockWithTransactions.class)),
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="description.key", value="success_response:description")
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<BlockWithTransactions> getBlockRange(@PathParam("height") int height, @Parameter(ref = "count") @QueryParam("count") int count) {
|
||||
boolean includeTransactions = false;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<BlockWithTransactions> blocks = new ArrayList<BlockWithTransactions>();
|
||||
|
||||
for (/* count already set */; count > 0; --count, ++height) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null)
|
||||
// Run out of blocks!
|
||||
break;
|
||||
|
||||
blocks.add(new BlockWithTransactions(repository, blockData, includeTransactions));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,8 +12,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import qora.crypto.Crypto;
|
||||
import qora.transaction.Transaction.TransactionType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@ -31,7 +33,6 @@ import data.transaction.TransactionData;
|
||||
import repository.DataException;
|
||||
import repository.Repository;
|
||||
import repository.RepositoryManager;
|
||||
import repository.TransactionRepository;
|
||||
import utils.Base58;
|
||||
|
||||
@Path("transactions")
|
||||
@ -40,7 +41,7 @@ import utils.Base58;
|
||||
@ExtensionProperty(name="path", value="/Api/TransactionsResource")
|
||||
}
|
||||
)
|
||||
@Tag(name = "transactions")
|
||||
@Tag(name = "Transactions")
|
||||
public class TransactionsResource {
|
||||
|
||||
@Context
|
||||
@ -57,26 +58,19 @@ public class TransactionsResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/address/{address}")
|
||||
@Path("/signature/{signature}")
|
||||
@Operation(
|
||||
summary = "Fetch transactions involving address",
|
||||
description = "Returns list of transactions",
|
||||
parameters = {
|
||||
@Parameter(in = ParameterIn.PATH, name = "address", description = "Account's address", schema = @Schema(type = "string"))
|
||||
},
|
||||
summary = "Fetch transaction using transaction signature",
|
||||
description = "Returns transaction",
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="path", value="GET block:signature"),
|
||||
@ExtensionProperty(name="description.key", value="operation:description")
|
||||
}),
|
||||
@Extension(properties = {
|
||||
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
|
||||
@Extension(properties = {
|
||||
@ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"TRANSACTION_NO_EXISTS\"]", parseValue = true),
|
||||
})
|
||||
},
|
||||
},
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "list of transactions",
|
||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))),
|
||||
description = "a transaction",
|
||||
content = @Content(schema = @Schema(implementation = TransactionData.class)),
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="description.key", value="success_response:description")
|
||||
@ -85,26 +79,21 @@ public class TransactionsResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<TransactionData> getAddressTransactions(@PathParam("address") String address, @Parameter(ref = "limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
|
||||
public TransactionData getTransactions(@PathParam("signature") String signature) {
|
||||
// Decode signature
|
||||
byte[] signatureBytes;
|
||||
try {
|
||||
signatureBytes = Base64.getDecoder().decode(signature);
|
||||
} catch (NumberFormatException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TransactionRepository txRepo = repository.getTransactionRepository();
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signatureBytes);
|
||||
if (transactionData == null)
|
||||
throw this.apiErrorFactory.createError(ApiError.TRANSACTION_NO_EXISTS);
|
||||
|
||||
List<byte[]> signatures = txRepo.getAllSignaturesInvolvingAddress(address);
|
||||
|
||||
// Pagination would take effect here (or as part of the repository access)
|
||||
int fromIndex = Integer.min(offset, signatures.size());
|
||||
int toIndex = limit == 0 ? signatures.size() : Integer.min(fromIndex + limit, signatures.size());
|
||||
signatures = signatures.subList(fromIndex, toIndex);
|
||||
|
||||
// Expand signatures to transactions
|
||||
List<TransactionData> transactions = new ArrayList<TransactionData>(signatures.size());
|
||||
for (byte[] signature : signatures)
|
||||
transactions.add(txRepo.fromSignature(signature));
|
||||
|
||||
return transactions;
|
||||
return transactionData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
@ -169,4 +158,89 @@ public class TransactionsResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/unconfirmed")
|
||||
@Operation(
|
||||
summary = "List unconfirmed transactions",
|
||||
description = "Returns transactions",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "transactions",
|
||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))),
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="description.key", value="success_response:description")
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<TransactionData> getUnconfirmedTransactions() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getTransactionRepository().getAllUnconfirmedTransactions();
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/search")
|
||||
@Operation(
|
||||
summary = "Find matching transactions",
|
||||
description = "Returns transactions that match criteria. At least either txType or address must be provided.",
|
||||
/*
|
||||
parameters = {
|
||||
@Parameter(in = ParameterIn.QUERY, name = "txType", description = "Transaction type", schema = @Schema(type = "integer")),
|
||||
@Parameter(in = ParameterIn.QUERY, name = "address", description = "Account's address", schema = @Schema(type = "string")),
|
||||
@Parameter(in = ParameterIn.QUERY, name = "startBlock", description = "Start block height", schema = @Schema(type = "integer")),
|
||||
@Parameter(in = ParameterIn.QUERY, name = "blockLimit", description = "Maximum number of blocks to search", schema = @Schema(type = "integer"))
|
||||
},
|
||||
*/
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "transactions",
|
||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))),
|
||||
extensions = {
|
||||
@Extension(name = "translation", properties = {
|
||||
@ExtensionProperty(name="description.key", value="success_response:description")
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
public List<TransactionData> searchTransactions(@QueryParam("startBlock") Integer startBlock, @QueryParam("blockLimit") Integer blockLimit,
|
||||
@QueryParam("txType") Integer txTypeNum, @QueryParam("address") String address, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
|
||||
if ((txTypeNum == null || txTypeNum == 0) && (address == null || address.isEmpty()))
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_CRITERIA);
|
||||
|
||||
TransactionType txType = null;
|
||||
if (txTypeNum != null) {
|
||||
txType = TransactionType.valueOf(txTypeNum);
|
||||
if (txType == null)
|
||||
throw this.apiErrorFactory.createError(ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getAllSignaturesMatchingCriteria(startBlock, blockLimit, txType, address);
|
||||
|
||||
// Pagination would take effect here (or as part of the repository access)
|
||||
int fromIndex = Integer.min(offset, signatures.size());
|
||||
int toIndex = limit == 0 ? signatures.size() : Integer.min(fromIndex + limit, signatures.size());
|
||||
signatures = signatures.subList(fromIndex, toIndex);
|
||||
|
||||
// Expand signatures to transactions
|
||||
List<TransactionData> transactions = new ArrayList<TransactionData>(signatures.size());
|
||||
for (byte[] signature : signatures)
|
||||
transactions.add(repository.getTransactionRepository().fromSignature(signature));
|
||||
|
||||
return transactions;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import javax.ws.rs.core.MediaType;
|
||||
|
||||
@Path("/utils")
|
||||
@Produces({MediaType.TEXT_PLAIN})
|
||||
@Tag(name = "utils")
|
||||
@Tag(name = "Utilities")
|
||||
public class UtilsResource {
|
||||
|
||||
@Context
|
||||
|
@ -5,16 +5,18 @@ public class AccountData {
|
||||
// Properties
|
||||
protected String address;
|
||||
protected byte[] reference;
|
||||
protected byte[] publicKey;
|
||||
|
||||
// Constructors
|
||||
|
||||
public AccountData(String address, byte[] reference) {
|
||||
public AccountData(String address, byte[] reference, byte[] publicKey) {
|
||||
this.address = address;
|
||||
this.reference = reference;
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
public AccountData(String address) {
|
||||
this(address, null);
|
||||
this(address, null, null);
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
@ -31,6 +33,14 @@ public class AccountData {
|
||||
this.reference = reference;
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
public void setPublicKey(byte[] publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
// Comparison
|
||||
|
||||
@Override
|
||||
|
@ -30,6 +30,7 @@ public class Account {
|
||||
protected Account() {
|
||||
}
|
||||
|
||||
/** Construct Account business object using account's address */
|
||||
public Account(Repository repository, String address) {
|
||||
this.repository = repository;
|
||||
this.accountData = new AccountData(address);
|
||||
@ -118,7 +119,7 @@ public class Account {
|
||||
|
||||
public void setConfirmedBalance(long assetId, BigDecimal balance) throws DataException {
|
||||
// Can't have a balance without an account - make sure it exists!
|
||||
this.repository.getAccountRepository().create(this.accountData.getAddress());
|
||||
this.repository.getAccountRepository().create(this.accountData);
|
||||
|
||||
AccountBalanceData accountBalanceData = new AccountBalanceData(this.accountData.getAddress(), assetId, balance);
|
||||
this.repository.getAccountRepository().save(accountBalanceData);
|
||||
|
@ -21,8 +21,9 @@ public class PrivateKeyAccount extends PublicKeyAccount {
|
||||
this.repository = repository;
|
||||
this.seed = seed;
|
||||
this.keyPair = Ed25519.createKeyPair(seed);
|
||||
this.publicKey = keyPair.getB();
|
||||
this.accountData = new AccountData(Crypto.toAddress(this.publicKey));
|
||||
|
||||
byte[] publicKey = keyPair.getB();
|
||||
this.accountData = new AccountData(Crypto.toAddress(publicKey), null, publicKey);
|
||||
}
|
||||
|
||||
public byte[] getSeed() {
|
||||
|
@ -6,23 +6,21 @@ import repository.Repository;
|
||||
|
||||
public class PublicKeyAccount extends Account {
|
||||
|
||||
protected byte[] publicKey;
|
||||
|
||||
public PublicKeyAccount(Repository repository, byte[] publicKey) {
|
||||
super(repository, Crypto.toAddress(publicKey));
|
||||
|
||||
this.publicKey = publicKey;
|
||||
this.accountData.setPublicKey(publicKey);
|
||||
}
|
||||
|
||||
protected PublicKeyAccount() {
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return publicKey;
|
||||
return this.accountData.getPublicKey();
|
||||
}
|
||||
|
||||
public boolean verify(byte[] signature, byte[] message) {
|
||||
return PublicKeyAccount.verify(this.publicKey, signature, message);
|
||||
return PublicKeyAccount.verify(this.accountData.getPublicKey(), signature, message);
|
||||
}
|
||||
|
||||
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
|
||||
|
@ -9,7 +9,7 @@ public interface AccountRepository {
|
||||
|
||||
// General account
|
||||
|
||||
public void create(String address) throws DataException;
|
||||
public void create(AccountData accountData) throws DataException;
|
||||
|
||||
public AccountData getAccount(String address) throws DataException;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package repository;
|
||||
|
||||
import data.transaction.TransactionData;
|
||||
import qora.transaction.Transaction.TransactionType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -22,6 +23,8 @@ public interface TransactionRepository {
|
||||
|
||||
public List<byte[]> getAllSignaturesInvolvingAddress(String address) throws DataException;
|
||||
|
||||
public List<byte[]> getAllSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of unconfirmed transactions in timestamp-else-signature order.
|
||||
*
|
||||
|
@ -22,10 +22,14 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
// General account
|
||||
|
||||
@Override
|
||||
public void create(String address) throws DataException {
|
||||
public void create(AccountData accountData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
|
||||
|
||||
saveHelper.bind("account", address);
|
||||
saveHelper.bind("account", accountData.getAddress());
|
||||
|
||||
byte[] publicKey = accountData.getPublicKey();
|
||||
if (publicKey != null)
|
||||
saveHelper.bind("public_key", publicKey);
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
@ -36,11 +40,14 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public AccountData getAccount(String address) throws DataException {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute("SELECT reference FROM Accounts WHERE account = ?", address)) {
|
||||
try (ResultSet resultSet = this.repository.checkedExecute("SELECT reference, public_key FROM Accounts WHERE account = ?", address)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
return new AccountData(address, resultSet.getBytes(1));
|
||||
byte[] reference = resultSet.getBytes(1);
|
||||
byte[] publicKey = resultSet.getBytes(2);
|
||||
|
||||
return new AccountData(address, reference, publicKey);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch account info from repository", e);
|
||||
}
|
||||
@ -50,7 +57,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
public void save(AccountData accountData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
|
||||
|
||||
saveHelper.bind("account", accountData.getAddress()).bind("reference", accountData.getReference());
|
||||
saveHelper.bind("account", accountData.getAddress()).bind("reference", accountData.getReference()).bind("public_key", accountData.getPublicKey());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
|
@ -5,6 +5,8 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
import qora.crypto.Crypto;
|
||||
|
||||
public class HSQLDBDatabaseUpdates {
|
||||
|
||||
/**
|
||||
@ -383,6 +385,35 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CREATE INDEX ATTransactionsIndex on ATTransactions (AT_address)");
|
||||
break;
|
||||
|
||||
case 28:
|
||||
// Associate public keys with accounts
|
||||
stmt.execute("ALTER TABLE Accounts add public_key QoraPublicKey");
|
||||
// For looking up an account by public key
|
||||
stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)");
|
||||
|
||||
// Do not call close() on this as connection did not come from pool!
|
||||
HSQLDBRepository repository = new HSQLDBRepository(connection);
|
||||
|
||||
try (ResultSet resultSet = repository.checkedExecute("SELECT DISTINCT creator from Transactions")) {
|
||||
if (resultSet == null) {
|
||||
repository = null;
|
||||
break;
|
||||
}
|
||||
|
||||
do {
|
||||
byte[] publicKey = resultSet.getBytes(1);
|
||||
|
||||
String address = Crypto.toAddress(publicKey);
|
||||
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
|
||||
saveHelper.bind("account", address).bind("public_key", publicKey);
|
||||
saveHelper.execute(repository);
|
||||
} while (resultSet.next());
|
||||
}
|
||||
|
||||
repository = null;
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@ -7,6 +7,9 @@ import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import data.PaymentData;
|
||||
import data.block.BlockData;
|
||||
@ -286,6 +289,87 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<byte[]> getAllSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address) throws DataException {
|
||||
List<byte[]> signatures = new ArrayList<byte[]>();
|
||||
|
||||
boolean hasAddress = address != null && !address.isEmpty();
|
||||
boolean hasTxType = txType != null;
|
||||
boolean hasHeightRange = startBlock != null || blockLimit != null;
|
||||
|
||||
if (hasHeightRange && startBlock == null)
|
||||
startBlock = 1;
|
||||
|
||||
String signatureColumn = "NULL";
|
||||
List<Object> bindParams = new ArrayList<Object>();
|
||||
|
||||
// Table JOINs first
|
||||
List<String> tableJoins = new ArrayList<String>();
|
||||
|
||||
if (hasHeightRange) {
|
||||
tableJoins.add("Blocks");
|
||||
tableJoins.add("BlockTransactions ON BlockTransactions.block_signature = Blocks.signature");
|
||||
signatureColumn = "BlockTransactions.transaction_signature";
|
||||
}
|
||||
|
||||
if (hasTxType) {
|
||||
if (hasHeightRange)
|
||||
tableJoins.add("Transactions ON Transactions.signature = BlockTransactions.transaction_signature");
|
||||
else
|
||||
tableJoins.add("Transactions");
|
||||
|
||||
signatureColumn = "Transactions.signature";
|
||||
}
|
||||
|
||||
if (hasAddress) {
|
||||
if (hasTxType)
|
||||
tableJoins.add("TransactionRecipients ON TransactionRecipients.signature = Transactions.signature");
|
||||
else if (hasHeightRange)
|
||||
tableJoins.add("TransactionRecipients ON TransactionRecipients.signature = BlockTransactions.transaction_signature");
|
||||
else
|
||||
tableJoins.add("TransactionRecipients");
|
||||
|
||||
signatureColumn = "TransactionRecipients.signature";
|
||||
}
|
||||
|
||||
// WHERE clauses next
|
||||
List<String> whereClauses = new ArrayList<String>();
|
||||
|
||||
if (hasHeightRange) {
|
||||
whereClauses.add("Blocks.height >= " + startBlock);
|
||||
|
||||
if (blockLimit != null)
|
||||
whereClauses.add("Blocks.height < " + (startBlock + blockLimit));
|
||||
}
|
||||
|
||||
if (hasTxType)
|
||||
whereClauses.add("Transactions.type = " + txType.value);
|
||||
|
||||
if (hasAddress) {
|
||||
whereClauses.add("TransactionRecipients.recipient = ?");
|
||||
bindParams.add(address);
|
||||
}
|
||||
|
||||
String sql = "SELECT " + signatureColumn + " FROM " + String.join(" JOIN ", tableJoins) + " WHERE " + String.join(" AND ", whereClauses);
|
||||
System.out.println("Transaction search SQL:\n" + sql);
|
||||
|
||||
// XXX We need a table for all parties involved in a transaction, not just recipients
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
return signatures;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
|
||||
signatures.add(signature);
|
||||
} while (resultSet.next());
|
||||
|
||||
return signatures;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch matching transaction signatures from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TransactionData> getAllUnconfirmedTransactions() throws DataException {
|
||||
List<TransactionData> transactions = new ArrayList<TransactionData>();
|
||||
|
@ -120,7 +120,7 @@ public class TransactionTests {
|
||||
|
||||
// Create test generator account
|
||||
generator = new PrivateKeyAccount(repository, generatorSeed);
|
||||
accountRepository.save(new AccountData(generator.getAddress(), generatorSeed));
|
||||
accountRepository.save(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey()));
|
||||
accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORA, initialGeneratorBalance));
|
||||
|
||||
// Create test sender account
|
||||
@ -128,7 +128,7 @@ public class TransactionTests {
|
||||
|
||||
// Mock account
|
||||
reference = senderSeed;
|
||||
accountRepository.save(new AccountData(sender.getAddress(), reference));
|
||||
accountRepository.save(new AccountData(sender.getAddress(), reference, sender.getPublicKey()));
|
||||
|
||||
// Mock balance
|
||||
accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, initialSenderBalance));
|
||||
|
Loading…
Reference in New Issue
Block a user