From 7998166c0a7ed46eda5408e873aee46ee0fbd939 Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 7 Jan 2019 13:12:42 +0000 Subject: [PATCH] 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. --- blockchain.json | 3 +- .../org/qora/api/resource/AdminResource.java | 4 +- .../api/resource/AnnotationPostProcessor.java | 2 +- .../api/resource/TransactionsResource.java | 58 ++++++++++++++++--- src/main/java/org/qora/block/BlockChain.java | 33 ++++++++--- .../java/org/qora/block/GenesisBlock.java | 37 +++++++++++- .../java/org/qora/controller/Controller.java | 5 ++ src/main/java/org/qora/crypto/Crypto.java | 20 +++++-- .../CancelOrderTransactionData.java | 2 +- .../IssueAssetTransactionData.java | 6 ++ .../transaction/PaymentTransactionData.java | 2 +- .../RegisterNameTransactionData.java | 2 +- .../data/transaction/TransactionData.java | 12 +++- .../HSQLDBTransactionRepository.java | 14 ++++- 14 files changed, 167 insertions(+), 33 deletions(-) diff --git a/blockchain.json b/blockchain.json index b04c3b5e..83563980 100644 --- a/blockchain.json +++ b/blockchain.json @@ -6,9 +6,10 @@ "blockTimestampMargin": 500, "maxBytesPerUnitFee": 1024, "unitFee": "1.0", + "useBrokenMD160ForAddresses": true, "genesis": { "assets": [ - { "name": "QORA", "description": "QORA coin" } + { "name": "QORA", "description": "QORA coin", "quantity": 10000000000, "isDivisible": true, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC" } ] "timestamp": "1400247274336", "generatingBalance": "10000000", diff --git a/src/main/java/org/qora/api/resource/AdminResource.java b/src/main/java/org/qora/api/resource/AdminResource.java index 988210f3..140a6120 100644 --- a/src/main/java/org/qora/api/resource/AdminResource.java +++ b/src/main/java/org/qora/api/resource/AdminResource.java @@ -28,10 +28,10 @@ public class AdminResource { @GET @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 = "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 = "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")) diff --git a/src/main/java/org/qora/api/resource/AnnotationPostProcessor.java b/src/main/java/org/qora/api/resource/AnnotationPostProcessor.java index 7944bd48..9d6ff0fc 100644 --- a/src/main/java/org/qora/api/resource/AnnotationPostProcessor.java +++ b/src/main/java/org/qora/api/resource/AnnotationPostProcessor.java @@ -66,7 +66,7 @@ public class AnnotationPostProcessor implements ReaderListener { if (apiErrors == null) 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); for (Operation operation : pathItem.readOperations()) diff --git a/src/main/java/org/qora/api/resource/TransactionsResource.java b/src/main/java/org/qora/api/resource/TransactionsResource.java index 70272704..b3d165cf 100644 --- a/src/main/java/org/qora/api/resource/TransactionsResource.java +++ b/src/main/java/org/qora/api/resource/TransactionsResource.java @@ -69,7 +69,7 @@ public class TransactionsResource { } ) @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; try { 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 @Path("/block/{signature}") @Operation( @@ -372,7 +413,7 @@ public class TransactionsResource { } ) @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()) { byte[] rawBytes = Base58.decode(rawBytes58); boolean hasSignature = true; @@ -388,12 +429,15 @@ public class TransactionsResource { } Transaction transaction = Transaction.fromData(repository, transactionData); - if (hasSignature && !transaction.isSignatureValid()) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE); - ValidationResult result = transaction.isValid(); - if (result != ValidationResult.OK) - throw createTransactionInvalidException(request, result); + if (!ignoreValidityChecks) { + if (hasSignature && !transaction.isSignatureValid()) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE); + + ValidationResult result = transaction.isValid(); + if (result != ValidationResult.OK) + throw createTransactionInvalidException(request, result); + } if (!hasSignature) transactionData.setSignature(null); diff --git a/src/main/java/org/qora/block/BlockChain.java b/src/main/java/org/qora/block/BlockChain.java index 61eb472b..4835cad3 100644 --- a/src/main/java/org/qora/block/BlockChain.java +++ b/src/main/java/org/qora/block/BlockChain.java @@ -11,7 +11,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.json.simple.JSONObject; -import org.qora.asset.Asset; import org.qora.data.asset.AssetData; import org.qora.data.block.BlockData; import org.qora.repository.BlockRepository; @@ -52,6 +51,9 @@ public class BlockChain { /** Map of which blockchain features are enabled when (height/timestamp) */ private Map> 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. private BlockChain() { @@ -59,7 +61,8 @@ public class BlockChain { public static BlockChain getInstance() { if (instance == null) - Settings.getInstance(); + // This will call BlockChain.fromJSON in turn + Settings.getInstance(); // synchronized return instance; } @@ -98,6 +101,10 @@ public class BlockChain { return this.blockTimestampMargin; } + public static boolean getUseBrokenMD160ForAddresses() { + return useBrokenMD160ForAddresses; + } + private long getFeatureTrigger(String feature, FeatureValueType valueType) { Map featureTrigger = featureTriggers.get(feature); if (featureTrigger == null) @@ -143,6 +150,14 @@ public class BlockChain { // Blockchain config from 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"); if (genesisJson == null) { LOGGER.error("No \"genesis\" entry found in blockchain config"); @@ -228,15 +243,15 @@ public class BlockChain { try (final Repository repository = RepositoryManager.getRepository()) { repository.rebuild(); - // Add Genesis Block GenesisBlock genesisBlock = GenesisBlock.getInstance(repository); - genesisBlock.process(); - // Add QORA asset. - // NOTE: Asset's transaction reference is Genesis Block's generator signature which doesn't exist as a transaction! - AssetData qoraAssetData = new AssetData(Asset.QORA, genesisBlock.getGenerator().getAddress(), "Qora", "This is the simulated Qora asset.", - BlockChain.getInstance().getMaxBalance().longValue(), true, genesisBlock.getBlockData().getGeneratorSignature()); - repository.getAssetRepository().save(qoraAssetData); + // Add initial assets + // NOTE: Asset's [transaction] reference doesn't exist as a transaction! + for (AssetData assetData : genesisBlock.getInitialAssets()) + repository.getAssetRepository().save(assetData); + + // Add Genesis Block to blockchain + genesisBlock.process(); repository.saveChanges(); } diff --git a/src/main/java/org/qora/block/GenesisBlock.java b/src/main/java/org/qora/block/GenesisBlock.java index 0d8be6c4..208b2e83 100644 --- a/src/main/java/org/qora/block/GenesisBlock.java +++ b/src/main/java/org/qora/block/GenesisBlock.java @@ -10,10 +10,12 @@ import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.bitcoinj.core.Base58; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.qora.account.GenesisAccount; import org.qora.crypto.Crypto; +import org.qora.data.asset.AssetData; import org.qora.data.block.BlockData; import org.qora.data.transaction.GenesisTransactionData; import org.qora.data.transaction.TransactionData; @@ -37,6 +39,7 @@ public class GenesisBlock extends Block { // Properties private static BlockData blockData; private static List transactionsData; + private static List initialAssets; // Constructors @@ -51,6 +54,8 @@ public class GenesisBlock extends Block { // Construction from JSON public static void fromJSON(JSONObject json) { + // All parsing first, then if successful we can proceed to construction + // Version 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"); } + // Generating balance + BigDecimal generatingBalance = Settings.getJsonBigDecimal(json, "generatingBalance"); + // Transactions JSONArray transactionsJson = (JSONArray) Settings.getTypedJson(json, "transactions", JSONArray.class); List transactions = new ArrayList<>(); @@ -96,8 +104,28 @@ public class GenesisBlock extends Block { } } - // Generating balance - BigDecimal generatingBalance = Settings.getJsonBigDecimal(json, "generatingBalance"); + // Assets + JSONArray assetsJson = (JSONArray) Settings.getTypedJson(json, "assets", JSONArray.class); + String genesisAddress = Crypto.toAddress(GenesisAccount.PUBLIC_KEY); + List 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; int transactionCount = transactions.size(); @@ -113,6 +141,7 @@ public class GenesisBlock extends Block { blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, generatorPublicKey, generatorSignature, atCount, atFees); transactionsData = transactions; + initialAssets = assets; } // More information @@ -134,6 +163,10 @@ public class GenesisBlock extends Block { return true; } + public List getInitialAssets() { + return Collections.unmodifiableList(initialAssets); + } + // Processing @Override diff --git a/src/main/java/org/qora/controller/Controller.java b/src/main/java/org/qora/controller/Controller.java index 6152879f..a1d99ab3 100644 --- a/src/main/java/org/qora/controller/Controller.java +++ b/src/main/java/org/qora/controller/Controller.java @@ -1,7 +1,10 @@ package org.qora.controller; +import java.security.Security; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.qora.api.ApiService; import org.qora.block.BlockChain; import org.qora.block.BlockGenerator; @@ -27,6 +30,8 @@ public class Controller { public static void main(String args[]) { LOGGER.info("Starting up..."); + Security.insertProviderAt(new BouncyCastleProvider(), 0); + // Load/check settings, which potentially sets up blockchain config, etc. Settings.getInstance(); diff --git a/src/main/java/org/qora/crypto/Crypto.java b/src/main/java/org/qora/crypto/Crypto.java index 05e68eeb..647df9bc 100644 --- a/src/main/java/org/qora/crypto/Crypto.java +++ b/src/main/java/org/qora/crypto/Crypto.java @@ -5,6 +5,7 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; import org.qora.account.Account; +import org.qora.block.BlockChain; import org.qora.utils.Base58; public class Crypto { @@ -28,7 +29,7 @@ public class Crypto { MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); return sha256.digest(input); } 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 byte[] inputHash = digest(input); - // Use BROKEN RIPEMD160 to create shorter address - BrokenMD160 brokenMD160 = new BrokenMD160(); - inputHash = brokenMD160.digest(inputHash); + // Use RIPEMD160 to create shorter address + if (BlockChain.getUseBrokenMD160ForAddresses()) { + // Legacy BROKEN MD160 + BrokenMD160 brokenMD160 = new BrokenMD160(); + 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) byte[] addressBytes = new byte[inputHash.length + 1]; diff --git a/src/main/java/org/qora/data/transaction/CancelOrderTransactionData.java b/src/main/java/org/qora/data/transaction/CancelOrderTransactionData.java index 27d0a02a..b7175885 100644 --- a/src/main/java/org/qora/data/transaction/CancelOrderTransactionData.java +++ b/src/main/java/org/qora/data/transaction/CancelOrderTransactionData.java @@ -17,7 +17,7 @@ import io.swagger.v3.oas.annotations.media.Schema; public class CancelOrderTransactionData extends TransactionData { // Properties - @Schema(description = "order ID to cancel", example = "2zYCM8P3PSzUxFNPAKFsSdwg9dWQcYTPCuKkuQbx3GVxTUVjXAUwEmEnvUUss11SZ3p38C16UfYb3cbXP9sRuqFx") + @Schema(description = "order ID to cancel", example = "real_order_ID_in_base58") private byte[] orderId; // Constructors diff --git a/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java b/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java index d37c4ce4..0dc95174 100644 --- a/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java +++ b/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java @@ -19,11 +19,17 @@ public class IssueAssetTransactionData extends TransactionData { // assetId can be null but assigned during save() or during load from repository @Schema(accessMode = AccessMode.READ_ONLY) private Long assetId = null; + @Schema(description = "asset issuer's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] issuerPublicKey; + @Schema(description = "asset owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") private String owner; + @Schema(description = "asset name", example = "GOLD") private String assetName; + @Schema(description = "asset description", example = "Gold asset - 1 unit represents one 1kg of gold") private String description; + @Schema(description = "total supply of asset in existence (integer)", example = "1000") private long quantity; + @Schema(description = "whether asset quantities can be fractional", example = "true") private boolean isDivisible; // Constructors diff --git a/src/main/java/org/qora/data/transaction/PaymentTransactionData.java b/src/main/java/org/qora/data/transaction/PaymentTransactionData.java index d9b82aef..c650ae82 100644 --- a/src/main/java/org/qora/data/transaction/PaymentTransactionData.java +++ b/src/main/java/org/qora/data/transaction/PaymentTransactionData.java @@ -18,7 +18,7 @@ public class PaymentTransactionData extends TransactionData { // Properties @Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] senderPublicKey; - @Schema(description = "recipient's address", example = "Qj2Stco8ziE3ZQN2AdpWCmkBFfYjuz8fGu") + @Schema(description = "recipient's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") private String recipient; @Schema(description = "amount to send", example = "123.456") @XmlJavaTypeAdapter( diff --git a/src/main/java/org/qora/data/transaction/RegisterNameTransactionData.java b/src/main/java/org/qora/data/transaction/RegisterNameTransactionData.java index 03b749b6..dcd4cd46 100644 --- a/src/main/java/org/qora/data/transaction/RegisterNameTransactionData.java +++ b/src/main/java/org/qora/data/transaction/RegisterNameTransactionData.java @@ -17,7 +17,7 @@ public class RegisterNameTransactionData extends TransactionData { // Properties @Schema(description = "registrant's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] registrantPublicKey; - @Schema(description = "new owner's address", example = "Qj2Stco8ziE3ZQN2AdpWCmkBFfYjuz8fGu") + @Schema(description = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") private String owner; @Schema(description = "requested name", example = "my-name") private String name; diff --git a/src/main/java/org/qora/data/transaction/TransactionData.java b/src/main/java/org/qora/data/transaction/TransactionData.java index 29bfa65c..76a760f1 100644 --- a/src/main/java/org/qora/data/transaction/TransactionData.java +++ b/src/main/java/org/qora/data/transaction/TransactionData.java @@ -43,13 +43,17 @@ public abstract class TransactionData { protected byte[] creatorPublicKey; @Schema(description = "timestamp when transaction created, in milliseconds since unix epoch", example = "1545062012000") 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; @Schema(description = "fee for processing transaction", example = "1.0") 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; + // For JAX-RS use + @Schema(accessMode = AccessMode.READ_ONLY, description = "height of block containing transaction") + protected Integer blockHeight; + // Constructors // For JAX-RS @@ -116,6 +120,10 @@ public abstract class TransactionData { this.creatorPublicKey = creatorPublicKey; } + public void setBlockHeight(int blockHeight) { + this.blockHeight = blockHeight; + } + // Comparison @Override diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index ebbb7a10..25854ba4 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -76,7 +76,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { long timestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); 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) { 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(); 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) { 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 public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException { try (ResultSet resultSet = this.repository.checkedExecute(