Browse Source

API: payments, issue asset, transaction decode, etc.

Added slow query check to HSQLDB repository to help
isolate cases where transaction searching takes too long.

Added BigDecimalTypeAdapter for normalizing API inputs
but doesn't seem to get reliably called so also added
.setScale(8) to BigDecimal serialization method.

API-built transactions are now validated before emitting
base58 raw transaction to help callers.

API's transaction decoder accepts signed/unsigned raw transactions.
pull/67/head
catbref 6 years ago
parent
commit
aab6b69da1
  1. 41
      src/api/AssetsResource.java
  2. 6
      src/api/Base58TypeAdapter.java
  3. 25
      src/api/BigDecimalTypeAdapter.java
  4. 16
      src/api/NamesResource.java
  5. 20
      src/api/PaymentsResource.java
  6. 79
      src/api/TransactionsResource.java
  7. 26
      src/api/models/IssueAssetRequest.java
  8. 6
      src/api/models/SimpleTransactionSignRequest.java
  9. 11
      src/data/package-info.java
  10. 3
      src/data/transaction/IssueAssetTransactionData.java
  11. 9
      src/data/transaction/PaymentTransactionData.java
  12. 15
      src/repository/hsqldb/HSQLDBRepository.java
  13. 1
      src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
  14. 9
      src/utils/Serialization.java

41
src/api/AssetsResource.java

@ -8,9 +8,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import qora.transaction.Transaction;
import qora.transaction.Transaction.ValidationResult;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
import transform.TransformationException;
import transform.transaction.IssueAssetTransactionTransformer;
import utils.Base58;
import java.util.ArrayList;
@ -27,13 +31,13 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import api.models.AssetWithHolders;
import api.models.IssueAssetRequest;
import api.models.OrderWithTrades;
import api.models.TradeWithOrderInfo;
import data.account.AccountBalanceData;
import data.assets.AssetData;
import data.assets.OrderData;
import data.assets.TradeData;
import data.transaction.IssueAssetTransactionData;
@Path("/assets")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@ -226,15 +230,36 @@ public class AssetsResource {
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = IssueAssetRequest.class)
schema = @Schema(implementation = IssueAssetTransactionData.class)
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned payment transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
public String issueAsset(IssueAssetRequest issueAssetRequest) {
// required: issuer (pubkey), name, description, quantity, isDivisible, fee
// optional: reference
// returns: raw tx
return "";
public String issueAsset(IssueAssetTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw new ApiException(400, ApiError.INVALID_DATA.getCode(), "Transaction invalid: " + result.name());
byte[] bytes = IssueAssetTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
}

6
src/api/Base58TypeAdapter.java

@ -15,11 +15,11 @@ public class Base58TypeAdapter extends XmlAdapter<String, byte[]> {
}
@Override
public String marshal(byte[] input) throws Exception {
if (input == null)
public String marshal(byte[] output) throws Exception {
if (output == null)
return null;
return Base58.encode(input);
return Base58.encode(output);
}
}

25
src/api/BigDecimalTypeAdapter.java

@ -0,0 +1,25 @@
package api;
import java.math.BigDecimal;
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class BigDecimalTypeAdapter extends XmlAdapter<String, BigDecimal> {
@Override
public BigDecimal unmarshal(String input) throws Exception {
if (input == null)
return null;
return new BigDecimal(input).setScale(8);
}
@Override
public String marshal(BigDecimal output) throws Exception {
if (output == null)
return null;
return output.toPlainString();
}
}

16
src/api/NamesResource.java

@ -6,6 +6,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import qora.transaction.Transaction;
import qora.transaction.Transaction.ValidationResult;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
import transform.TransformationException;
import transform.transaction.RegisterNameTransactionTransformer;
import utils.Base58;
@ -48,6 +53,7 @@ public class NamesResource {
@ApiResponse(
description = "raw, unsigned REGISTER_NAME transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
@ -56,11 +62,19 @@ public class NamesResource {
}
)
public String buildTransaction(RegisterNameTransactionData transactionData) {
try {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw new ApiException(400, ApiError.INVALID_DATA.getCode(), "Transaction invalid: " + result.name());
byte[] bytes = RegisterNameTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}

20
src/api/PaymentsResource.java

@ -6,6 +6,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import qora.transaction.Transaction;
import qora.transaction.Transaction.ValidationResult;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
import transform.TransformationException;
import transform.transaction.PaymentTransactionTransformer;
import utils.Base58;
@ -48,6 +53,7 @@ public class PaymentsResource {
@ApiResponse(
description = "raw, unsigned payment transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
@ -55,12 +61,20 @@ public class PaymentsResource {
)
}
)
public String buildTransaction(PaymentTransactionData paymentTransactionData) {
try {
byte[] bytes = PaymentTransactionTransformer.toBytes(paymentTransactionData);
public String buildTransaction(PaymentTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw new ApiException(400, ApiError.INVALID_DATA.getCode(), "Transaction invalid: " + result.name());
byte[] bytes = PaymentTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}

79
src/api/TransactionsResource.java

@ -33,13 +33,11 @@ import com.google.common.primitives.Bytes;
import api.models.SimpleTransactionSignRequest;
import data.transaction.GenesisTransactionData;
import data.transaction.PaymentTransactionData;
import data.transaction.RegisterNameTransactionData;
import data.transaction.TransactionData;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
import transform.TransformationException;
import transform.transaction.RegisterNameTransactionTransformer;
import transform.transaction.TransactionTransformer;
import utils.Base58;
@ -352,13 +350,18 @@ public class TransactionsResource {
)
public String signTransaction(SimpleTransactionSignRequest signRequest) {
try {
// Append null signature on the end
// Append null signature on the end before transformation
byte[] rawBytes = Bytes.concat(signRequest.transactionBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
PrivateKeyAccount signer = new PrivateKeyAccount(null, signRequest.privateKey);
Transaction transaction = Transaction.fromData(null, transactionData);
transaction.sign(signer);
byte[] signedBytes = TransactionTransformer.toBytes(transactionData);
return Base58.encode(signedBytes);
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
@ -375,7 +378,8 @@ public class TransactionsResource {
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "raw, signed transaction in base58 encoding"
description = "raw, signed transaction in base58 encoding",
example = "base58"
)
)
),
@ -400,8 +404,9 @@ public class TransactionsResource {
if (!transaction.isSignatureValid())
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE);
if (transaction.isValid() != ValidationResult.OK)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw new ApiException(400, ApiError.INVALID_DATA.getCode(), "Transaction invalid: " + result.name());
repository.getTransactionRepository().save(transactionData);
repository.getTransactionRepository().unconfirmTransaction(transactionData);
@ -417,4 +422,66 @@ public class TransactionsResource {
}
}
@POST
@Path("/decode")
@Operation(
summary = "Decode a raw, signed transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "raw, unsigned/signed transaction in base58 encoding",
example = "base58"
)
)
),
responses = {
@ApiResponse(
description = "a transaction",
content = @Content(
schema = @Schema(
implementation = TransactionData.class
)
)
)
}
)
public TransactionData decodeTransaction(String rawBytes58) {
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58);
boolean hasSignature = true;
TransactionData transactionData;
try {
transactionData = TransactionTransformer.fromBytes(rawBytes);
} catch (TransformationException e) {
// Maybe we're missing a signature, so append one and try one more time
rawBytes = Bytes.concat(rawBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
hasSignature = false;
transactionData = TransactionTransformer.fromBytes(rawBytes);
}
Transaction transaction = Transaction.fromData(repository, transactionData);
if (hasSignature && !transaction.isSignatureValid())
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw new ApiException(400, ApiError.INVALID_DATA.getCode(), "Transaction invalid: " + result.name());
if (!hasSignature)
transactionData.setSignature(null);
return transactionData;
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
}

26
src/api/models/IssueAssetRequest.java

@ -1,26 +0,0 @@
package api.models;
import java.math.BigDecimal;
import io.swagger.v3.oas.annotations.media.Schema;
public class IssueAssetRequest {
@Schema(description = "asset issuer's public key")
public byte[] issuer;
@Schema(description = "asset name - must be lowercase", example = "my-asset123")
public String name;
@Schema(description = "asset description")
public String description;
public BigDecimal quantity;
public boolean isDivisible;
public BigDecimal fee;
public byte[] reference;
}

6
src/api/models/SimpleTransactionSignRequest.java

@ -9,12 +9,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
public class SimpleTransactionSignRequest {
@Schema(
description = "signer's private key"
description = "signer's private key",
example = "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6"
)
public byte[] privateKey;
@Schema(
description = "raw, unsigned transaction bytes"
description = "raw, unsigned transaction bytes",
example = "base58"
)
public byte[] transactionBytes;

11
src/data/package-info.java

@ -1,6 +1,15 @@
// This file (data/package-info.java) is used as a template!
@XmlJavaTypeAdapter(type = byte[].class, value = api.Base58TypeAdapter.class)
@XmlJavaTypeAdapters({
@XmlJavaTypeAdapter(
type = byte[].class,
value = api.Base58TypeAdapter.class
), @XmlJavaTypeAdapter(
type = java.math.BigDecimal.class,
value = api.BigDecimalTypeAdapter.class
)
})
package data;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;

3
src/data/transaction/IssueAssetTransactionData.java

@ -6,6 +6,7 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
import qora.transaction.Transaction.TransactionType;
// All properties to be converted to JSON via JAX-RS
@ -15,6 +16,7 @@ public class IssueAssetTransactionData extends TransactionData {
// Properties
// assetId can be null but assigned during save() or during load from repository
@Schema(accessMode = AccessMode.READ_ONLY)
private Long assetId = null;
private byte[] issuerPublicKey;
private String owner;
@ -27,6 +29,7 @@ public class IssueAssetTransactionData extends TransactionData {
// For JAX-RS
protected IssueAssetTransactionData() {
super(TransactionType.ISSUE_ASSET);
}
public IssueAssetTransactionData(Long assetId, byte[] issuerPublicKey, String owner, String assetName, String description, long quantity,

9
src/data/transaction/PaymentTransactionData.java

@ -4,6 +4,7 @@ import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
import qora.transaction.Transaction.TransactionType;
@ -14,14 +15,22 @@ import qora.transaction.Transaction.TransactionType;
public class PaymentTransactionData extends TransactionData {
// Properties
@Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
private byte[] senderPublicKey;
@Schema(description = "recipient's address", example = "Qj2Stco8ziE3ZQN2AdpWCmkBFfYjuz8fGu")
private String recipient;
@Schema(description = "amount to send", example = "123.456")
@XmlJavaTypeAdapter(
type = BigDecimal.class,
value = api.BigDecimalTypeAdapter.class
)
private BigDecimal amount;
// Constructors
// For JAX-RS
protected PaymentTransactionData() {
super(TransactionType.PAYMENT);
}
public PaymentTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference,

15
src/repository/hsqldb/HSQLDBRepository.java

@ -24,6 +24,9 @@ import repository.hsqldb.transaction.HSQLDBTransactionRepository;
public class HSQLDBRepository implements Repository {
/** Queries that take longer than this (milliseconds) are logged */
private static final long MAX_QUERY_TIME = 1000L;
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class);
public static final TimeZone UTC = TimeZone.getTimeZone("UTC");
@ -136,10 +139,20 @@ public class HSQLDBRepository implements Repository {
@SuppressWarnings("resource")
public ResultSet checkedExecute(String sql, Object... objects) throws SQLException {
PreparedStatement preparedStatement = this.connection.prepareStatement(sql);
// Close the PreparedStatement when the ResultSet is closed otherwise there's a potential resource leak.
// We can't use try-with-resources here as closing the PreparedStatement on return would also prematurely close the ResultSet.
preparedStatement.closeOnCompletion();
return this.checkedExecuteResultSet(preparedStatement, objects);
long beforeQuery = System.currentTimeMillis();
ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement, objects);
long queryTime = System.currentTimeMillis() - beforeQuery;
if (queryTime > MAX_QUERY_TIME)
LOGGER.info(String.format("HSQLDB query took %d ms: %s", queryTime, sql));
return resultSet;
}
/**

1
src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java

@ -353,7 +353,6 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
String sql = "SELECT " + signatureColumn + " FROM " + String.join(" JOIN ", tableJoins) + " WHERE " + String.join(" AND ", whereClauses);
System.out.println("Transaction search SQL:\n" + sql);
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) {
if (resultSet == null)

9
src/utils/Serialization.java

@ -23,7 +23,9 @@ public class Serialization {
* @throws IOException
*/
public static byte[] serializeBigDecimal(BigDecimal amount, int length) throws IOException {
byte[] amountBytes = amount.unscaledValue().toByteArray();
// Note: we call .setScale(8) here to normalize values, especially values from API as they can have varying scale
// (At least until the BigDecimal XmlAdapter works - see data/package-info.java)
byte[] amountBytes = amount.setScale(8).unscaledValue().toByteArray();
byte[] output = new byte[length];
System.arraycopy(amountBytes, 0, output, length - amountBytes.length, amountBytes.length);
return output;
@ -49,10 +51,7 @@ public class Serialization {
* @throws IOException
*/
public static void serializeBigDecimal(ByteArrayOutputStream bytes, BigDecimal amount, int length) throws IOException {
byte[] amountBytes = amount.unscaledValue().toByteArray();
byte[] output = new byte[length];
System.arraycopy(amountBytes, 0, output, length - amountBytes.length, amountBytes.length);
bytes.write(output);
bytes.write(serializeBigDecimal(amount, length));
}
/**

Loading…
Cancel
Save