API, switchable MD160,

Now uses working RIPE-MD160 by default but can be switched to broken MD160 using flag in blockchain config,
e.g. for Qora v1 blockchain.

Replaced API signature/reference examples with descriptive text as they weren't very useful.
Replaced API address examples with ones generated using working MD160.

Added GET /transactions/signature/{signature}/raw that returns raw transaction in base58 encoding.

Added "ignoreValidityChecks" query param to POST /transactions/decode to bypass INVALID_REFERENCE errors
if supplying an old/speculative transaction that can't be added to unconfirmed transaction pile.

Finally fixed creating inital assets in BlockChain.

Controller now inserts BouncyCastle as highest priority Security Provider.

TransactionData & transaction repository now tries to return transaction's block height in data when possible.
This commit is contained in:
catbref 2019-01-07 13:12:42 +00:00
parent b2ca63ce88
commit 7998166c0a
14 changed files with 167 additions and 33 deletions

View File

@ -6,9 +6,10 @@
"blockTimestampMargin": 500, "blockTimestampMargin": 500,
"maxBytesPerUnitFee": 1024, "maxBytesPerUnitFee": 1024,
"unitFee": "1.0", "unitFee": "1.0",
"useBrokenMD160ForAddresses": true,
"genesis": { "genesis": {
"assets": [ "assets": [
{ "name": "QORA", "description": "QORA coin" } { "name": "QORA", "description": "QORA coin", "quantity": 10000000000, "isDivisible": true, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC" }
] ]
"timestamp": "1400247274336", "timestamp": "1400247274336",
"generatingBalance": "10000000", "generatingBalance": "10000000",

View File

@ -28,10 +28,10 @@ public class AdminResource {
@GET @GET
@Path("/unused") @Path("/unused")
@Parameter(in = ParameterIn.PATH, name = "blockSignature", description = "Block signature", schema = @Schema(type = "string", format = "byte"), example = "ZZZZ==") @Parameter(in = ParameterIn.PATH, name = "blockSignature", description = "Block signature", schema = @Schema(type = "string", format = "byte"), example = "very_long_block_signature_in_base58")
@Parameter(in = ParameterIn.PATH, name = "assetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte")) @Parameter(in = ParameterIn.PATH, name = "assetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte"))
@Parameter(in = ParameterIn.PATH, name = "otherAssetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte")) @Parameter(in = ParameterIn.PATH, name = "otherAssetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte"))
@Parameter(in = ParameterIn.PATH, name = "address", description = "an account address", example = "QRHDHASWAXarqTvB2X4SNtJCWbxGf68M2o") @Parameter(in = ParameterIn.PATH, name = "address", description = "an account address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
@Parameter(in = ParameterIn.QUERY, name = "count", description = "Maximum number of entries to return, 0 means none", schema = @Schema(type = "integer", defaultValue = "20")) @Parameter(in = ParameterIn.QUERY, name = "count", description = "Maximum number of entries to return, 0 means none", schema = @Schema(type = "integer", defaultValue = "20"))
@Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return, 0 means unlimited", schema = @Schema(type = "integer", defaultValue = "20")) @Parameter(in = ParameterIn.QUERY, name = "limit", description = "Maximum number of entries to return, 0 means unlimited", schema = @Schema(type = "integer", defaultValue = "20"))
@Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results, 0 is first entry", schema = @Schema(type = "integer")) @Parameter(in = ParameterIn.QUERY, name = "offset", description = "Starting entry in results, 0 is first entry", schema = @Schema(type = "integer"))

View File

@ -66,7 +66,7 @@ public class AnnotationPostProcessor implements ReaderListener {
if (apiErrors == null) if (apiErrors == null)
continue; continue;
LOGGER.info("Found @ApiErrors annotation on " + clazz.getSimpleName() + "." + method.getName()); LOGGER.trace("Found @ApiErrors annotation on " + clazz.getSimpleName() + "." + method.getName());
PathItem pathItem = getPathItemFromMethod(openAPI, classPathString, method); PathItem pathItem = getPathItemFromMethod(openAPI, classPathString, method);
for (Operation operation : pathItem.readOperations()) for (Operation operation : pathItem.readOperations())

View File

@ -69,7 +69,7 @@ public class TransactionsResource {
} }
) )
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public TransactionData getTransactions(@PathParam("signature") String signature58) { public TransactionData getTransaction(@PathParam("signature") String signature58) {
byte[] signature; byte[] signature;
try { try {
signature = Base58.decode(signature58); signature = Base58.decode(signature58);
@ -90,6 +90,47 @@ public class TransactionsResource {
} }
} }
@GET
@Path("/signature/{signature}/raw")
@Operation(
summary = "Fetch raw, base58-encoded, transaction using transaction signature",
description = "Returns transaction",
responses = {
@ApiResponse(
description = "raw transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(type = "string")
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE, ApiError.TRANSFORMATION_ERROR})
public String getRawTransaction(@PathParam("signature") String signature58) {
byte[] signature;
try {
signature = Base58.decode(signature58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_NO_EXISTS);
byte[] transactionBytes = TransactionTransformer.toBytes(transactionData);
return Base58.encode(transactionBytes);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
}
}
@GET @GET
@Path("/block/{signature}") @Path("/block/{signature}")
@Operation( @Operation(
@ -372,7 +413,7 @@ public class TransactionsResource {
} }
) )
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) @ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public TransactionData decodeTransaction(String rawBytes58) { public TransactionData decodeTransaction(String rawBytes58, @QueryParam("ignoreValidityChecks") boolean ignoreValidityChecks) {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58); byte[] rawBytes = Base58.decode(rawBytes58);
boolean hasSignature = true; boolean hasSignature = true;
@ -388,12 +429,15 @@ public class TransactionsResource {
} }
Transaction transaction = Transaction.fromData(repository, transactionData); Transaction transaction = Transaction.fromData(repository, transactionData);
if (!ignoreValidityChecks) {
if (hasSignature && !transaction.isSignatureValid()) if (hasSignature && !transaction.isSignatureValid())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
ValidationResult result = transaction.isValid(); ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK) if (result != ValidationResult.OK)
throw createTransactionInvalidException(request, result); throw createTransactionInvalidException(request, result);
}
if (!hasSignature) if (!hasSignature)
transactionData.setSignature(null); transactionData.setSignature(null);

View File

@ -11,7 +11,6 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.json.simple.JSONObject; import org.json.simple.JSONObject;
import org.qora.asset.Asset;
import org.qora.data.asset.AssetData; import org.qora.data.asset.AssetData;
import org.qora.data.block.BlockData; import org.qora.data.block.BlockData;
import org.qora.repository.BlockRepository; import org.qora.repository.BlockRepository;
@ -52,6 +51,9 @@ public class BlockChain {
/** Map of which blockchain features are enabled when (height/timestamp) */ /** Map of which blockchain features are enabled when (height/timestamp) */
private Map<String, Map<FeatureValueType, Long>> featureTriggers; private Map<String, Map<FeatureValueType, Long>> featureTriggers;
// This property is slightly different as we need it early and we want to avoid getInstance() loop
private static boolean useBrokenMD160ForAddresses = false;
// Constructors, etc. // Constructors, etc.
private BlockChain() { private BlockChain() {
@ -59,7 +61,8 @@ public class BlockChain {
public static BlockChain getInstance() { public static BlockChain getInstance() {
if (instance == null) if (instance == null)
Settings.getInstance(); // This will call BlockChain.fromJSON in turn
Settings.getInstance(); // synchronized
return instance; return instance;
} }
@ -98,6 +101,10 @@ public class BlockChain {
return this.blockTimestampMargin; return this.blockTimestampMargin;
} }
public static boolean getUseBrokenMD160ForAddresses() {
return useBrokenMD160ForAddresses;
}
private long getFeatureTrigger(String feature, FeatureValueType valueType) { private long getFeatureTrigger(String feature, FeatureValueType valueType) {
Map<FeatureValueType, Long> featureTrigger = featureTriggers.get(feature); Map<FeatureValueType, Long> featureTrigger = featureTriggers.get(feature);
if (featureTrigger == null) if (featureTrigger == null)
@ -143,6 +150,14 @@ public class BlockChain {
// Blockchain config from JSON // Blockchain config from JSON
public static void fromJSON(JSONObject json) { public static void fromJSON(JSONObject json) {
// Determine hash function for generating addresses as we need that to build genesis block, etc.
Boolean useBrokenMD160 = null;
if (json.containsKey("useBrokenMD160ForAddresses"))
useBrokenMD160 = (Boolean) Settings.getTypedJson(json, "useBrokenMD160ForAddresses", Boolean.class);
if (useBrokenMD160 != null)
useBrokenMD160ForAddresses = useBrokenMD160.booleanValue();
Object genesisJson = json.get("genesis"); Object genesisJson = json.get("genesis");
if (genesisJson == null) { if (genesisJson == null) {
LOGGER.error("No \"genesis\" entry found in blockchain config"); LOGGER.error("No \"genesis\" entry found in blockchain config");
@ -228,15 +243,15 @@ public class BlockChain {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
repository.rebuild(); repository.rebuild();
// Add Genesis Block
GenesisBlock genesisBlock = GenesisBlock.getInstance(repository); GenesisBlock genesisBlock = GenesisBlock.getInstance(repository);
genesisBlock.process();
// Add QORA asset. // Add initial assets
// NOTE: Asset's transaction reference is Genesis Block's generator signature which doesn't exist as a transaction! // NOTE: Asset's [transaction] reference doesn't exist as a transaction!
AssetData qoraAssetData = new AssetData(Asset.QORA, genesisBlock.getGenerator().getAddress(), "Qora", "This is the simulated Qora asset.", for (AssetData assetData : genesisBlock.getInitialAssets())
BlockChain.getInstance().getMaxBalance().longValue(), true, genesisBlock.getBlockData().getGeneratorSignature()); repository.getAssetRepository().save(assetData);
repository.getAssetRepository().save(qoraAssetData);
// Add Genesis Block to blockchain
genesisBlock.process();
repository.saveChanges(); repository.saveChanges();
} }

View File

@ -10,10 +10,12 @@ import java.util.List;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Base58;
import org.json.simple.JSONArray; import org.json.simple.JSONArray;
import org.json.simple.JSONObject; import org.json.simple.JSONObject;
import org.qora.account.GenesisAccount; import org.qora.account.GenesisAccount;
import org.qora.crypto.Crypto; import org.qora.crypto.Crypto;
import org.qora.data.asset.AssetData;
import org.qora.data.block.BlockData; import org.qora.data.block.BlockData;
import org.qora.data.transaction.GenesisTransactionData; import org.qora.data.transaction.GenesisTransactionData;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
@ -37,6 +39,7 @@ public class GenesisBlock extends Block {
// Properties // Properties
private static BlockData blockData; private static BlockData blockData;
private static List<TransactionData> transactionsData; private static List<TransactionData> transactionsData;
private static List<AssetData> initialAssets;
// Constructors // Constructors
@ -51,6 +54,8 @@ public class GenesisBlock extends Block {
// Construction from JSON // Construction from JSON
public static void fromJSON(JSONObject json) { public static void fromJSON(JSONObject json) {
// All parsing first, then if successful we can proceed to construction
// Version // Version
int version = 1; // but could be bumped later int version = 1; // but could be bumped later
@ -68,6 +73,9 @@ public class GenesisBlock extends Block {
throw new RuntimeException("Unable to parse genesis timestamp"); throw new RuntimeException("Unable to parse genesis timestamp");
} }
// Generating balance
BigDecimal generatingBalance = Settings.getJsonBigDecimal(json, "generatingBalance");
// Transactions // Transactions
JSONArray transactionsJson = (JSONArray) Settings.getTypedJson(json, "transactions", JSONArray.class); JSONArray transactionsJson = (JSONArray) Settings.getTypedJson(json, "transactions", JSONArray.class);
List<TransactionData> transactions = new ArrayList<>(); List<TransactionData> transactions = new ArrayList<>();
@ -96,8 +104,28 @@ public class GenesisBlock extends Block {
} }
} }
// Generating balance // Assets
BigDecimal generatingBalance = Settings.getJsonBigDecimal(json, "generatingBalance"); JSONArray assetsJson = (JSONArray) Settings.getTypedJson(json, "assets", JSONArray.class);
String genesisAddress = Crypto.toAddress(GenesisAccount.PUBLIC_KEY);
List<AssetData> assets = new ArrayList<>();
for (Object assetObj : assetsJson) {
if (!(assetObj instanceof JSONObject)) {
LOGGER.error("Genesis asset malformed in blockchain config file");
throw new RuntimeException("Genesis asset malformed in blockchain config file");
}
JSONObject assetJson = (JSONObject) assetObj;
String name = (String) Settings.getTypedJson(assetJson, "name", String.class);
String description = (String) Settings.getTypedJson(assetJson, "description", String.class);
String reference58 = (String) Settings.getTypedJson(assetJson, "reference", String.class);
byte[] reference = Base58.decode(reference58);
long quantity = (Long) Settings.getTypedJson(assetJson, "quantity", Long.class);
boolean isDivisible = (Boolean) Settings.getTypedJson(assetJson, "isDivisible", Boolean.class);
assets.add(new AssetData(genesisAddress, name, description, quantity, isDivisible, reference));
}
byte[] reference = GENESIS_REFERENCE; byte[] reference = GENESIS_REFERENCE;
int transactionCount = transactions.size(); int transactionCount = transactions.size();
@ -113,6 +141,7 @@ public class GenesisBlock extends Block {
blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generatorPublicKey, generatorSignature, atCount, atFees); generatorPublicKey, generatorSignature, atCount, atFees);
transactionsData = transactions; transactionsData = transactions;
initialAssets = assets;
} }
// More information // More information
@ -134,6 +163,10 @@ public class GenesisBlock extends Block {
return true; return true;
} }
public List<AssetData> getInitialAssets() {
return Collections.unmodifiableList(initialAssets);
}
// Processing // Processing
@Override @Override

View File

@ -1,7 +1,10 @@
package org.qora.controller; package org.qora.controller;
import java.security.Security;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qora.api.ApiService; import org.qora.api.ApiService;
import org.qora.block.BlockChain; import org.qora.block.BlockChain;
import org.qora.block.BlockGenerator; import org.qora.block.BlockGenerator;
@ -27,6 +30,8 @@ public class Controller {
public static void main(String args[]) { public static void main(String args[]) {
LOGGER.info("Starting up..."); LOGGER.info("Starting up...");
Security.insertProviderAt(new BouncyCastleProvider(), 0);
// Load/check settings, which potentially sets up blockchain config, etc. // Load/check settings, which potentially sets up blockchain config, etc.
Settings.getInstance(); Settings.getInstance();

View File

@ -5,6 +5,7 @@ import java.security.NoSuchAlgorithmException;
import java.util.Arrays; import java.util.Arrays;
import org.qora.account.Account; import org.qora.account.Account;
import org.qora.block.BlockChain;
import org.qora.utils.Base58; import org.qora.utils.Base58;
public class Crypto { public class Crypto {
@ -28,7 +29,7 @@ public class Crypto {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
return sha256.digest(input); return sha256.digest(input);
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
return null; throw new RuntimeException("SHA-256 message digest not available");
} }
} }
@ -48,9 +49,20 @@ public class Crypto {
// SHA2-256 input to create new data and of known size // SHA2-256 input to create new data and of known size
byte[] inputHash = digest(input); byte[] inputHash = digest(input);
// Use BROKEN RIPEMD160 to create shorter address // Use RIPEMD160 to create shorter address
if (BlockChain.getUseBrokenMD160ForAddresses()) {
// Legacy BROKEN MD160
BrokenMD160 brokenMD160 = new BrokenMD160(); BrokenMD160 brokenMD160 = new BrokenMD160();
inputHash = brokenMD160.digest(inputHash); inputHash = brokenMD160.digest(inputHash);
} else {
// Use legit MD160
try {
MessageDigest md160 = MessageDigest.getInstance("RIPEMD160");
inputHash = md160.digest(inputHash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("RIPEMD160 message digest not available");
}
}
// Create address data using above hash and addressVersion (prepended) // Create address data using above hash and addressVersion (prepended)
byte[] addressBytes = new byte[inputHash.length + 1]; byte[] addressBytes = new byte[inputHash.length + 1];

View File

@ -17,7 +17,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
public class CancelOrderTransactionData extends TransactionData { public class CancelOrderTransactionData extends TransactionData {
// Properties // Properties
@Schema(description = "order ID to cancel", example = "2zYCM8P3PSzUxFNPAKFsSdwg9dWQcYTPCuKkuQbx3GVxTUVjXAUwEmEnvUUss11SZ3p38C16UfYb3cbXP9sRuqFx") @Schema(description = "order ID to cancel", example = "real_order_ID_in_base58")
private byte[] orderId; private byte[] orderId;
// Constructors // Constructors

View File

@ -19,11 +19,17 @@ public class IssueAssetTransactionData extends TransactionData {
// assetId can be null but assigned during save() or during load from repository // assetId can be null but assigned during save() or during load from repository
@Schema(accessMode = AccessMode.READ_ONLY) @Schema(accessMode = AccessMode.READ_ONLY)
private Long assetId = null; private Long assetId = null;
@Schema(description = "asset issuer's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
private byte[] issuerPublicKey; private byte[] issuerPublicKey;
@Schema(description = "asset owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
private String owner; private String owner;
@Schema(description = "asset name", example = "GOLD")
private String assetName; private String assetName;
@Schema(description = "asset description", example = "Gold asset - 1 unit represents one 1kg of gold")
private String description; private String description;
@Schema(description = "total supply of asset in existence (integer)", example = "1000")
private long quantity; private long quantity;
@Schema(description = "whether asset quantities can be fractional", example = "true")
private boolean isDivisible; private boolean isDivisible;
// Constructors // Constructors

View File

@ -18,7 +18,7 @@ public class PaymentTransactionData extends TransactionData {
// Properties // Properties
@Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") @Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
private byte[] senderPublicKey; private byte[] senderPublicKey;
@Schema(description = "recipient's address", example = "Qj2Stco8ziE3ZQN2AdpWCmkBFfYjuz8fGu") @Schema(description = "recipient's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
private String recipient; private String recipient;
@Schema(description = "amount to send", example = "123.456") @Schema(description = "amount to send", example = "123.456")
@XmlJavaTypeAdapter( @XmlJavaTypeAdapter(

View File

@ -17,7 +17,7 @@ public class RegisterNameTransactionData extends TransactionData {
// Properties // Properties
@Schema(description = "registrant's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") @Schema(description = "registrant's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
private byte[] registrantPublicKey; private byte[] registrantPublicKey;
@Schema(description = "new owner's address", example = "Qj2Stco8ziE3ZQN2AdpWCmkBFfYjuz8fGu") @Schema(description = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v")
private String owner; private String owner;
@Schema(description = "requested name", example = "my-name") @Schema(description = "requested name", example = "my-name")
private String name; private String name;

View File

@ -43,13 +43,17 @@ public abstract class TransactionData {
protected byte[] creatorPublicKey; protected byte[] creatorPublicKey;
@Schema(description = "timestamp when transaction created, in milliseconds since unix epoch", example = "1545062012000") @Schema(description = "timestamp when transaction created, in milliseconds since unix epoch", example = "1545062012000")
protected long timestamp; protected long timestamp;
@Schema(description = "sender's last transaction ID", example = "47fw82McxnTQ8wtTS5A51Qojhg62b8px1rF3FhJp5a3etKeb5Y2DniL4Q6E7GbVCs6BAjHVe6sA4gTPxtYzng3AX") @Schema(description = "sender's last transaction ID", example = "real_transaction_reference_in_base58")
protected byte[] reference; protected byte[] reference;
@Schema(description = "fee for processing transaction", example = "1.0") @Schema(description = "fee for processing transaction", example = "1.0")
protected BigDecimal fee; protected BigDecimal fee;
@Schema(accessMode = AccessMode.READ_ONLY, description = "signature for transaction's raw bytes, using sender's private key", example = "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC") @Schema(accessMode = AccessMode.READ_ONLY, description = "signature for transaction's raw bytes, using sender's private key", example = "real_transaction_signature_in_base58")
protected byte[] signature; protected byte[] signature;
// For JAX-RS use
@Schema(accessMode = AccessMode.READ_ONLY, description = "height of block containing transaction")
protected Integer blockHeight;
// Constructors // Constructors
// For JAX-RS // For JAX-RS
@ -116,6 +120,10 @@ public abstract class TransactionData {
this.creatorPublicKey = creatorPublicKey; this.creatorPublicKey = creatorPublicKey;
} }
public void setBlockHeight(int blockHeight) {
this.blockHeight = blockHeight;
}
// Comparison // Comparison
@Override @Override

View File

@ -76,7 +76,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
long timestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); long timestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
BigDecimal fee = resultSet.getBigDecimal(5).setScale(8); BigDecimal fee = resultSet.getBigDecimal(5).setScale(8);
return this.fromBase(type, signature, reference, creatorPublicKey, timestamp, fee); TransactionData transactionData = this.fromBase(type, signature, reference, creatorPublicKey, timestamp, fee);
return maybeIncludeBlockHeight(transactionData);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to fetch transaction from repository", e); throw new DataException("Unable to fetch transaction from repository", e);
} }
@ -95,12 +96,21 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
long timestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); long timestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
BigDecimal fee = resultSet.getBigDecimal(5).setScale(8); BigDecimal fee = resultSet.getBigDecimal(5).setScale(8);
return this.fromBase(type, signature, reference, creatorPublicKey, timestamp, fee); TransactionData transactionData = this.fromBase(type, signature, reference, creatorPublicKey, timestamp, fee);
return maybeIncludeBlockHeight(transactionData);
} catch (SQLException e) { } catch (SQLException e) {
throw new DataException("Unable to fetch transaction from repository", e); throw new DataException("Unable to fetch transaction from repository", e);
} }
} }
private TransactionData maybeIncludeBlockHeight(TransactionData transactionData) throws DataException {
int blockHeight = getHeightFromSignature(transactionData.getSignature());
if (blockHeight != 0)
transactionData.setBlockHeight(blockHeight);
return transactionData;
}
@Override @Override
public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException { public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute( try (ResultSet resultSet = this.repository.checkedExecute(