initial work towards OSGi

refactored packages so they all start with org.qora

added some attempt at OSGi mega bundle using Maven (doesn't work)
This commit is contained in:
catbref
2019-01-04 10:19:33 +00:00
parent 9e425d3877
commit 5c6e239d76
209 changed files with 1325 additions and 1320 deletions

View File

@@ -0,0 +1,190 @@
package org.qora.account;
import java.math.BigDecimal;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.asset.Asset;
import org.qora.block.Block;
import org.qora.block.BlockChain;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.BlockRepository;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.transaction.Transaction;
public class Account {
private static final Logger LOGGER = LogManager.getLogger(Account.class);
public static final int ADDRESS_LENGTH = 25;
protected Repository repository;
protected AccountData accountData;
protected Account() {
}
/** Construct Account business object using account's address */
public Account(Repository repository, String address) {
this.repository = repository;
this.accountData = new AccountData(address);
}
public String getAddress() {
return this.accountData.getAddress();
}
// More information
/**
* Calculate current generating balance for this account.
* <p>
* This is the current confirmed balance minus amounts received in the last <code>BlockChain.BLOCK_RETARGET_INTERVAL</code> blocks.
*
* @throws DataException
*/
public BigDecimal getGeneratingBalance() throws DataException {
BigDecimal balance = this.getConfirmedBalance(Asset.QORA);
BlockRepository blockRepository = this.repository.getBlockRepository();
BlockData blockData = blockRepository.getLastBlock();
for (int i = 1; i < BlockChain.getInstance().getBlockDifficultyInterval() && blockData != null && blockData.getHeight() > 1; ++i) {
Block block = new Block(this.repository, blockData);
// CIYAM AT transactions should be fetched from repository so no special handling needed here
for (Transaction transaction : block.getTransactions()) {
if (transaction.isInvolved(this)) {
final BigDecimal amount = transaction.getAmount(this);
// Subtract positive amounts only
if (amount.compareTo(BigDecimal.ZERO) > 0)
balance = balance.subtract(amount);
}
}
blockData = block.getParent();
}
// Do not go below 0
balance = balance.max(BigDecimal.ZERO);
return balance;
}
// Balance manipulations - assetId is 0 for QORA
public BigDecimal getBalance(long assetId, int confirmations) throws DataException {
// Simple case: we only need balance with 1 confirmation
if (confirmations == 1)
return this.getConfirmedBalance(assetId);
/*
* For a balance with more confirmations work back from last block, undoing transactions involving this account, until we have processed required number
* of blocks.
*/
BlockRepository blockRepository = this.repository.getBlockRepository();
BigDecimal balance = this.getConfirmedBalance(assetId);
BlockData blockData = blockRepository.getLastBlock();
// Note: "blockData.getHeight() > 1" to make sure we don't examine genesis block
for (int i = 1; i < confirmations && blockData != null && blockData.getHeight() > 1; ++i) {
Block block = new Block(this.repository, blockData);
// CIYAM AT transactions should be fetched from repository so no special handling needed here
for (Transaction transaction : block.getTransactions())
if (transaction.isInvolved(this))
balance = balance.subtract(transaction.getAmount(this));
blockData = block.getParent();
}
// Return balance
return balance;
}
public BigDecimal getConfirmedBalance(long assetId) throws DataException {
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.accountData.getAddress(), assetId);
if (accountBalanceData == null)
return BigDecimal.ZERO.setScale(8);
return accountBalanceData.getBalance();
}
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);
AccountBalanceData accountBalanceData = new AccountBalanceData(this.accountData.getAddress(), assetId, balance);
this.repository.getAccountRepository().save(accountBalanceData);
LOGGER.trace(this.accountData.getAddress() + " balance now: " + balance.toPlainString() + " [assetId " + assetId + "]");
}
public void deleteBalance(long assetId) throws DataException {
this.repository.getAccountRepository().delete(this.accountData.getAddress(), assetId);
}
// Reference manipulations
/**
* Fetch last reference for account.
*
* @return byte[] reference, or null if no reference or account not found.
* @throws DataException
*/
public byte[] getLastReference() throws DataException {
AccountData accountData = this.repository.getAccountRepository().getAccount(this.accountData.getAddress());
if (accountData == null)
return null;
return accountData.getReference();
}
/**
* Fetch last reference for account, considering unconfirmed transactions.
* <p>
* NOTE: <tt>repository.discardChanges()</tt> may be called during execution.
*
* @return byte[] reference, or null if no reference or account not found.
* @throws DataException
*/
public byte[] getUnconfirmedLastReference() throws DataException {
// Newest unconfirmed transaction takes priority
List<TransactionData> unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
byte[] reference = null;
for (TransactionData transactionData : unconfirmedTransactions) {
String address = PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey());
if (address.equals(this.accountData.getAddress()))
reference = transactionData.getSignature();
}
if (reference != null)
return reference;
// No unconfirmed transactions
return getLastReference();
}
/**
* Set last reference for account.
*
* @param reference
* -- null allowed
* @throws DataException
*/
public void setLastReference(byte[] reference) throws DataException {
accountData.setReference(reference);
this.repository.getAccountRepository().save(accountData);
}
}

View File

@@ -0,0 +1,13 @@
package org.qora.account;
import org.qora.repository.Repository;
public final class GenesisAccount extends PublicKeyAccount {
public static final byte[] PUBLIC_KEY = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 };
public GenesisAccount(Repository repository) {
super(repository, PUBLIC_KEY);
}
}

View File

@@ -0,0 +1,50 @@
package org.qora.account;
import org.qora.crypto.Crypto;
import org.qora.crypto.Ed25519;
import org.qora.data.account.AccountData;
import org.qora.repository.Repository;
import org.qora.utils.Pair;
public class PrivateKeyAccount extends PublicKeyAccount {
private byte[] seed;
private Pair<byte[], byte[]> keyPair;
/**
* Create PrivateKeyAccount using byte[32] seed.
*
* @param seed
* byte[32] used to create private/public key pair
* @throws IllegalArgumentException if passed invalid seed
*/
public PrivateKeyAccount(Repository repository, byte[] seed) {
this.repository = repository;
this.seed = seed;
this.keyPair = Ed25519.createKeyPair(seed);
byte[] publicKey = keyPair.getB();
this.accountData = new AccountData(Crypto.toAddress(publicKey), null, publicKey);
}
public byte[] getSeed() {
return this.seed;
}
public byte[] getPrivateKey() {
return this.keyPair.getA();
}
public Pair<byte[], byte[]> getKeyPair() {
return this.keyPair;
}
public byte[] sign(byte[] message) {
try {
return Ed25519.sign(this.keyPair, message);
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,38 @@
package org.qora.account;
import org.qora.crypto.Crypto;
import org.qora.crypto.Ed25519;
import org.qora.repository.Repository;
public class PublicKeyAccount extends Account {
public PublicKeyAccount(Repository repository, byte[] publicKey) {
super(repository, Crypto.toAddress(publicKey));
this.accountData.setPublicKey(publicKey);
}
protected PublicKeyAccount() {
}
public byte[] getPublicKey() {
return this.accountData.getPublicKey();
}
public boolean verify(byte[] signature, byte[] message) {
return PublicKeyAccount.verify(this.accountData.getPublicKey(), signature, message);
}
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
try {
return Ed25519.verify(signature, message, publicKey);
} catch (Exception e) {
return false;
}
}
public static String getAddress(byte[] publicKey) {
return Crypto.toAddress(publicKey);
}
}

View File

@@ -0,0 +1,141 @@
package org.qora.api;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.util.Map;
public enum ApiError {
// COMMON
UNKNOWN(0, 500),
JSON(1, 400),
NO_BALANCE(2, 422),
NOT_YET_RELEASED(3, 422),
UNAUTHORIZED(4, 403),
REPOSITORY_ISSUE(5, 500),
// VALIDATION
INVALID_SIGNATURE(101, 400),
INVALID_ADDRESS(102, 400),
INVALID_SEED(103, 400),
INVALID_AMOUNT(104, 400),
INVALID_FEE(105, 400),
INVALID_SENDER(106, 400),
INVALID_RECIPIENT(107, 400),
INVALID_NAME_LENGTH(108, 400),
INVALID_VALUE_LENGTH(109, 400),
INVALID_NAME_OWNER(110, 400),
INVALID_BUYER(111, 400),
INVALID_PUBLIC_KEY(112, 400),
INVALID_OPTIONS_LENGTH(113, 400),
INVALID_OPTION_LENGTH(114, 400),
INVALID_DATA(115, 400),
INVALID_DATA_LENGTH(116, 400),
INVALID_UPDATE_VALUE(117, 400),
KEY_ALREADY_EXISTS(118, 422),
KEY_NOT_EXISTS(119, 404),
LAST_KEY_IS_DEFAULT_KEY_ERROR(120, 422),
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),
INVALID_REFERENCE(126, 400),
TRANSFORMATION_ERROR(127, 400),
INVALID_PRIVATE_KEY(128, 400),
// WALLET
WALLET_NO_EXISTS(201, 404),
WALLET_ADDRESS_NO_EXISTS(202, 404),
WALLET_LOCKED(203, 422),
WALLET_ALREADY_EXISTS(204, 422),
WALLET_API_CALL_FORBIDDEN_BY_USER(205, 403),
// BLOCKS
BLOCK_NO_EXISTS(301, 404),
// TRANSACTIONS
TRANSACTION_NO_EXISTS(311, 404),
PUBLIC_KEY_NOT_FOUND(304, 404),
TRANSACTION_INVALID(312, 400),
// NAMING
NAME_NO_EXISTS(401, 404),
NAME_ALREADY_EXISTS(402, 422),
NAME_ALREADY_FOR_SALE(403, 422),
NAME_NOT_LOWER_CASE(404, 422),
NAME_SALE_NO_EXISTS(410, 404),
BUYER_ALREADY_OWNER(411, 422),
// POLLS
POLL_NO_EXISTS(501, 404),
POLL_ALREADY_EXISTS(502, 422),
DUPLICATE_OPTION(503, 422),
POLL_OPTION_NO_EXISTS(504, 404),
ALREADY_VOTED_FOR_THAT_OPTION(505, 422),
// ASSET
INVALID_ASSET_ID(601, 400),
INVALID_ORDER_ID(602, 400),
ORDER_NO_EXISTS(603, 404),
// NAME PAYMENTS
NAME_NOT_REGISTERED(701, 422),
NAME_FOR_SALE(702, 422),
NAME_WITH_SPACE(703, 422),
// ATs
INVALID_DESC_LENGTH(801, 400),
EMPTY_CODE(802, 400),
DATA_SIZE(803, 400),
NULL_PAGES(804, 400),
INVALID_TYPE_LENGTH(805, 400),
INVALID_TAGS_LENGTH(806, 400),
INVALID_CREATION_BYTES(809, 400),
// BLOG/Namestorage
BODY_EMPTY(901, 400),
BLOG_DISABLED(902, 403),
NAME_NOT_OWNER(903, 422),
TX_AMOUNT(904, 400),
BLOG_ENTRY_NO_EXISTS(905, 404),
BLOG_EMPTY(906, 404),
POSTID_EMPTY(907, 400),
POST_NOT_EXISTING(908, 404),
COMMENTING_DISABLED(909, 403),
COMMENT_NOT_EXISTING(910, 404),
INVALID_COMMENT_OWNER(911, 422),
// Messages
MESSAGE_FORMAT_NOT_HEX(1001, 400),
MESSAGE_BLANK(1002, 400),
NO_PUBLIC_KEY(1003, 422),
MESSAGESIZE_EXCEEDED(1004, 400);
private final static Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));
private final int code; // API error code
private final int status; // HTTP status code
private ApiError(int code) {
this(code, 400); // defaults to "400 - BAD REQUEST"
}
private ApiError(int code, int status) {
this.code = code;
this.status = status;
}
public static ApiError fromCode(int code) {
return map.get(code);
}
public int getCode() {
return this.code;
}
public int getStatus() {
return this.status;
}
}

View File

@@ -0,0 +1,22 @@
package org.qora.api;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class ApiErrorMessage {
protected int error;
protected String message;
protected ApiErrorMessage() {
}
public ApiErrorMessage(int errorCode, String message) {
this.error = errorCode;
this.message = message;
}
}

View File

@@ -0,0 +1,18 @@
package org.qora.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation lists potential ApiErrors that may be returned, or thrown, during the execution of this method.
* <p>
* Value is expected to be an array of ApiError enum instances.
*
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrors {
ApiError[] value() default {};
}

View File

@@ -0,0 +1,38 @@
package org.qora.api;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
public class ApiException extends WebApplicationException {
private static final long serialVersionUID = 4619299036312089050L;
// HTTP status code
public int status;
// API error code
public int error;
public String message;
public ApiException(int status, int error, String message) {
this(status, error, message, null);
}
public ApiException(int status, int error, String message, Throwable throwable) {
super(
message,
throwable,
Response.status(Status.fromStatusCode(status))
.entity(new ApiErrorMessage(error, message))
.type(MediaType.APPLICATION_JSON)
.build()
);
this.status = status;
this.error = error;
this.message = message;
}
}

View File

@@ -0,0 +1,19 @@
package org.qora.api;
import javax.servlet.http.HttpServletRequest;
import org.qora.globalization.Translator;
public enum ApiExceptionFactory {
INSTANCE;
public ApiException createException(HttpServletRequest request, ApiError apiError, Throwable throwable, Object... args) {
String message = Translator.INSTANCE.translate("ApiError", request.getLocale().getLanguage(), apiError.name(), args);
return new ApiException(apiError.getStatus(), apiError.getCode(), message, throwable);
}
public ApiException createException(HttpServletRequest request, ApiError apiError) {
return createException(request, apiError, null);
}
}

View File

@@ -0,0 +1,108 @@
package org.qora.api;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.InetAccessHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.qora.api.resource.AnnotationPostProcessor;
import org.qora.api.resource.ApiDefinition;
import org.qora.settings.Settings;
public class ApiService {
private final Server server;
private final ResourceConfig config;
public ApiService() {
config = new ResourceConfig();
config.packages("api.resource");
config.register(OpenApiResource.class);
config.register(ApiDefinition.class);
config.register(AnnotationPostProcessor.class);
// Create RPC server
this.server = new Server(Settings.getInstance().getRpcPort());
// IP address based access control
InetAccessHandler accessHandler = new InetAccessHandler();
for (String pattern : Settings.getInstance().getRpcAllowed()) {
accessHandler.include(pattern);
}
this.server.setHandler(accessHandler);
// URL rewriting
RewriteHandler rewriteHandler = new RewriteHandler();
accessHandler.setHandler(rewriteHandler);
// Context
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
context.setContextPath("/");
rewriteHandler.setHandler(context);
FilterHolder filterHolder = new FilterHolder(CrossOriginFilter.class);
filterHolder.setInitParameter("allowedOrigins", "*");
filterHolder.setInitParameter("allowedMethods", "GET, POST");
context.addFilter(filterHolder, "/*", null);
// API servlet
ServletContainer container = new ServletContainer(config);
ServletHolder apiServlet = new ServletHolder(container);
apiServlet.setInitOrder(1);
context.addServlet(apiServlet, "/*");
// Swagger-UI static content
ClassLoader loader = this.getClass().getClassLoader();
ServletHolder swaggerUIServlet = new ServletHolder("static-swagger-ui", DefaultServlet.class);
swaggerUIServlet.setInitParameter("resourceBase", loader.getResource("resources/swagger-ui/").toString());
swaggerUIServlet.setInitParameter("dirAllowed", "true");
swaggerUIServlet.setInitParameter("pathInfoOnly", "true");
context.addServlet(swaggerUIServlet, "/api-documentation/*");
rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/index.html")); // redirect to swagger ui start page
}
// XXX: replace singleton pattern by dependency injection?
private static ApiService instance;
public static ApiService getInstance() {
if (instance == null) {
instance = new ApiService();
}
return instance;
}
public Iterable<Class<?>> getResources() {
// return resources;
return config.getClasses();
}
public void start() {
try {
// Start server
server.start();
} catch (Exception e) {
// Failed to start
throw new RuntimeException("Failed to start API", e);
}
}
public void stop() {
try {
// Stop server
server.stop();
} catch (Exception e) {
// Failed to stop
}
}
}

View File

@@ -0,0 +1,25 @@
package org.qora.api;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import org.bitcoinj.core.Base58;
public class Base58TypeAdapter extends XmlAdapter<String, byte[]> {
@Override
public byte[] unmarshal(String input) throws Exception {
if (input == null)
return null;
return Base58.decode(input);
}
@Override
public String marshal(byte[] output) throws Exception {
if (output == null)
return null;
return Base58.encode(output);
}
}

View File

@@ -0,0 +1,25 @@
package org.qora.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();
}
}

View File

@@ -0,0 +1,76 @@
package org.qora.api;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.responses.ApiResponse;
import static java.util.Arrays.asList;
import java.util.List;
class Constants {
public static final String APIERROR_CONTEXT_PATH = "/Api";
public static final String APIERROR_KEY = "ApiError/%s";
public static final String TRANSLATION_EXTENSION_NAME = "translation";
public static final String TRANSLATION_PATH_EXTENSION_NAME = "path";
public static final String TRANSLATION_ANNOTATION_DESCRIPTION_KEY = "description.key";
public static final String TRANSLATION_ANNOTATION_SUMMARY_KEY = "summary.key";
public static final String TRANSLATION_ANNOTATION_TITLE_KEY = "title.key";
public static final String TRANSLATION_ANNOTATION_TERMS_OF_SERVICE_KEY = "termsOfService.key";
public static final String API_ERRORS_EXTENSION_NAME = "apiErrors";
public static final String API_ERROR_CODE_EXTENSION_NAME = "apiErrorCode";
public static final List<TranslatableProperty<Info>> TRANSLATABLE_INFO_PROPERTIES = asList(
new TranslatableProperty<Info>() {
@Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; }
@Override public void setValue(Info item, String translation) { item.setDescription(translation); }
@Override public String getValue(Info item) { return item.getDescription(); }
},
new TranslatableProperty<Info>() {
@Override public String keyName() { return TRANSLATION_ANNOTATION_TITLE_KEY; }
@Override public void setValue(Info item, String translation) { item.setTitle(translation); }
@Override public String getValue(Info item) { return item.getTitle(); }
},
new TranslatableProperty<Info>() {
@Override public String keyName() { return TRANSLATION_ANNOTATION_TERMS_OF_SERVICE_KEY; }
@Override public void setValue(Info item, String translation) { item.setTermsOfService(translation); }
@Override public String getValue(Info item) { return item.getTermsOfService(); }
}
);
public static final List<TranslatableProperty<PathItem>> TRANSLATABLE_PATH_ITEM_PROPERTIES = asList(
new TranslatableProperty<PathItem>() {
@Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; }
@Override public void setValue(PathItem item, String translation) { item.setDescription(translation); }
@Override public String getValue(PathItem item) { return item.getDescription(); }
},
new TranslatableProperty<PathItem>() {
@Override public String keyName() { return TRANSLATION_ANNOTATION_SUMMARY_KEY; }
@Override public void setValue(PathItem item, String translation) { item.setSummary(translation); }
@Override public String getValue(PathItem item) { return item.getSummary(); }
}
);
public static final List<TranslatableProperty<Operation>> TRANSLATABLE_OPERATION_PROPERTIES = asList(
new TranslatableProperty<Operation>() {
@Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; }
@Override public void setValue(Operation item, String translation) { item.setDescription(translation); }
@Override public String getValue(Operation item) { return item.getDescription(); }
},
new TranslatableProperty<Operation>() {
@Override public String keyName() { return TRANSLATION_ANNOTATION_SUMMARY_KEY; }
@Override public void setValue(Operation item, String translation) { item.setSummary(translation); }
@Override public String getValue(Operation item) { return item.getSummary(); }
}
);
public static final List<TranslatableProperty<ApiResponse>> TRANSLATABLE_API_RESPONSE_PROPERTIES = asList(
new TranslatableProperty<ApiResponse>() {
@Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; }
@Override public void setValue(ApiResponse item, String translation) { item.setDescription(translation); }
@Override public String getValue(ApiResponse item) { return item.getDescription(); }
}
);
}

View File

@@ -0,0 +1,22 @@
package org.qora.api;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.servlet.http.HttpServletRequest;
public class Security {
// TODO: replace with proper authentication
public static void checkApiCallAllowed(HttpServletRequest request) {
InetAddress remoteAddr;
try {
remoteAddr = InetAddress.getByName(request.getRemoteAddr());
} catch (UnknownHostException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
}
if (!remoteAddr.isLoopbackAddress())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
}
}

View File

@@ -0,0 +1,16 @@
package org.qora.api;
import org.eclipse.persistence.descriptors.ClassExtractor;
import org.eclipse.persistence.sessions.Record;
import org.eclipse.persistence.sessions.Session;
public class TransactionClassExtractor extends ClassExtractor {
@SuppressWarnings("rawtypes")
@Override
public Class extractClassFromRow(Record record, Session session) {
// Never called anyway?
return null;
}
}

View File

@@ -0,0 +1,7 @@
package org.qora.api;
interface TranslatableProperty<T> {
public String keyName();
public void setValue(T item, String translation);
public String getValue(T item);
}

View File

@@ -0,0 +1,18 @@
package org.qora.api;
import javax.xml.bind.Unmarshaller.Listener;
import org.qora.data.transaction.TransactionData;
public class UnmarshalListener extends Listener {
@Override
public void afterUnmarshal(Object target, Object parent) {
if (!(target instanceof TransactionData))
return;
// do something
return;
}
}

View File

@@ -0,0 +1,34 @@
package org.qora.api.model;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.asset.AssetData;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Asset info, maybe including asset holders")
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class AssetWithHolders {
@Schema(implementation = AssetData.class, name = "asset", title = "asset data")
@XmlElement(name = "asset")
public AssetData assetData;
public List<AccountBalanceData> holders;
// For JAX-RS
protected AssetWithHolders() {
}
public AssetWithHolders(AssetData assetData, List<AccountBalanceData> holders) {
this.assetData = assetData;
this.holders = holders;
}
}

View File

@@ -0,0 +1,34 @@
package org.qora.api.model;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Block info, maybe including transactions")
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockWithTransactions {
@Schema(implementation = BlockData.class, name = "block", title = "block data")
@XmlElement(name = "block")
public BlockData blockData;
public List<TransactionData> transactions;
// For JAX-RS
protected BlockWithTransactions() {
}
public BlockWithTransactions(BlockData blockData, List<TransactionData> transactions) {
this.blockData = blockData;
this.transactions = transactions;
}
}

View File

@@ -0,0 +1,34 @@
package org.qora.api.model;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.TradeData;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Asset order info, maybe including trades")
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class OrderWithTrades {
@Schema(implementation = OrderData.class, name = "order", title = "order data")
@XmlElement(name = "order")
public OrderData orderData;
List<TradeData> trades;
// For JAX-RS
protected OrderWithTrades() {
}
public OrderWithTrades(OrderData orderData, List<TradeData> trades) {
this.orderData = orderData;
this.trades = trades;
}
}

View File

@@ -0,0 +1,23 @@
package org.qora.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class SimpleTransactionSignRequest {
@Schema(
description = "signer's private key",
example = "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6"
)
public byte[] privateKey;
@Schema(
description = "raw, unsigned transaction bytes",
example = "base58"
)
public byte[] transactionBytes;
}

View File

@@ -0,0 +1,39 @@
package org.qora.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.TradeData;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Asset trade, including order info")
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeWithOrderInfo {
@Schema(implementation = TradeData.class, name = "trade", title = "trade data")
@XmlElement(name = "trade")
public TradeData tradeData;
@Schema(implementation = OrderData.class, name = "order", title = "order data")
@XmlElement(name = "initiatingOrder")
public OrderData initiatingOrderData;
@Schema(implementation = OrderData.class, name = "order", title = "order data")
@XmlElement(name = "targetOrder")
public OrderData targetOrderData;
// For JAX-RS
protected TradeWithOrderInfo() {
}
public TradeWithOrderInfo(TradeData tradeData, OrderData initiatingOrderData, OrderData targetOrderData) {
this.tradeData = tradeData;
this.initiatingOrderData = initiatingOrderData;
this.targetOrderData = targetOrderData;
}
}

View File

@@ -0,0 +1,284 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
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 java.math.BigDecimal;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qora.account.Account;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.transform.Transformer;
import org.qora.utils.Base58;
@Path("/addresses")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Addresses")
public class AddressesResource {
@Context
HttpServletRequest request;
@GET
@Path("/lastreference/{address}")
@Operation(
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions",
description = "Returns the base58-encoded signature of the last confirmed/unconfirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no transactions.",
responses = {
@ApiResponse(
description = "the base58-encoded transaction signature",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
byte[] lastReference = null;
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
lastReference = account.getUnconfirmedLastReference();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
if(lastReference == null || lastReference.length == 0) {
return "false";
} else {
return Base58.encode(lastReference);
}
}
@GET
@Path("/validate/{address}")
@Operation(
summary = "Validates the given address",
description = "Returns true/false.",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
)
}
)
public boolean validate(@PathParam("address") String address) {
return Crypto.isValidAddress(address);
}
@GET
@Path("/generatingbalance/{address}")
@Operation(
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.",
responses = {
@ApiResponse(
description = "the generating balance",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public BigDecimal getGeneratingBalanceOfAddress(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
return account.getGeneratingBalance();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/balance/{address}")
@Operation(
summary = "Returns the confirmed balance of the given address",
responses = {
@ApiResponse(
description = "the balance",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public BigDecimal getGeneratingBalance(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
return account.getConfirmedBalance(Asset.QORA);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, 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.",
responses = {
@ApiResponse(
description = "the balance",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public BigDecimal getAssetBalance(@PathParam("assetid") long assetid, @PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
return account.getConfirmedBalance(assetid);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/assets/{address}")
@Operation(
summary = "All assets owned by this address",
description = "Returns the list of assets for this address, with balances.",
responses = {
@ApiResponse(
description = "the list of assets",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AccountBalanceData.class)))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<AccountBalanceData> getAssets(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getAccountRepository().getAllBalances(address);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/balance/{address}/{confirmations}")
@Operation(
summary = "Calculates the balance of the given address for the given confirmations",
responses = {
@ApiResponse(
description = "the balance",
content = @Content(schema = @Schema(type = "string", format = "number"))
)
}
)
public String getGeneratingBalance(@PathParam("address") String address, @PathParam("confirmations") int confirmations) {
throw new UnsupportedOperationException();
}
@GET
@Path("/publickey/{address}")
@Operation(
summary = "Get public key of address",
description = "Returns the base58-encoded account public key of the given address, or \"false\" if address not known or has no public key.",
responses = {
@ApiResponse(
description = "the public key",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public String getPublicKey(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, 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 Base58.encode(publicKey);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/convert/{publickey}")
@Operation(
summary = "Convert public key into address",
description = "Returns account address based on supplied public key. Expects base58-encoded, 32-byte public key.",
responses = {
@ApiResponse(
description = "the address",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.REPOSITORY_ISSUE})
public String fromPublicKey(@PathParam("publickey") String publicKey58) {
// Decode public key
byte[] publicKey;
try {
publicKey = Base58.decode(publicKey58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
}
// Correct size for public key?
if (publicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
try (final Repository repository = RepositoryManager.getRepository()) {
return Crypto.toAddress(publicKey);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -0,0 +1,86 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
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 javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qora.api.Security;
import org.qora.controller.Controller;
@Path("/admin")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Admin")
public class AdminResource {
@Context
HttpServletRequest request;
@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 = "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.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"))
@Parameter(in = ParameterIn.QUERY, name = "includeTransactions", description = "Include associated transactions in results", schema = @Schema(type = "boolean"))
@Parameter(in = ParameterIn.QUERY, name = "includeHolders", description = "Include asset holders in results", schema = @Schema(type = "boolean"))
@Parameter(in = ParameterIn.QUERY, name = "queryAssetId", description = "Asset ID, 0 is native coin", schema = @Schema(type = "string", format = "byte"))
public String globalParameters() {
return "";
}
@GET
@Path("/uptime")
@Operation(
summary = "Fetch running time of server",
description = "Returns uptime in milliseconds",
responses = {
@ApiResponse(
description = "uptime in milliseconds",
content = @Content(schema = @Schema(type = "number"))
)
}
)
public long uptime() {
return System.currentTimeMillis() - Controller.startTime;
}
@GET
@Path("/stop")
@Operation(
summary = "Shutdown",
description = "Shutdown",
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(schema = @Schema(type = "string"))
)
}
)
public String shutdown() {
Security.checkApiCallAllowed(request);
new Thread(new Runnable() {
@Override
public void run() {
Controller.shutdown();
}
}).start();
return "true";
}
}

View File

@@ -0,0 +1,113 @@
package org.qora.api.resource;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.jaxrs2.Reader;
import io.swagger.v3.jaxrs2.ReaderListener;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import java.lang.reflect.Method;
import java.util.Locale;
import javax.ws.rs.Path;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.api.ApiError;
import org.qora.api.ApiErrorMessage;
import org.qora.api.ApiErrors;
import org.qora.api.ApiService;
import org.qora.globalization.Translator;
public class AnnotationPostProcessor implements ReaderListener {
private static final Logger LOGGER = LogManager.getLogger(AnnotationPostProcessor.class);
@Override
public void beforeScan(Reader reader, OpenAPI openAPI) {
}
@Override
public void afterScan(Reader reader, OpenAPI openAPI) {
// Populate Components section with reusable parameters, like "limit" and "offset"
// We take the reusable parameters from AdminResource.globalParameters path "/admin/unused"
Components components = openAPI.getComponents();
PathItem globalParametersPathItem = openAPI.getPaths().get("/admin/unused");
if (globalParametersPathItem != null) {
for (Parameter parameter : globalParametersPathItem.getGet().getParameters())
components.addParameters(parameter.getName(), parameter);
openAPI.getPaths().remove("/admin/unused");
}
// Search all ApiService resources (classes) for @ApiErrors annotations
// to generate corresponding openAPI operation responses.
for (Class<?> clazz : ApiService.getInstance().getResources()) {
Path classPath = clazz.getAnnotation(Path.class);
if (classPath == null)
continue;
String classPathString = classPath.value();
if (classPathString.charAt(0) != '/')
classPathString = "/" + classPathString;
for (Method method : clazz.getDeclaredMethods()) {
ApiErrors apiErrors = method.getAnnotation(ApiErrors.class);
if (apiErrors == null)
continue;
LOGGER.info("Found @ApiErrors annotation on " + clazz.getSimpleName() + "." + method.getName());
PathItem pathItem = getPathItemFromMethod(openAPI, classPathString, method);
for (Operation operation : pathItem.readOperations())
for (ApiError apiError : apiErrors.value())
addApiErrorResponse(operation, apiError);
}
}
}
private PathItem getPathItemFromMethod(OpenAPI openAPI, String classPathString, Method method) {
Path path = method.getAnnotation(Path.class);
if (path == null)
throw new RuntimeException("API method has no @Path annotation?");
String pathString = path.value();
return openAPI.getPaths().get(classPathString + pathString);
}
private void addApiErrorResponse(Operation operation, ApiError apiError) {
String statusCode = Integer.toString(apiError.getStatus()) + " " + apiError.name();
// Create response for this HTTP response code if it doesn't already exist
ApiResponse apiResponse = operation.getResponses().get(statusCode);
if (apiResponse == null) {
Schema<?> errorMessageSchema = ModelConverters.getInstance().readAllAsResolvedSchema(ApiErrorMessage.class).schema;
MediaType mediaType = new MediaType().schema(errorMessageSchema);
Content content = new Content().addMediaType(javax.ws.rs.core.MediaType.APPLICATION_JSON, mediaType);
apiResponse = new ApiResponse().content(content);
operation.getResponses().addApiResponse(statusCode, apiResponse);
}
// Add this specific ApiError code as an example
int apiErrorCode = apiError.getCode();
String lang = Locale.getDefault().getLanguage();
ApiErrorMessage apiErrorMessage = new ApiErrorMessage(apiErrorCode, Translator.INSTANCE.translate("ApiError", lang, apiError.name()));
Example example = new Example().value(apiErrorMessage);
// XXX: addExamples(..) is not working in Swagger 2.0.4. This bug is referenced in https://github.com/swagger-api/swagger-ui/issues/2651
// Replace the call to .setExample(..) by .addExamples(..) when the bug is fixed.
apiResponse.getContent().get(javax.ws.rs.core.MediaType.APPLICATION_JSON).setExample(example);
//apiResponse.getContent().get(javax.ws.rs.core.MediaType.APPLICATION_JSON).addExamples(Integer.toString(apiErrorCode), example);
}
}

View File

@@ -0,0 +1,28 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.tags.Tag;
@OpenAPIDefinition(
info = @Info( title = "Qora API", description = "NOTE: byte-arrays are encoded in Base58" ),
tags = {
@Tag(name = "Addresses"),
@Tag(name = "Admin"),
@Tag(name = "Assets"),
@Tag(name = "Blocks"),
@Tag(name = "Names"),
@Tag(name = "Payments"),
@Tag(name = "Transactions"),
@Tag(name = "Utilities")
},
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="title.key", value="info:title")
})
}
)
public class ApiDefinition {
}

View File

@@ -0,0 +1,274 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
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 java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.AssetWithHolders;
import org.qora.api.model.OrderWithTrades;
import org.qora.api.model.TradeWithOrderInfo;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.asset.AssetData;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.TradeData;
import org.qora.data.transaction.IssueAssetTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
import org.qora.transform.transaction.IssueAssetTransactionTransformer;
import org.qora.utils.Base58;
@Path("/assets")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Assets")
public class AssetsResource {
@Context
HttpServletRequest request;
@GET
@Path("/all")
@Operation(
summary = "List all known assets",
responses = {
@ApiResponse(
description = "asset info",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetData.class)))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<AssetData> getAllAssets(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
List<AssetData> assets = repository.getAssetRepository().getAllAssets();
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, assets.size());
int toIndex = limit == 0 ? assets.size() : Integer.min(fromIndex + limit, assets.size());
assets = assets.subList(fromIndex, toIndex);
return assets;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/info")
@Operation(
summary = "Info on specific asset",
description = "Supply either assetId OR assetName. (If both supplied, assetId takes priority).",
responses = {
@ApiResponse(
description = "asset info",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetWithHolders.class)))
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE})
public AssetWithHolders getAssetInfo(@QueryParam("assetId") Integer assetId, @QueryParam("assetName") String assetName, @Parameter(ref = "includeHolders") @QueryParam("includeHolders") boolean includeHolders) {
if (assetId == null && (assetName == null || assetName.isEmpty()))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
AssetData assetData = null;
if (assetId != null)
assetData = repository.getAssetRepository().fromAssetId(assetId);
else
assetData = repository.getAssetRepository().fromAssetName(assetName);
if (assetData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<AccountBalanceData> holders = null;
if (includeHolders)
holders = repository.getAccountRepository().getAssetBalances(assetData.getAssetId());
return new AssetWithHolders(assetData, holders);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/orderbook/{assetId}/{otherAssetId}")
@Operation(
summary = "Asset order book",
description = "Returns open orders, offering {assetId} for {otherAssetId} in return.",
responses = {
@ApiResponse(
description = "asset orders",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = OrderData.class)))
)
}
)
@ApiErrors({ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE})
public List<OrderData> getAssetOrders(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId,
@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<OrderData> orders = repository.getAssetRepository().getOpenOrders(assetId, otherAssetId);
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, orders.size());
int toIndex = limit == 0 ? orders.size() : Integer.min(fromIndex + limit, orders.size());
orders = orders.subList(fromIndex, toIndex);
return orders;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/trades/{assetId}/{otherAssetId}")
@Operation(
summary = "Asset trades",
description = "Returns successful trades of {assetId} for {otherAssetId}.<br>" +
"Does NOT include trades of {otherAssetId} for {assetId}!<br>" +
"\"Initiating\" order is the order that caused the actual trade by matching up with the \"target\" order.",
responses = {
@ApiResponse(
description = "asset trades",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TradeWithOrderInfo.class)))
)
}
)
@ApiErrors({ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE})
public List<TradeWithOrderInfo> getAssetTrades(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId,
@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<TradeData> trades = repository.getAssetRepository().getTrades(assetId, otherAssetId);
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, trades.size());
int toIndex = limit == 0 ? trades.size() : Integer.min(fromIndex + limit, trades.size());
trades = trades.subList(fromIndex, toIndex);
// Expanding remaining entries
List<TradeWithOrderInfo> fullTrades = new ArrayList<>();
for (TradeData tradeData : trades) {
OrderData initiatingOrderData = repository.getAssetRepository().fromOrderId(tradeData.getInitiator());
OrderData targetOrderData = repository.getAssetRepository().fromOrderId(tradeData.getTarget());
fullTrades.add(new TradeWithOrderInfo(tradeData, initiatingOrderData, targetOrderData));
}
return fullTrades;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/order/{orderId}")
@Operation(
summary = "Fetch asset order",
description = "Returns asset order info.",
responses = {
@ApiResponse(
description = "asset order",
content = @Content(schema = @Schema(implementation = OrderData.class))
)
}
)
@ApiErrors({ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public OrderWithTrades getAssetOrder(@PathParam("orderId") String orderId58) {
// Decode orderID
byte[] orderId;
try {
orderId = Base58.decode(orderId58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ORDER_ID, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
OrderData orderData = repository.getAssetRepository().fromOrderId(orderId);
if (orderData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
List<TradeData> trades = repository.getAssetRepository().getOrdersTrades(orderId);
return new OrderWithTrades(orderData, trades);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/issue")
@Operation(
summary = "Issue new asset",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
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"
)
)
)
}
)
@ApiErrors({ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID})
public String issueAsset(IssueAssetTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = IssueAssetTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -0,0 +1,37 @@
package org.qora.api.resource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import io.swagger.v3.oas.annotations.Operation;
@Path("/")
public class BlockExplorerResource {
@Context
HttpServletRequest request;
@GET
@Path("/block-explorer.html")
@Operation(hidden = true)
public String getBlockExplorer() {
ClassLoader loader = this.getClass().getClassLoader();
try (InputStream inputStream = loader.getResourceAsStream("block-explorer.html")) {
if (inputStream == null)
return "block-explorer.html resource not found";
return new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
return "Error reading block-explorer.html resource";
}
}
}

View File

@@ -0,0 +1,468 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
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 java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.BlockWithTransactions;
import org.qora.block.Block;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.utils.Base58;
@Path("/blocks")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Blocks")
public class BlocksResource {
@Context
HttpServletRequest request;
@GET
@Path("/signature/{signature}")
@Operation(
summary = "Fetch block using base58 signature",
description = "Returns the block that matches the given signature",
responses = {
@ApiResponse(
description = "the block",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
)
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getBlock(@PathParam("signature") String signature58, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
// Decode signature
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()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
return packageBlockData(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/first")
@Operation(
summary = "Fetch genesis block",
description = "Returns the genesis block",
responses = {
@ApiResponse(
description = "the block",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
)
)
)
}
)
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getFirstBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(1);
return packageBlockData(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/last")
@Operation(
summary = "Fetch last/newest block in blockchain",
description = "Returns the last valid block",
responses = {
@ApiResponse(
description = "the block",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
)
)
)
}
)
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getLastBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
return packageBlockData(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/child/{signature}")
@Operation(
summary = "Fetch child block using base58 signature of parent block",
description = "Returns the child block of the block that matches the given signature",
responses = {
@ApiResponse(
description = "the block",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
)
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getChild(@PathParam("signature") String signature58, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
// Decode signature
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()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
// Check block exists
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
BlockData childBlockData = repository.getBlockRepository().fromReference(signature);
// Checking child exists is handled by packageBlockData()
return packageBlockData(repository, childBlockData, includeTransactions);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/generatingbalance")
@Operation(
summary = "Generating balance of next block",
description = "Calculates the generating balance of the block that will follow the last block",
responses = {
@ApiResponse(
description = "the generating balance",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
implementation = BigDecimal.class
)
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public BigDecimal getGeneratingBalance() {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
Block block = new Block(repository, blockData);
return block.calcNextBlockGeneratingBalance();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/generatingbalance/{signature}")
@Operation(
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",
responses = {
@ApiResponse(
description = "the block",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
implementation = BigDecimal.class
)
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BigDecimal getGeneratingBalance(@PathParam("signature") String signature58) {
// Decode signature
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()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
// Check block exists
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
Block block = new Block(repository, blockData);
return block.calcNextBlockGeneratingBalance();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/time")
@Operation(
summary = "Estimated time to forge next block",
description = "Calculates the time it should take for the network to generate the next block",
responses = {
@ApiResponse(
description = "the time in seconds",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public long getTimePerBlock() {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
return Block.calcForgingDelay(blockData.getGeneratingBalance());
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/time/{generatingbalance}")
@Operation(
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",
responses = {
@ApiResponse(
description = "the time", // in seconds?
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number"
)
)
)
}
)
public long getTimePerBlock(@PathParam("generatingbalance") BigDecimal generatingbalance) {
return Block.calcForgingDelay(generatingbalance);
}
@GET
@Path("/height")
@Operation(
summary = "Current blockchain height",
description = "Returns the block height of the last block.",
responses = {
@ApiResponse(
description = "the height",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public int getHeight() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockchainHeight();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/height/{signature}")
@Operation(
summary = "Height of specific block",
description = "Returns the block height of the block that matches the given signature",
responses = {
@ApiResponse(
description = "the height",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public int getHeight(@PathParam("signature") String signature58) {
// Decode signature
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()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
// Check block exists
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
return blockData.getHeight();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/byheight/{height}")
@Operation(
summary = "Fetch block using block height",
description = "Returns the block with given height",
responses = {
@ApiResponse(
description = "the block",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
)
)
)
}
)
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
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 packageBlockData(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/range/{height}")
@Operation(
summary = "Fetch blocks starting with given height",
description = "Returns blocks starting with given height.",
responses = {
@ApiResponse(
description = "blocks",
content = @Content(
schema = @Schema(
implementation = BlockWithTransactions.class
)
)
)
}
)
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
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(packageBlockData(repository, blockData, includeTransactions));
}
return blocks;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
/**
* Returns block, optionally including transactions.
* <p>
* Throws ApiException using ApiError.BLOCK_NO_EXISTS if blockData is null.
*
* @param repository
* @param blockData
* @param includeTransactions
* @return packaged block, with optional transactions
* @throws DataException
* @throws ApiException ApiError.BLOCK_NO_EXISTS
*/
private BlockWithTransactions packageBlockData(Repository repository, BlockData blockData, boolean includeTransactions) throws DataException {
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
List<TransactionData> transactions = null;
if (includeTransactions) {
Block block = new Block(repository, blockData);
transactions = block.getTransactions().stream().map(transaction -> transaction.getTransactionData()).collect(Collectors.toList());
}
return new BlockWithTransactions(blockData, transactions);
}
}

View File

@@ -0,0 +1,81 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
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 javax.servlet.http.HttpServletRequest;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiExceptionFactory;
import org.qora.data.transaction.RegisterNameTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
import org.qora.transform.transaction.RegisterNameTransactionTransformer;
import org.qora.utils.Base58;
@Path("/names")
@Produces({ MediaType.TEXT_PLAIN})
@Tag(name = "Names")
public class NamesResource {
@Context
HttpServletRequest request;
@POST
@Path("/register")
@Operation(
summary = "Build raw, unsigned REGISTER_NAME transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = RegisterNameTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned REGISTER_NAME transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String buildTransaction(RegisterNameTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = RegisterNameTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -0,0 +1,81 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
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 javax.servlet.http.HttpServletRequest;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiExceptionFactory;
import org.qora.data.transaction.PaymentTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
import org.qora.transform.transaction.PaymentTransactionTransformer;
import org.qora.utils.Base58;
@Path("/payments")
@Produces({MediaType.TEXT_PLAIN})
@Tag(name = "Payments")
public class PaymentsResource {
@Context
HttpServletRequest request;
@POST
@Path("/pay")
@Operation(
summary = "Build raw, unsigned payment transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = PaymentTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned payment transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String buildTransaction(PaymentTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = PaymentTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -0,0 +1,418 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
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 java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qora.account.PrivateKeyAccount;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.SimpleTransactionSignRequest;
import org.qora.data.transaction.GenesisTransactionData;
import org.qora.data.transaction.PaymentTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.globalization.Translator;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.transform.TransformationException;
import org.qora.transform.transaction.TransactionTransformer;
import org.qora.utils.Base58;
import com.google.common.primitives.Bytes;
@Path("/transactions")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Transactions")
public class TransactionsResource {
@Context
HttpServletRequest request;
@GET
@Path("/signature/{signature}")
@Operation(
summary = "Fetch transaction using transaction signature",
description = "Returns transaction",
responses = {
@ApiResponse(
description = "a transaction",
content = @Content(
schema = @Schema(
implementation = TransactionData.class
)
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public TransactionData getTransactions(@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);
return transactionData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/block/{signature}")
@Operation(
summary = "Fetch transactions using block signature",
description = "Returns list of transactions",
responses = {
@ApiResponse(
description = "list of transactions",
content = @Content(
array = @ArraySchema(
schema = @Schema(
oneOf = {
GenesisTransactionData.class, PaymentTransactionData.class
}
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
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()) {
List<TransactionData> transactions = repository.getBlockRepository().getTransactionsFromSignature(signature);
// check if block exists
if (transactions == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, transactions.size());
int toIndex = limit == 0 ? transactions.size() : Integer.min(fromIndex + limit, transactions.size());
transactions = transactions.subList(fromIndex, toIndex);
return transactions;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/unconfirmed")
@Operation(
summary = "List unconfirmed transactions",
description = "Returns transactions",
responses = {
@ApiResponse(
description = "transactions",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TransactionData.class
)
)
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<TransactionData> getUnconfirmedTransactions() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getTransactionRepository().getAllUnconfirmedTransactions();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, 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
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
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") int limit, @Parameter(
ref = "offset"
) @QueryParam("offset") int offset) {
if ((txTypeNum == null || txTypeNum == 0) && (address == null || address.isEmpty()))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
TransactionType txType = null;
if (txTypeNum != null) {
txType = TransactionType.valueOf(txTypeNum);
if (txType == null)
throw ApiExceptionFactory.INSTANCE.createException(request, 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 ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/sign")
@Operation(
summary = "Sign a raw, unsigned transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = SimpleTransactionSignRequest.class
)
)
),
responses = {
@ApiResponse(
description = "raw, signed transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.TRANSFORMATION_ERROR})
public String signTransaction(SimpleTransactionSignRequest signRequest) {
if (signRequest.transactionBytes.length == 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.JSON);
try {
// 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 (IllegalArgumentException e) {
// Invalid private key
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
}
}
@POST
@Path("/process")
@Operation(
summary = "Submit raw, signed transaction for processing and adding to blockchain",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "raw, signed transaction in base58 encoding",
example = "base58"
)
)
),
responses = {
@ApiResponse(
description = "true if accepted, false otherwise",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String processTransaction(String rawBytes58) {
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58);
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
Transaction transaction = Transaction.fromData(repository, transactionData);
if (!transaction.isSignatureValid())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw createTransactionInvalidException(request, result);
repository.getTransactionRepository().save(transactionData);
repository.getTransactionRepository().unconfirmTransaction(transactionData);
repository.saveChanges();
return "true";
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@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
)
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
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 ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw createTransactionInvalidException(request, result);
if (!hasSignature)
transactionData.setSignature(null);
return transactionData;
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
public static ApiException createTransactionInvalidException(HttpServletRequest request, ValidationResult result) {
String translatedResult = Translator.INSTANCE.translate("TransactionValidity", request.getLocale().getLanguage(), result.name());
return ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID, null, translatedResult);
}
}

View File

@@ -0,0 +1,375 @@
package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
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 java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qora.account.PrivateKeyAccount;
import org.qora.api.ApiError;
import org.qora.api.ApiErrors;
import org.qora.api.ApiExceptionFactory;
import org.qora.crypto.Crypto;
import org.qora.utils.BIP39;
import org.qora.utils.Base58;
import org.qora.utils.NTP;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
@Path("/utils")
@Produces({
MediaType.TEXT_PLAIN
})
@Tag(
name = "Utilities"
)
public class UtilsResource {
@Context
HttpServletRequest request;
@POST
@Path("/fromBase64")
@Operation(
summary = "Convert base64 data to hex",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
),
responses = {
@ApiResponse(
description = "hex string",
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String fromBase64(String base64) {
try {
return HashCode.fromBytes(Base64.getDecoder().decode(base64.trim())).toString();
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
}
@POST
@Path("/fromBase58")
@Operation(
summary = "Convert base58 data to hex",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
),
responses = {
@ApiResponse(
description = "hex string",
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String base64from58(String base58) {
try {
return HashCode.fromBytes(Base58.decode(base58.trim())).toString();
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
}
@GET
@Path("/toBase64/{hex}")
@Operation(
summary = "Convert hex to base64",
responses = {
@ApiResponse(
description = "base64",
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
public String toBase64(@PathParam("hex") String hex) {
return Base64.getEncoder().encodeToString(HashCode.fromString(hex).asBytes());
}
@GET
@Path("/toBase58/{hex}")
@Operation(
summary = "Convert hex to base58",
responses = {
@ApiResponse(
description = "base58",
content = @Content(
schema = @Schema(
type = "string"
)
)
)
}
)
public String toBase58(@PathParam("hex") String hex) {
return Base58.encode(HashCode.fromString(hex).asBytes());
}
@GET
@Path("/random")
@Operation(
summary = "Generate random data",
description = "Optionally pass data length, defaults to 32 bytes.",
responses = {
@ApiResponse(
description = "base58 data",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
public String random(@QueryParam("length") Integer length) {
if (length == null)
length = 32;
byte[] random = new byte[length];
new SecureRandom().nextBytes(random);
return Base58.encode(random);
}
@GET
@Path("/mnemonic")
@Operation(
summary = "Generate 12-word BIP39 mnemonic",
description = "Optionally pass 16-byte, base58-encoded entropy or entropy will be internally generated.<br>"
+ "Example entropy input: YcVfxkQb6JRzqk5kF2tNLv",
responses = {
@ApiResponse(
description = "mnemonic",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String getMnemonic(@QueryParam("entropy") String suppliedEntropy) {
/*
* BIP39 word lists have 2048 entries so can be represented by 11 bits.
* UUID (128bits) and another 4 bits gives 132 bits.
* 132 bits, divided by 11, gives 12 words.
*/
byte[] entropy;
if (suppliedEntropy != null) {
// Use caller-supplied entropy input
try {
entropy = Base58.decode(suppliedEntropy);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
// Must be 16-bytes
if (entropy.length != 16)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} else {
// Generate entropy internally
UUID uuid = UUID.randomUUID();
byte[] uuidMSB = Longs.toByteArray(uuid.getMostSignificantBits());
byte[] uuidLSB = Longs.toByteArray(uuid.getLeastSignificantBits());
entropy = Bytes.concat(uuidMSB, uuidLSB);
}
// Use SHA256 to generate more bits
byte[] hash = Crypto.digest(entropy);
// Append first 4 bits from hash to end. (Actually 8 bits but we only use 4).
byte checksum = (byte) (hash[0] & 0xf0);
entropy = Bytes.concat(entropy, new byte[] {
checksum
});
return BIP39.encode(entropy, "en");
}
@POST
@Path("/mnemonic")
@Operation(
summary = "Calculate binary entropy from 12-word BIP39 mnemonic",
description = "Returns the base58-encoded binary form, or \"false\" if mnemonic is invalid.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
),
responses = {
@ApiResponse(
description = "entropy in base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
public String fromMnemonic(String mnemonic) {
if (mnemonic.isEmpty())
return "false";
// Strip leading/trailing whitespace if any
mnemonic = mnemonic.trim();
String[] phraseWords = mnemonic.split(" ");
if (phraseWords.length != 12)
return "false";
// Convert BIP39 mnemonic to binary
byte[] binary = BIP39.decode(phraseWords, "en");
if (binary == null)
return "false";
byte[] entropy = Arrays.copyOf(binary, 16); // 132 bits is 16.5 bytes, but we're discarding checksum nybble
byte checksumNybble = (byte) (binary[16] & 0xf0);
byte[] checksum = Crypto.digest(entropy);
if (checksumNybble != (byte) (checksum[0] & 0xf0))
return "false";
return Base58.encode(entropy);
}
@GET
@Path("/privateKey/{entropy}")
@Operation(
summary = "Calculate private key from supplied 16-byte entropy",
responses = {
@ApiResponse(
description = "private key in base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String privateKey(@PathParam("entropy") String entropy58) {
byte[] entropy;
try {
entropy = Base58.decode(entropy58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
if (entropy.length != 16)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
byte[] privateKey = Crypto.digest(entropy);
return Base58.encode(privateKey);
}
@GET
@Path("/publicKey/{privateKey}")
@Operation(
summary = "Calculate public key from supplied 32-byte private key",
responses = {
@ApiResponse(
description = "public key in base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String publicKey(@PathParam("privateKey") String privateKey58) {
byte[] privateKey;
try {
privateKey = Base58.decode(privateKey58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
if (privateKey.length != 32)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
byte[] publicKey = new PrivateKeyAccount(null, privateKey).getPublicKey();
return Base58.encode(publicKey);
}
@GET
@Path("/timestamp")
@Operation(
summary = "Returns current timestamp as milliseconds from unix epoch",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number"
)
)
)
}
)
public long getTimestamp() {
return NTP.getTime();
}
}

View File

@@ -0,0 +1,54 @@
package org.qora.asset;
import org.qora.data.asset.AssetData;
import org.qora.data.transaction.IssueAssetTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
public class Asset {
/**
* QORA coins are just another asset but with fixed assetId of zero.
*/
public static final long QORA = 0L;
// Properties
private Repository repository;
private AssetData assetData;
// Constructors
public Asset(Repository repository, AssetData assetData) {
this.repository = repository;
this.assetData = assetData;
}
public Asset(Repository repository, IssueAssetTransactionData issueAssetTransactionData) {
this.repository = repository;
this.assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(),
issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(),
issueAssetTransactionData.getReference());
}
public Asset(Repository repository, long assetId) throws DataException {
this.repository = repository;
this.assetData = this.repository.getAssetRepository().fromAssetId(assetId);
}
// Getters/setters
public AssetData getAssetData() {
return this.assetData;
}
// Processing
public void issue() throws DataException {
this.repository.getAssetRepository().save(this.assetData);
}
public void deissue() throws DataException {
this.repository.getAssetRepository().delete(this.assetData.getAssetId());
}
}

View File

@@ -0,0 +1,239 @@
package org.qora.asset;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.account.Account;
import org.qora.account.PublicKeyAccount;
import org.qora.data.asset.AssetData;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.TradeData;
import org.qora.repository.AssetRepository;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import com.google.common.hash.HashCode;
public class Order {
private static final Logger LOGGER = LogManager.getLogger(Order.class);
// Properties
private Repository repository;
private OrderData orderData;
// Constructors
public Order(Repository repository, OrderData orderData) {
this.repository = repository;
this.orderData = orderData;
}
// Getters/Setters
public OrderData getOrderData() {
return this.orderData;
}
// More information
public static BigDecimal getAmountLeft(OrderData orderData) {
return orderData.getAmount().subtract(orderData.getFulfilled());
}
public BigDecimal getAmountLeft() {
return Order.getAmountLeft(this.orderData);
}
public static boolean isFulfilled(OrderData orderData) {
return orderData.getFulfilled().compareTo(orderData.getAmount()) == 0;
}
public boolean isFulfilled() {
return Order.isFulfilled(this.orderData);
}
public BigDecimal calculateAmountGranularity(AssetData haveAssetData, AssetData wantAssetData, OrderData theirOrderData) {
// 100 million to scale BigDecimal.setScale(8) fractional amounts into integers, essentially 1e8
BigInteger multiplier = BigInteger.valueOf(100_000_000L);
// Calculate the minimum increment at which I can buy using greatest-common-divisor
BigInteger haveAmount = BigInteger.ONE.multiply(multiplier);
BigInteger priceAmount = theirOrderData.getPrice().multiply(new BigDecimal(multiplier)).toBigInteger();
BigInteger gcd = haveAmount.gcd(priceAmount);
haveAmount = haveAmount.divide(gcd);
priceAmount = priceAmount.divide(gcd);
// Calculate GCD in combination with divisibility
if (wantAssetData.getIsDivisible())
haveAmount = haveAmount.multiply(multiplier);
if (haveAssetData.getIsDivisible())
priceAmount = priceAmount.multiply(multiplier);
gcd = haveAmount.gcd(priceAmount);
// Calculate the increment at which we have to buy
BigDecimal increment = new BigDecimal(haveAmount.divide(gcd));
if (wantAssetData.getIsDivisible())
increment = increment.divide(new BigDecimal(multiplier));
// Return
return increment;
}
// Navigation
public List<TradeData> getTrades() throws DataException {
return this.repository.getAssetRepository().getOrdersTrades(this.orderData.getOrderId());
}
// Processing
public void process() throws DataException {
AssetRepository assetRepository = this.repository.getAssetRepository();
long haveAssetId = this.orderData.getHaveAssetId();
AssetData haveAssetData = assetRepository.fromAssetId(haveAssetId);
long wantAssetId = this.orderData.getWantAssetId();
AssetData wantAssetData = assetRepository.fromAssetId(wantAssetId);
// Subtract asset from creator
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(this.orderData.getAmount()));
// Save this order into repository so it's available for matching, possibly by itself
this.repository.getAssetRepository().save(this.orderData);
// Attempt to match orders
LOGGER.debug("Processing our order " + HashCode.fromBytes(this.orderData.getOrderId()).toString());
LOGGER.trace("We have: " + this.orderData.getAmount().toPlainString() + " " + haveAssetData.getName());
LOGGER.trace("We want " + this.orderData.getPrice().toPlainString() + " " + wantAssetData.getName() + " per " + haveAssetData.getName());
// Fetch corresponding open orders that might potentially match, hence reversed want/have assetId args.
// Returned orders are sorted with lowest "price" first.
List<OrderData> orders = assetRepository.getOpenOrders(wantAssetId, haveAssetId);
LOGGER.trace("Open orders fetched from repository: " + orders.size());
/*
* Our order example:
*
* haveAssetId=[GOLD], amount=10,000, wantAssetId=0 (QORA), price=0.002
*
* This translates to "we have 10,000 GOLD and want to buy QORA at a price of 0.002 QORA per GOLD"
*
* So if our order matched, we'd end up with 10,000 * 0.002 = 20 QORA, essentially costing 1/0.002 = 500 GOLD each.
*
* So 500 GOLD [each] is our "buyingPrice".
*/
BigDecimal ourPrice = this.orderData.getPrice();
for (OrderData theirOrderData : orders) {
LOGGER.trace("Considering order " + HashCode.fromBytes(theirOrderData.getOrderId()).toString());
// Note swapped use of have/want asset data as this is from 'their' perspective.
LOGGER.trace("They have: " + theirOrderData.getAmount().toPlainString() + " " + wantAssetData.getName());
LOGGER.trace("They want " + theirOrderData.getPrice().toPlainString() + " " + haveAssetData.getName() + " per " + wantAssetData.getName());
/*
* Potential matching order example:
*
* haveAssetId=0 (QORA), amount=40, wantAssetId=[GOLD], price=486
*
* This translates to "we have 40 QORA and want to buy GOLD at a price of 486 GOLD per QORA"
*
* So if their order matched, they'd end up with 40 * 486 = 19,440 GOLD, essentially costing 1/486 = 0.00205761 QORA each.
*
* So 0.00205761 QORA [each] is their "buyingPrice".
*/
// Round down otherwise their buyingPrice would be better than advertised and cause issues
BigDecimal theirBuyingPrice = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
LOGGER.trace("theirBuyingPrice: " + theirBuyingPrice.toPlainString() + " " + wantAssetData.getName() + " per " + haveAssetData.getName());
// If their buyingPrice is less than what we're willing to pay then we're done as prices only get worse as we iterate through list of orders
if (theirBuyingPrice.compareTo(ourPrice) < 0)
break;
// Calculate how many want-asset we could buy at their price
BigDecimal ourAmountLeft = this.getAmountLeft().multiply(theirBuyingPrice).setScale(8, RoundingMode.DOWN);
LOGGER.trace("ourAmountLeft (max we could buy at their price): " + ourAmountLeft.toPlainString() + " " + wantAssetData.getName());
// How many want-asset is remaining available in this order
BigDecimal theirAmountLeft = Order.getAmountLeft(theirOrderData);
LOGGER.trace("theirAmountLeft (max amount remaining in order): " + theirAmountLeft.toPlainString() + " " + wantAssetData.getName());
// So matchable want-asset amount is the minimum of above two values
BigDecimal matchedAmount = ourAmountLeft.min(theirAmountLeft);
LOGGER.trace("matchedAmount: " + matchedAmount.toPlainString() + " " + wantAssetData.getName());
// If we can't buy anything then try another order
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
continue;
// Calculate amount granularity based on both assets' divisibility
BigDecimal increment = this.calculateAmountGranularity(haveAssetData, wantAssetData, theirOrderData);
LOGGER.trace("increment (want-asset amount granularity): " + increment.toPlainString() + " " + wantAssetData.getName());
matchedAmount = matchedAmount.subtract(matchedAmount.remainder(increment));
LOGGER.trace("matchedAmount adjusted for granularity: " + matchedAmount.toPlainString() + " " + wantAssetData.getName());
// If we can't buy anything then try another order
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
continue;
// Trade can go ahead!
// Calculate the total cost to us, in have-asset, based on their price
BigDecimal tradePrice = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8);
LOGGER.trace("tradePrice ('want' trade agreed): " + tradePrice.toPlainString() + " " + haveAssetData.getName());
// Construct trade
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(), matchedAmount, tradePrice,
this.orderData.getTimestamp());
// Process trade, updating corresponding orders in repository
Trade trade = new Trade(this.repository, tradeData);
trade.process();
// Update our order in terms of fulfilment, etc. but do not save into repository as that's handled by Trade above
this.orderData.setFulfilled(this.orderData.getFulfilled().add(tradePrice));
LOGGER.trace("Updated our order's fulfilled amount to: " + this.orderData.getFulfilled().toPlainString() + " " + haveAssetData.getName());
LOGGER.trace("Our order's amount remaining: " + this.getAmountLeft().toPlainString() + " " + haveAssetData.getName());
// Continue on to process other open orders if we still have amount left to match
if (this.getAmountLeft().compareTo(BigDecimal.ZERO) <= 0)
break;
}
}
public void orphan() throws DataException {
// Orphan trades that occurred as a result of this order
for (TradeData tradeData : getTrades())
if (Arrays.equals(this.orderData.getOrderId(), tradeData.getInitiator())) {
Trade trade = new Trade(this.repository, tradeData);
trade.orphan();
}
// Delete this order from repository
this.repository.getAssetRepository().delete(this.orderData.getOrderId());
// Return asset to creator
long haveAssetId = this.orderData.getHaveAssetId();
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(this.orderData.getAmount()));
}
// This is called by CancelOrderTransaction so that an Order can no longer trade
public void cancel() throws DataException {
this.orderData.setIsClosed(true);
this.repository.getAssetRepository().save(this.orderData);
}
// Opposite of cancel() above for use during orphaning
public void reopen() throws DataException {
this.orderData.setIsClosed(false);
this.repository.getAssetRepository().save(this.orderData);
}
}

View File

@@ -0,0 +1,80 @@
package org.qora.asset;
import org.qora.account.Account;
import org.qora.account.PublicKeyAccount;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.TradeData;
import org.qora.repository.AssetRepository;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
public class Trade {
// Properties
private Repository repository;
private TradeData tradeData;
// Constructors
public Trade(Repository repository, TradeData tradeData) {
this.repository = repository;
this.tradeData = tradeData;
}
// Processing
public void process() throws DataException {
AssetRepository assetRepository = this.repository.getAssetRepository();
// Save trade into repository
assetRepository.save(tradeData);
// Update corresponding Orders on both sides of trade
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(tradeData.getPrice()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
assetRepository.save(initiatingOrder);
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
targetOrder.setFulfilled(targetOrder.getFulfilled().add(tradeData.getAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
assetRepository.save(targetOrder);
// Actually transfer asset balances
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(),
initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).add(tradeData.getAmount()));
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(),
targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).add(tradeData.getPrice()));
}
public void orphan() throws DataException {
AssetRepository assetRepository = this.repository.getAssetRepository();
// Revert corresponding Orders on both sides of trade
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(tradeData.getPrice()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
assetRepository.save(initiatingOrder);
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(tradeData.getAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
assetRepository.save(targetOrder);
// Reverse asset transfers
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(),
initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).subtract(tradeData.getAmount()));
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(),
targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).subtract(tradeData.getPrice()));
// Remove trade from repository
assetRepository.delete(tradeData);
}
}

View File

@@ -0,0 +1,159 @@
package org.qora.at;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.List;
import org.ciyam.at.MachineState;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.data.at.ATData;
import org.qora.data.at.ATStateData;
import org.qora.data.transaction.DeployATTransactionData;
import org.qora.repository.ATRepository;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.transaction.ATTransaction;
public class AT {
// Properties
private Repository repository;
private ATData atData;
private ATStateData atStateData;
// Constructors
public AT(Repository repository, ATData atData, ATStateData atStateData) {
this.repository = repository;
this.atData = atData;
this.atStateData = atStateData;
}
public AT(Repository repository, ATData atData) {
this(repository, atData, null);
}
/** Constructs AT-handling object when deploying AT */
public AT(Repository repository, DeployATTransactionData deployATTransactionData) throws DataException {
this.repository = repository;
String atAddress = deployATTransactionData.getATAddress();
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
byte[] creatorPublicKey = deployATTransactionData.getCreatorPublicKey();
long creation = deployATTransactionData.getTimestamp();
byte[] creationBytes = deployATTransactionData.getCreationBytes();
long assetId = deployATTransactionData.getAssetId();
short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
if (version >= 2) {
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(),
machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(),
machineState.getIsFrozen(), machineState.getFrozenBalance());
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, BigDecimal.ZERO.setScale(8));
} else {
// Legacy v1 AT
// We would deploy these in 'dead' state as they will never be run on Qora2
// but this breaks import from Qora1 so something else will have to mark them dead at hard-fork
// Extract code bytes length
ByteBuffer byteBuffer = ByteBuffer.wrap(deployATTransactionData.getCreationBytes());
// v1 AT header is: version, reserved, code-pages, data-pages, call-stack-pages, user-stack-pages (all shorts)
// Number of code pages
short numCodePages = byteBuffer.get(2 + 2);
// Skip header and also "minimum activation amount" (long)
byteBuffer.position(6 * 2 + 8);
int codeLen = 0;
// Extract actual code length, stored in minimal-size form (byte, short or int)
if (numCodePages * 256 < 257) {
codeLen = (int) (byteBuffer.get() & 0xff);
} else if (numCodePages * 256 < Short.MAX_VALUE + 1) {
codeLen = byteBuffer.getShort() & 0xffff;
} else if (numCodePages * 256 <= Integer.MAX_VALUE) {
codeLen = byteBuffer.getInt();
}
// Extract code bytes
byte[] codeBytes = new byte[codeLen];
byteBuffer.get(codeBytes);
// Create AT
boolean isSleeping = false;
Integer sleepUntilHeight = null;
boolean isFinished = false;
boolean hadFatalError = false;
boolean isFrozen = false;
Long frozenBalance = null;
this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORA, codeBytes, isSleeping, sleepUntilHeight, isFinished,
hadFatalError, isFrozen, frozenBalance);
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8));
}
}
// Getters / setters
public ATStateData getATStateData() {
return this.atStateData;
}
// Processing
public void deploy() throws DataException {
ATRepository atRepository = this.repository.getATRepository();
atRepository.save(this.atData);
// For version 2+ we also store initial AT state data
if (this.atData.getVersion() >= 2)
atRepository.save(this.atStateData);
}
public void undeploy() throws DataException {
// AT states deleted implicitly by repository
this.repository.getATRepository().delete(this.atData.getATAddress());
}
public List<ATTransaction> run(long blockTimestamp) throws DataException {
String atAddress = this.atData.getATAddress();
QoraATAPI api = new QoraATAPI(repository, this.atData, blockTimestamp);
QoraATLogger logger = new QoraATLogger();
byte[] codeBytes = this.atData.getCodeBytes();
// Fetch latest ATStateData for this AT (if any)
ATStateData atStateData = this.repository.getATRepository().getLatestATState(atAddress);
// There should be at least initial AT state data
if (atStateData == null)
throw new IllegalStateException("No initial AT state data found");
// [Re]create AT machine state using AT state data or from scratch as applicable
MachineState state = MachineState.fromBytes(api, logger, atStateData.getStateData(), codeBytes);
state.execute();
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
long creation = this.atData.getCreation();
byte[] stateData = state.toBytes();
byte[] stateHash = Crypto.digest(stateData);
BigDecimal atFees = api.calcFinalFees(state);
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, atFees);
return api.getTransactions();
}
}

View File

@@ -0,0 +1,133 @@
package org.qora.at;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
import org.qora.account.Account;
import org.qora.block.Block;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.ATTransactionData;
import org.qora.data.transaction.PaymentTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.BlockRepository;
import org.qora.repository.DataException;
import org.qora.transaction.Transaction;
public enum BlockchainAPI {
QORA(0) {
@Override
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1;
QoraATAPI api = (QoraATAPI) state.getAPI();
Account recipientAccount = new Account(api.repository, recipient);
BlockRepository blockRepository = api.repository.getBlockRepository();
try {
while (height <= blockRepository.getBlockchainHeight()) {
BlockData blockData = blockRepository.fromHeight(height);
if (blockData == null)
throw new DataException("Unable to fetch block " + height + " from repository?");
Block block = new Block(api.repository, blockData);
List<Transaction> transactions = block.getTransactions();
// No more transactions in this block? Try next block
if (sequence >= transactions.size()) {
++height;
sequence = 0;
continue;
}
Transaction transaction = transactions.get(sequence);
// Transaction needs to be sent to specified recipient
if (transaction.getRecipientAccounts().contains(recipientAccount)) {
// Found a transaction
api.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue());
// Hash transaction's signature into other three A fields for future verification that it's the same transaction
byte[] hash = QoraATAPI.sha192(transaction.getTransactionData().getSignature());
api.setA2(state, QoraATAPI.fromBytes(hash, 0));
api.setA3(state, QoraATAPI.fromBytes(hash, 8));
api.setA4(state, QoraATAPI.fromBytes(hash, 16));
return;
}
// Transaction wasn't for us - keep going
++sequence;
}
// No more transactions - zero A and exit
api.zeroA(state);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch next transaction?", e);
}
}
@Override
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
QoraATAPI api = (QoraATAPI) state.getAPI();
TransactionData transactionData = api.fetchTransaction(state);
switch (transactionData.getType()) {
case PAYMENT:
return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue();
case AT:
BigDecimal amount = ((ATTransactionData) transactionData).getAmount();
if (amount != null)
return amount.unscaledValue().longValue();
else
return 0xffffffffffffffffL;
default:
return 0xffffffffffffffffL;
}
}
},
BTC(1) {
@Override
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
// TODO
}
@Override
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
// TODO
return 0;
}
};
public final int value;
private final static Map<Integer, BlockchainAPI> map = stream(BlockchainAPI.values()).collect(toMap(type -> type.value, type -> type));
BlockchainAPI(int value) {
this.value = value;
}
public static BlockchainAPI valueOf(int value) {
return map.get(value);
}
// Blockchain-specific API methods
public abstract void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state);
public abstract long getAmountFromTransactionInA(Timestamp timestamp, MachineState state);
}

View File

@@ -0,0 +1,435 @@
package org.qora.at;
import java.math.BigDecimal;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import org.ciyam.at.API;
import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
import org.qora.account.Account;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.data.at.ATData;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.ATTransactionData;
import org.qora.data.transaction.MessageTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.transaction.ATTransaction;
import com.google.common.primitives.Bytes;
public class QoraATAPI extends API {
// Useful constants
private static final BigDecimal FEE_PER_STEP = BigDecimal.valueOf(1.0).setScale(8); // 1 Qora per "step"
private static final int MAX_STEPS_PER_ROUND = 500;
private static final int STEPS_PER_FUNCTION_CALL = 10;
private static final int MINUTES_PER_BLOCK = 10;
// Properties
Repository repository;
ATData atData;
long blockTimestamp;
/** List of generated AT transactions */
List<ATTransaction> transactions;
// Constructors
public QoraATAPI(Repository repository, ATData atData, long blockTimestamp) {
this.repository = repository;
this.atData = atData;
this.transactions = new ArrayList<ATTransaction>();
this.blockTimestamp = blockTimestamp;
}
// Methods specific to Qora AT processing, not inherited
public List<ATTransaction> getTransactions() {
return this.transactions;
}
public BigDecimal calcFinalFees(MachineState state) {
return FEE_PER_STEP.multiply(BigDecimal.valueOf(state.getSteps()));
}
// Inherited methods from CIYAM AT API
@Override
public int getMaxStepsPerRound() {
return MAX_STEPS_PER_ROUND;
}
@Override
public int getOpCodeSteps(OpCode opcode) {
if (opcode.value >= OpCode.EXT_FUN.value && opcode.value <= OpCode.EXT_FUN_RET_DAT_2.value)
return STEPS_PER_FUNCTION_CALL;
return 1;
}
@Override
public long getFeePerStep() {
return FEE_PER_STEP.unscaledValue().longValue();
}
@Override
public int getCurrentBlockHeight() {
try {
return this.repository.getBlockRepository().getBlockchainHeight();
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch current blockchain height?", e);
}
}
@Override
public int getATCreationBlockHeight(MachineState state) {
try {
return this.repository.getATRepository().getATCreationBlockHeight(this.atData.getATAddress());
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch AT's creation block height?", e);
}
}
@Override
public void putPreviousBlockHashInA(MachineState state) {
try {
BlockData blockData = this.repository.getBlockRepository().fromHeight(this.getPreviousBlockHeight());
// Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes)
byte[] blockHash = Crypto.digest(blockData.getSignature());
this.setA(state, blockHash);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch previous block?", e);
}
}
@Override
public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) {
// Recipient is this AT
String recipient = this.atData.getATAddress();
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
}
@Override
public long getTypeFromTransactionInA(MachineState state) {
TransactionData transactionData = this.fetchTransaction(state);
switch (transactionData.getType()) {
case PAYMENT:
return ATTransactionType.PAYMENT.value;
case MESSAGE:
return ATTransactionType.MESSAGE.value;
case AT:
if (((ATTransactionData) transactionData).getAmount() != null)
return ATTransactionType.PAYMENT.value;
else
return ATTransactionType.MESSAGE.value;
default:
return 0xffffffffffffffffL;
}
}
@Override
public long getAmountFromTransactionInA(MachineState state) {
Timestamp timestamp = new Timestamp(state.getA1());
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
return blockchainAPI.getAmountFromTransactionInA(timestamp, state);
}
@Override
public long getTimestampFromTransactionInA(MachineState state) {
// Transaction's "timestamp" already stored in A1
Timestamp timestamp = new Timestamp(state.getA1());
return timestamp.longValue();
}
@Override
public long generateRandomUsingTransactionInA(MachineState state) {
// The plan here is to sleep for a block then use next block's signature and this transaction's signature to generate pseudo-random, but deterministic,
// value.
if (!isFirstOpCodeAfterSleeping(state)) {
// First call
// Sleep for a block
this.setIsSleeping(state, true);
return 0L; // not used
} else {
// Second call
// HASH(A and new block hash)
TransactionData transactionData = this.fetchTransaction(state);
try {
BlockData blockData = this.repository.getBlockRepository().getLastBlock();
if (blockData == null)
throw new RuntimeException("AT API unable to fetch latest block?");
byte[] input = Bytes.concat(transactionData.getSignature(), blockData.getSignature());
byte[] hash = Crypto.digest(input);
return fromBytes(hash, 0);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch latest block from repository?", e);
}
}
}
@Override
public void putMessageFromTransactionInAIntoB(MachineState state) {
// Zero B in case of issues or shorter-than-B message
this.zeroB(state);
TransactionData transactionData = this.fetchTransaction(state);
byte[] messageData = null;
switch (transactionData.getType()) {
case MESSAGE:
messageData = ((MessageTransactionData) transactionData).getData();
break;
case AT:
messageData = ((ATTransactionData) transactionData).getMessage();
break;
default:
return;
}
// Check data length is appropriate, i.e. not larger than B
if (messageData.length > 4 * 8)
return;
// Pad messageData to fit B
byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
// Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally
this.setB(state, paddedMessageData);
}
@Override
public void putAddressFromTransactionInAIntoB(MachineState state) {
TransactionData transactionData = this.fetchTransaction(state);
// We actually use public key as it has more potential utility (e.g. message verification) than an address
byte[] bytes = transactionData.getCreatorPublicKey();
this.setB(state, bytes);
}
@Override
public void putCreatorAddressIntoB(MachineState state) {
// We actually use public key as it has more potential utility (e.g. message verification) than an address
byte[] bytes = atData.getCreatorPublicKey();
this.setB(state, bytes);
}
@Override
public long getCurrentBalance(MachineState state) {
Account atAccount = this.getATAccount();
try {
return atAccount.getConfirmedBalance(Asset.QORA).unscaledValue().longValue();
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch AT's current balance?", e);
}
}
@Override
public void payAmountToB(long unscaledAmount, MachineState state) {
byte[] publicKey = state.getB();
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
BigDecimal amount = BigDecimal.valueOf(unscaledAmount, 8);
ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), recipient.getAddress(), amount, this.atData.getAssetId(),
new byte[0], BigDecimal.ZERO.setScale(8), timestamp, reference);
ATTransaction atTransaction = new ATTransaction(this.repository, atTransactionData);
// Add to our transactions
this.transactions.add(atTransaction);
}
@Override
public void messageAToB(MachineState state) {
byte[] message = state.getA();
byte[] publicKey = state.getB();
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), recipient.getAddress(), BigDecimal.ZERO,
this.atData.getAssetId(), message, BigDecimal.ZERO.setScale(8), timestamp, reference);
ATTransaction atTransaction = new ATTransaction(this.repository, atTransactionData);
// Add to our transactions
this.transactions.add(atTransaction);
}
@Override
public long addMinutesToTimestamp(Timestamp timestamp, long minutes, MachineState state) {
int blockHeight = timestamp.blockHeight;
// At least one block in the future
blockHeight += (minutes / MINUTES_PER_BLOCK) + 1;
return new Timestamp(blockHeight, 0).longValue();
}
@Override
public void onFinished(long finalBalance, MachineState state) {
// Refund remaining balance (if any) to AT's creator
Account creator = this.getCreator();
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
BigDecimal amount = BigDecimal.valueOf(finalBalance, 8);
ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), creator.getAddress(), amount, this.atData.getAssetId(),
new byte[0], BigDecimal.ZERO.setScale(8), timestamp, reference);
ATTransaction atTransaction = new ATTransaction(this.repository, atTransactionData);
// Add to our transactions
this.transactions.add(atTransaction);
}
@Override
public void onFatalError(MachineState state, ExecutionException e) {
state.getLogger().error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage());
}
@Override
public void platformSpecificPreExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode)
throws IllegalFunctionCodeException {
QoraFunctionCode qoraFunctionCode = QoraFunctionCode.valueOf(rawFunctionCode);
if (qoraFunctionCode == null)
throw new IllegalFunctionCodeException("Unknown Qora function code 0x" + String.format("%04x", rawFunctionCode) + " encountered");
qoraFunctionCode.preExecuteCheck(2, true, state, rawFunctionCode);
}
@Override
public void platformSpecificPostCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
QoraFunctionCode qoraFunctionCode = QoraFunctionCode.valueOf(rawFunctionCode);
qoraFunctionCode.execute(functionData, state, rawFunctionCode);
}
// Utility methods
/** Convert part of little-endian byte[] to long */
/* package */ static long fromBytes(byte[] bytes, int start) {
return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
}
/** Returns SHA2-192 digest of input - used to verify transaction signatures */
public static byte[] sha192(byte[] input) {
try {
// SHA2-192
MessageDigest sha192 = MessageDigest.getInstance("SHA-192");
return sha192.digest(input);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-192 not available");
}
}
/** Verify transaction's SHA2-192 hashed signature matches A2 thru A4 */
private static void verifyTransaction(TransactionData transactionData, MachineState state) {
// Compare SHA2-192 of transaction's signature against A2 thru A4
byte[] hash = sha192(transactionData.getSignature());
if (state.getA2() != fromBytes(hash, 0) || state.getA3() != fromBytes(hash, 8) || state.getA4() != fromBytes(hash, 16))
throw new IllegalStateException("Transaction signature in A no longer matches signature from repository");
}
/** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */
/* package */ TransactionData fetchTransaction(MachineState state) {
Timestamp timestamp = new Timestamp(state.getA1());
try {
TransactionData transactionData = this.repository.getTransactionRepository().fromHeightAndSequence(timestamp.blockHeight,
timestamp.transactionSequence);
if (transactionData == null)
throw new RuntimeException("AT API unable to fetch transaction?");
// Check transaction referenced still matches the one from the repository
verifyTransaction(transactionData, state);
return transactionData;
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch transaction type?", e);
}
}
/** Returns AT's account */
/* package */ Account getATAccount() {
return new Account(this.repository, this.atData.getATAddress());
}
/** Returns AT's creator's account */
private PublicKeyAccount getCreator() {
return new PublicKeyAccount(this.repository, this.atData.getCreatorPublicKey());
}
/** Returns the timestamp to use for next AT Transaction */
private long getNextTransactionTimestamp() {
/*
* Timestamp is block's timestamp + position in AT-Transactions list.
*
* We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed.
*
* As Qora blocks must share the same milliseconds component in their timestamps, this allows us to generate up to 1,000 AT-Transactions per AT without
* issue.
*
* As long as ATs are not allowed to generate that many per block, e.g. by limiting maximum steps per execution round, then we should be fine.
*/
return this.blockTimestamp + this.transactions.size();
}
/** Returns AT account's lastReference, taking newly generated ATTransactions into account */
private byte[] getLastReference() {
// Use signature from last AT Transaction we generated
if (!this.transactions.isEmpty())
return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature();
// No transactions yet, so look up AT's account's last reference from repository
Account atAccount = this.getATAccount();
try {
return atAccount.getLastReference();
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch AT's last reference from repository?", e);
}
}
}

View File

@@ -0,0 +1,27 @@
package org.qora.at;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.at.AT;
public class QoraATLogger implements org.ciyam.at.LoggerInterface {
// NOTE: We're logging on behalf of qora.at.AT, not ourselves!
private static final Logger LOGGER = LogManager.getLogger(AT.class);
@Override
public void error(String message) {
LOGGER.error(message);
}
@Override
public void debug(String message) {
LOGGER.debug(message);
}
@Override
public void echo(String message) {
LOGGER.info(message);
}
}

View File

@@ -0,0 +1,107 @@
package org.qora.at;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
/**
* Qora-specific CIYAM-AT Functions.
* <p>
* Function codes need to be between 0x0500 and 0x06ff.
*
*/
public enum QoraFunctionCode {
/**
* <tt>0x0500</tt><br>
* Returns current BTC block's "timestamp"
*/
GET_BTC_BLOCK_TIMESTAMP(0x0500, 0, true) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
functionData.returnValue = Timestamp.toLong(state.getAPI().getCurrentBlockHeight(), BlockchainAPI.BTC.value, 0);
}
},
/**
* <tt>0x0501</tt><br>
* Put transaction from specific recipient after timestamp in A, or zero if none<br>
*/
PUT_TX_FROM_B_RECIPIENT_AFTER_TIMESTAMP_IN_A(0x0501, 1, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
Timestamp timestamp = new Timestamp(functionData.value2);
try {
String recipient = new String(state.getB(), "UTF-8");
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
} catch (UnsupportedEncodingException e) {
throw new ExecutionException("Couldn't parse recipient from B", e);
}
}
};
public final short value;
public final int paramCount;
public final boolean returnsValue;
private final static Map<Short, QoraFunctionCode> map = Arrays.stream(QoraFunctionCode.values())
.collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode));
private QoraFunctionCode(int value, int paramCount, boolean returnsValue) {
this.value = (short) value;
this.paramCount = paramCount;
this.returnsValue = returnsValue;
}
public static QoraFunctionCode valueOf(int value) {
return map.get((short) value);
}
public void preExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) throws IllegalFunctionCodeException {
if (paramCount != this.paramCount)
throw new IllegalFunctionCodeException(
"Passed paramCount (" + paramCount + ") does not match function's required paramCount (" + this.paramCount + ")");
if (returnValueExpected != this.returnsValue)
throw new IllegalFunctionCodeException(
"Passed returnValueExpected (" + returnValueExpected + ") does not match function's return signature (" + this.returnsValue + ")");
}
/**
* Execute Function
* <p>
* Can modify various fields of <tt>state</tt>, including <tt>programCounter</tt>.
* <p>
* Throws a subclass of <tt>ExecutionException</tt> on error, e.g. <tt>InvalidAddressException</tt>.
*
* @param functionData
* @param state
* @throws ExecutionException
*/
public void execute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
// Check passed functionData against requirements of this function
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, state, rawFunctionCode);
if (functionData.paramCount >= 1 && functionData.value1 == null)
throw new IllegalFunctionCodeException("Passed value1 is null but function has paramCount of (" + this.paramCount + ")");
if (functionData.paramCount == 2 && functionData.value2 == null)
throw new IllegalFunctionCodeException("Passed value2 is null but function has paramCount of (" + this.paramCount + ")");
state.getLogger().debug("Function \"" + this.name() + "\"");
postCheckExecute(functionData, state, rawFunctionCode);
}
/** Actually execute function */
abstract protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException;
}

View File

@@ -0,0 +1,992 @@
package org.qora.block;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.account.Account;
import org.qora.account.PrivateKeyAccount;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.at.AT;
import org.qora.crypto.Crypto;
import org.qora.data.at.ATData;
import org.qora.data.at.ATStateData;
import org.qora.data.block.BlockData;
import org.qora.data.block.BlockTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.ATRepository;
import org.qora.repository.BlockRepository;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.transaction.ATTransaction;
import org.qora.transaction.GenesisTransaction;
import org.qora.transaction.Transaction;
import org.qora.transform.TransformationException;
import org.qora.transform.block.BlockTransformer;
import org.qora.transform.transaction.TransactionTransformer;
import org.qora.utils.Base58;
import org.qora.utils.NTP;
import com.google.common.primitives.Bytes;
/*
* Typical use-case scenarios:
*
* 1. Loading a Block from the database using height, signature, reference, etc.
* 2. Generating a new block, adding unconfirmed transactions
* 3. Receiving a block from another node
*
* Transaction count, transactions signature and total fees need to be maintained by Block.
* In scenario (1) these can be found in database.
* In scenarios (2) and (3) Transactions are added to the Block via addTransaction() method.
* Also in scenarios (2) and (3), Block is responsible for saving Transactions to DB.
*
* When is height set?
* In scenario (1) this can be found in database.
* In scenarios (2) and (3) this will need to be set after successful processing,
* but before Block is saved into database.
*
* GeneratorSignature's data is: reference + generatingBalance + generator's public key
* TransactionSignature's data is: generatorSignature + transaction signatures
* Block signature is: generatorSignature + transactionsSignature
*/
public class Block {
// Validation results
public enum ValidationResult {
OK(1),
REFERENCE_MISSING(10),
PARENT_DOES_NOT_EXIST(11),
BLOCKCHAIN_NOT_EMPTY(12),
PARENT_HAS_EXISTING_CHILD(13),
TIMESTAMP_OLDER_THAN_PARENT(20),
TIMESTAMP_IN_FUTURE(21),
TIMESTAMP_MS_INCORRECT(22),
TIMESTAMP_TOO_SOON(23),
VERSION_INCORRECT(30),
FEATURE_NOT_YET_RELEASED(31),
GENERATING_BALANCE_INCORRECT(40),
GENERATOR_NOT_ACCEPTED(41),
GENESIS_TRANSACTIONS_INVALID(50),
TRANSACTION_TIMESTAMP_INVALID(51),
TRANSACTION_INVALID(52),
TRANSACTION_PROCESSING_FAILED(53),
AT_STATES_MISMATCH(61);
public final int value;
private final static Map<Integer, ValidationResult> map = stream(ValidationResult.values()).collect(toMap(result -> result.value, result -> result));
ValidationResult(int value) {
this.value = value;
}
public static ValidationResult valueOf(int value) {
return map.get(value);
}
}
// Properties
protected Repository repository;
protected BlockData blockData;
protected PublicKeyAccount generator;
// Other properties
private static final Logger LOGGER = LogManager.getLogger(Block.class);
/** Sorted list of transactions attached to this block */
protected List<Transaction> transactions;
/** Remote/imported/loaded AT states */
protected List<ATStateData> atStates;
/** Locally-generated AT states */
protected List<ATStateData> ourAtStates;
/** Locally-generated AT fees */
protected BigDecimal ourAtFees; // Generated locally
/** Cached copy of next block's generating balance */
protected BigDecimal cachedNextGeneratingBalance;
/** Minimum Qora balance for use in calculations. */
public static final BigDecimal MIN_BALANCE = BigDecimal.valueOf(1L).setScale(8);
// Other useful constants
/** Maximum size of block in bytes */
public static final int MAX_BLOCK_BYTES = 1048576;
// Constructors
/**
* Constructs Block-handling object without loading transactions and AT states.
* <p>
* Transactions and AT states are loaded on first call to getTransactions() or getATStates() respectively.
*
* @param repository
* @param blockData
* @throws DataException
*/
public Block(Repository repository, BlockData blockData) throws DataException {
this.repository = repository;
this.blockData = blockData;
this.generator = new PublicKeyAccount(repository, blockData.getGeneratorPublicKey());
}
/**
* Constructs Block-handling object using passed transaction and AT states.
* <p>
* This constructor typically used when receiving a serialized block over the network.
*
* @param repository
* @param blockData
* @param transactions
* @param atStates
* @throws DataException
*/
public Block(Repository repository, BlockData blockData, List<TransactionData> transactions, List<ATStateData> atStates) throws DataException {
this(repository, blockData);
this.transactions = new ArrayList<Transaction>();
BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
// We have to sum fees too
for (TransactionData transactionData : transactions) {
this.transactions.add(Transaction.fromData(repository, transactionData));
totalFees = totalFees.add(transactionData.getFee());
}
this.atStates = atStates;
for (ATStateData atState : atStates)
totalFees = totalFees.add(atState.getFees());
this.blockData.setTotalFees(totalFees);
}
/**
* Constructs Block-handling object with basic, initial values.
* <p>
* This constructor typically used when generating a new block.
* <p>
* Note that CIYAM ATs will be executed and AT-Transactions prepended to this block, along with AT state data and fees.
*
* @param repository
* @param version
* @param reference
* @param timestamp
* @param generatingBalance
* @param generator
* @throws DataException
*/
public Block(Repository repository, BlockData parentBlockData, PrivateKeyAccount generator) throws DataException {
this.repository = repository;
this.generator = generator;
Block parentBlock = new Block(repository, parentBlockData);
int version = parentBlock.getNextBlockVersion();
byte[] reference = parentBlockData.getSignature();
BigDecimal generatingBalance = parentBlock.calcNextBlockGeneratingBalance();
byte[] generatorSignature;
try {
generatorSignature = generator
.sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator));
} catch (TransformationException e) {
throw new DataException("Unable to calculate next block generator signature", e);
}
long timestamp = parentBlock.calcNextBlockTimestamp(version, generatorSignature, generator);
long maximumTimestamp = parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMaxBlockTime();
if (timestamp > maximumTimestamp)
timestamp = maximumTimestamp;
int transactionCount = 0;
byte[] transactionsSignature = null;
int height = parentBlockData.getHeight() + 1;
this.transactions = new ArrayList<Transaction>();
int atCount = 0;
BigDecimal atFees = BigDecimal.ZERO.setScale(8);
BigDecimal totalFees = atFees;
// This instance used for AT processing
this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generator.getPublicKey(), generatorSignature, atCount, atFees);
// Requires this.blockData and this.transactions, sets this.ourAtStates and this.ourAtFees
this.executeATs();
atCount = this.ourAtStates.size();
this.atStates = this.ourAtStates;
atFees = this.ourAtFees;
totalFees = atFees;
// Rebuild blockData using post-AT-execute data
this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generator.getPublicKey(), generatorSignature, atCount, atFees);
}
// Getters/setters
public BlockData getBlockData() {
return this.blockData;
}
public PublicKeyAccount getGenerator() {
return this.generator;
}
// More information
/**
* Return composite block signature (generatorSignature + transactionsSignature).
*
* @return byte[], or null if either component signature is null.
*/
public byte[] getSignature() {
if (this.blockData.getGeneratorSignature() == null || this.blockData.getTransactionsSignature() == null)
return null;
return Bytes.concat(this.blockData.getGeneratorSignature(), this.blockData.getTransactionsSignature());
}
/**
* Return the next block's version.
*
* @return 1, 2, 3 or 4
*/
public int getNextBlockVersion() {
if (this.blockData.getHeight() == null)
throw new IllegalStateException("Can't determine next block's version as this block has no height set");
if (this.blockData.getHeight() < BlockChain.getInstance().getATReleaseHeight())
return 1;
else if (this.blockData.getTimestamp() < BlockChain.getInstance().getPowFixReleaseTimestamp())
return 2;
else if (this.blockData.getTimestamp() < BlockChain.getInstance().getQoraV2Timestamp())
return 3;
else
return 4;
}
/**
* Return the next block's generating balance.
* <p>
* Every BLOCK_RETARGET_INTERVAL the generating balance is recalculated.
* <p>
* If this block starts a new interval then the new generating balance is calculated, cached and returned.<br>
* Within this interval, the generating balance stays the same so the current block's generating balance will be returned.
*
* @return next block's generating balance
* @throws DataException
*/
public BigDecimal calcNextBlockGeneratingBalance() throws DataException {
if (this.blockData.getHeight() == null)
throw new IllegalStateException("Can't calculate next block's generating balance as this block's height is unset");
// This block not at the start of an interval?
if (this.blockData.getHeight() % BlockChain.getInstance().getBlockDifficultyInterval() != 0)
return this.blockData.getGeneratingBalance();
// Return cached calculation if we have one
if (this.cachedNextGeneratingBalance != null)
return this.cachedNextGeneratingBalance;
// Perform calculation
// Navigate back to first block in previous interval:
// XXX: why can't we simply load using block height?
BlockRepository blockRepo = this.repository.getBlockRepository();
BlockData firstBlock = this.blockData;
try {
for (int i = 1; firstBlock != null && i < BlockChain.getInstance().getBlockDifficultyInterval(); ++i)
firstBlock = blockRepo.fromSignature(firstBlock.getReference());
} catch (DataException e) {
firstBlock = null;
}
// Couldn't navigate back far enough?
if (firstBlock == null)
throw new IllegalStateException("Failed to calculate next block's generating balance due to lack of historic blocks");
// Calculate the actual time period (in ms) over previous interval's blocks.
long previousGeneratingTime = this.blockData.getTimestamp() - firstBlock.getTimestamp();
// Calculate expected forging time (in ms) for a whole interval based on this block's generating balance.
long expectedGeneratingTime = Block.calcForgingDelay(this.blockData.getGeneratingBalance()) * BlockChain.getInstance().getBlockDifficultyInterval()
* 1000;
// Finally, scale generating balance such that faster than expected previous intervals produce larger generating balances.
// NOTE: we have to use doubles and longs here to keep compatibility with Qora v1 results
double multiplier = (double) expectedGeneratingTime / (double) previousGeneratingTime;
long nextGeneratingBalance = (long) (this.blockData.getGeneratingBalance().doubleValue() * multiplier);
this.cachedNextGeneratingBalance = Block.minMaxBalance(BigDecimal.valueOf(nextGeneratingBalance).setScale(8));
return this.cachedNextGeneratingBalance;
}
public static long calcBaseTarget(BigDecimal generatingBalance) {
generatingBalance = Block.minMaxBalance(generatingBalance);
return generatingBalance.longValue() * calcForgingDelay(generatingBalance);
}
/**
* Return expected forging delay, in seconds, since previous block based on passed generating balance.
*/
public static long calcForgingDelay(BigDecimal generatingBalance) {
generatingBalance = Block.minMaxBalance(generatingBalance);
double percentageOfTotal = generatingBalance.divide(BlockChain.getInstance().getMaxBalance()).doubleValue();
long actualBlockTime = (long) (BlockChain.getInstance().getMinBlockTime()
+ ((BlockChain.getInstance().getMaxBlockTime() - BlockChain.getInstance().getMinBlockTime()) * (1 - percentageOfTotal)));
return actualBlockTime;
}
private BigInteger calcGeneratorsTarget(Account nextBlockGenerator) throws DataException {
// Start with 32-byte maximum integer representing all possible correct "guesses"
// Where a "correct guess" is an integer greater than the threshold represented by calcBlockHash()
byte[] targetBytes = new byte[32];
Arrays.fill(targetBytes, Byte.MAX_VALUE);
BigInteger target = new BigInteger(1, targetBytes);
// Divide by next block's base target
// So if next block requires a higher generating balance then there are fewer remaining "correct guesses"
BigInteger baseTarget = BigInteger.valueOf(calcBaseTarget(calcNextBlockGeneratingBalance()));
target = target.divide(baseTarget);
// Multiply by account's generating balance
// So the greater the account's generating balance then the greater the remaining "correct guesses"
target = target.multiply(nextBlockGenerator.getGeneratingBalance().toBigInteger());
return target;
}
/** Returns pseudo-random, but deterministic, integer for this block (and block's generator for v3+ blocks) */
private BigInteger calcBlockHash() {
byte[] hashData;
if (this.blockData.getVersion() < 3)
hashData = this.blockData.getGeneratorSignature();
else
hashData = Bytes.concat(this.blockData.getReference(), generator.getPublicKey());
// Calculate 32-byte hash as pseudo-random, but deterministic, integer (unique to this generator for v3+ blocks)
byte[] hash = Crypto.digest(hashData);
// Convert hash to BigInteger form
return new BigInteger(1, hash);
}
/** Returns pseudo-random, but deterministic, integer for next block (and next block's generator for v3+ blocks) */
private BigInteger calcNextBlockHash(int nextBlockVersion, byte[] preVersion3GeneratorSignature, PublicKeyAccount nextBlockGenerator) {
byte[] hashData;
if (nextBlockVersion < 3)
hashData = preVersion3GeneratorSignature;
else
hashData = Bytes.concat(this.blockData.getSignature(), nextBlockGenerator.getPublicKey());
// Calculate 32-byte hash as pseudo-random, but deterministic, integer (unique to this generator for v3+ blocks)
byte[] hash = Crypto.digest(hashData);
// Convert hash to BigInteger form
return new BigInteger(1, hash);
}
/** Calculate next block's timestamp, given next block's version, generator signature and generator's private key */
private long calcNextBlockTimestamp(int nextBlockVersion, byte[] nextBlockGeneratorSignature, PrivateKeyAccount nextBlockGenerator) throws DataException {
BigInteger hashValue = calcNextBlockHash(nextBlockVersion, nextBlockGeneratorSignature, nextBlockGenerator);
BigInteger target = calcGeneratorsTarget(nextBlockGenerator);
// If target is zero then generator has no balance so return longest value
if (target.compareTo(BigInteger.ZERO) == 0)
return Long.MAX_VALUE;
// Use ratio of "correct guesses" to calculate minimum delay until this generator can forge a block
BigInteger seconds = hashValue.divide(target).add(BigInteger.ONE);
// Calculate next block timestamp using delay
BigInteger timestamp = seconds.multiply(BigInteger.valueOf(1000)).add(BigInteger.valueOf(this.blockData.getTimestamp()));
// Limit timestamp to maximum long value
timestamp = timestamp.min(BigInteger.valueOf(Long.MAX_VALUE));
return timestamp.longValue();
}
/**
* Return block's transactions.
* <p>
* If the block was loaded from repository then it's possible this method will call the repository to fetch the transactions if not done already.
*
* @return
* @throws DataException
*/
public List<Transaction> getTransactions() throws DataException {
// Already loaded?
if (this.transactions != null)
return this.transactions;
// Allocate cache for results
List<TransactionData> transactionsData = this.repository.getBlockRepository().getTransactionsFromSignature(this.blockData.getSignature());
// The number of transactions fetched from repository should correspond with Block's transactionCount
if (transactionsData.size() != this.blockData.getTransactionCount())
throw new IllegalStateException("Block's transactions from repository do not match block's transaction count");
this.transactions = new ArrayList<Transaction>();
for (TransactionData transactionData : transactionsData)
this.transactions.add(Transaction.fromData(this.repository, transactionData));
return this.transactions;
}
/**
* Return block's AT states.
* <p>
* If the block was loaded from repository then it's possible this method will call the repository to fetch the AT states if not done already.
* <p>
* <b>Note:</b> AT states fetched from repository only contain summary info, not actual data like serialized state data or AT creation timestamps!
*
* @return
* @throws DataException
*/
public List<ATStateData> getATStates() throws DataException {
// Already loaded?
if (this.atStates != null)
return this.atStates;
// If loading from repository, this block must have a height
if (this.blockData.getHeight() == null)
throw new IllegalStateException("Can't fetch block's AT states from repository without a block height");
// Allocate cache for results
List<ATStateData> atStateData = this.repository.getATRepository().getBlockATStatesAtHeight(this.blockData.getHeight());
// The number of AT states fetched from repository should correspond with Block's atCount
if (atStateData.size() != this.blockData.getATCount())
throw new IllegalStateException("Block's AT states from repository do not match block's AT count");
this.atStates = atStateData;
return this.atStates;
}
// Navigation
/**
* Load parent block's data from repository via this block's reference.
*
* @return parent's BlockData, or null if no parent found
* @throws DataException
*/
public BlockData getParent() throws DataException {
byte[] reference = this.blockData.getReference();
if (reference == null)
return null;
return this.repository.getBlockRepository().fromSignature(reference);
}
/**
* Load child block's data from repository via this block's signature.
*
* @return child's BlockData, or null if no parent found
* @throws DataException
*/
public BlockData getChild() throws DataException {
byte[] signature = this.blockData.getSignature();
if (signature == null)
return null;
return this.repository.getBlockRepository().fromReference(signature);
}
// Processing
/**
* Add a transaction to the block.
* <p>
* Used when constructing a new block during forging.
* <p>
* Requires block's {@code generator} being a {@code PrivateKeyAccount} so block's transactions signature can be recalculated.
*
* @param transactionData
* @return true if transaction successfully added to block, false otherwise
* @throws IllegalStateException
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
*/
public boolean addTransaction(TransactionData transactionData) {
// Can't add to transactions if we haven't loaded existing ones yet
if (this.transactions == null)
throw new IllegalStateException("Attempted to add transaction to partially loaded database Block");
if (!(this.generator instanceof PrivateKeyAccount))
throw new IllegalStateException("Block's generator has no private key");
if (this.blockData.getGeneratorSignature() == null)
throw new IllegalStateException("Cannot calculate transactions signature as block has no generator signature");
// Already added?
if (this.transactions.contains(transactionData))
return true;
// Check there is space in block
try {
if (BlockTransformer.getDataLength(this) + TransactionTransformer.getDataLength(transactionData) > MAX_BLOCK_BYTES)
return false;
} catch (TransformationException e) {
return false;
}
// Add to block
this.transactions.add(Transaction.fromData(this.repository, transactionData));
// Re-sort
this.transactions.sort(Transaction.getComparator());
// Update transaction count
this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1);
// Update totalFees
this.blockData.setTotalFees(this.blockData.getTotalFees().add(transactionData.getFee()));
// We've added a transaction, so recalculate transactions signature
calcTransactionsSignature();
return true;
}
/**
* Recalculate block's generator signature.
* <p>
* Requires block's {@code generator} being a {@code PrivateKeyAccount}.
* <p>
* Generator signature is made by the generator signing the following data:
* <p>
* previous block's generator signature + this block's generating balance + generator's public key
* <p>
* (Previous block's generator signature is extracted from this block's reference).
*
* @throws IllegalStateException
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
* @throws RuntimeException
* if somehow the generator signature cannot be calculated
*/
protected void calcGeneratorSignature() {
if (!(this.generator instanceof PrivateKeyAccount))
throw new IllegalStateException("Block's generator has no private key");
try {
this.blockData.setGeneratorSignature(((PrivateKeyAccount) this.generator).sign(BlockTransformer.getBytesForGeneratorSignature(this.blockData)));
} catch (TransformationException e) {
throw new RuntimeException("Unable to calculate block's generator signature", e);
}
}
/**
* Recalculate block's transactions signature.
* <p>
* Requires block's {@code generator} being a {@code PrivateKeyAccount}.
*
* @throws IllegalStateException
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
* @throws RuntimeException
* if somehow the transactions signature cannot be calculated
*/
protected void calcTransactionsSignature() {
if (!(this.generator instanceof PrivateKeyAccount))
throw new IllegalStateException("Block's generator has no private key");
try {
this.blockData.setTransactionsSignature(((PrivateKeyAccount) this.generator).sign(BlockTransformer.getBytesForTransactionsSignature(this)));
} catch (TransformationException e) {
throw new RuntimeException("Unable to calculate block's transactions signature", e);
}
}
/**
* Recalculate block's generator and transactions signatures, thus giving block full signature.
* <p>
* Note: Block instance must have been constructed with a <tt>PrivateKeyAccount generator</tt> or this call will throw an <tt>IllegalStateException</tt>.
*
* @throws IllegalStateException
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
*/
public void sign() {
this.calcGeneratorSignature();
this.calcTransactionsSignature();
this.blockData.setSignature(this.getSignature());
}
/**
* Returns whether this block's signatures are valid.
*
* @return true if both generator and transaction signatures are valid, false otherwise
*/
public boolean isSignatureValid() {
try {
// Check generator's signature first
if (!this.generator.verify(this.blockData.getGeneratorSignature(), BlockTransformer.getBytesForGeneratorSignature(this.blockData)))
return false;
// Check transactions signature
if (!this.generator.verify(this.blockData.getTransactionsSignature(), BlockTransformer.getBytesForTransactionsSignature(this)))
return false;
} catch (TransformationException e) {
return false;
}
return true;
}
/**
* Returns whether Block is valid.
* <p>
* Performs various tests like checking for parent block, correct block timestamp, version, generating balance, etc.
* <p>
* Checks block's transactions by testing their validity then processing them.<br>
* Hence <b>calls repository.discardChanges()</b> before returning.
*
* @return ValidationResult.OK if block is valid, or some other ValidationResult otherwise.
* @throws DataException
*/
public ValidationResult isValid() throws DataException {
// Check parent block exists
if (this.blockData.getReference() == null)
return ValidationResult.REFERENCE_MISSING;
BlockData parentBlockData = this.repository.getBlockRepository().fromSignature(this.blockData.getReference());
if (parentBlockData == null)
return ValidationResult.PARENT_DOES_NOT_EXIST;
Block parentBlock = new Block(this.repository, parentBlockData);
// Check parent doesn't already have a child block
if (parentBlock.getChild() != null)
return ValidationResult.PARENT_HAS_EXISTING_CHILD;
// Check timestamp is newer than parent timestamp
if (this.blockData.getTimestamp() <= parentBlockData.getTimestamp())
return ValidationResult.TIMESTAMP_OLDER_THAN_PARENT;
// Check timestamp is not in the future (within configurable ~500ms margin)
if (this.blockData.getTimestamp() - BlockChain.getInstance().getBlockTimestampMargin() > NTP.getTime())
return ValidationResult.TIMESTAMP_IN_FUTURE;
// Legacy gen1 test: check timestamp milliseconds is the same as parent timestamp milliseconds?
if (this.blockData.getTimestamp() % 1000 != parentBlockData.getTimestamp() % 1000)
return ValidationResult.TIMESTAMP_MS_INCORRECT;
// Too early to forge block?
// XXX DISABLED
// if (this.blockData.getTimestamp() < parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMinBlockTime())
// return ValidationResult.TIMESTAMP_TOO_SOON;
// Check block version
if (this.blockData.getVersion() != parentBlock.getNextBlockVersion())
return ValidationResult.VERSION_INCORRECT;
if (this.blockData.getVersion() < 2 && this.blockData.getATCount() != 0)
return ValidationResult.FEATURE_NOT_YET_RELEASED;
// Check generating balance
if (this.blockData.getGeneratingBalance().compareTo(parentBlock.calcNextBlockGeneratingBalance()) != 0)
return ValidationResult.GENERATING_BALANCE_INCORRECT;
// After maximum block period, then generator checks are relaxed
if (this.blockData.getTimestamp() < parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMaxBlockTime()) {
// Check generator is allowed to forge this block
BigInteger hashValue = this.calcBlockHash();
BigInteger target = parentBlock.calcGeneratorsTarget(this.generator);
// Multiply target by guesses
long guesses = (this.blockData.getTimestamp() - parentBlockData.getTimestamp()) / 1000;
BigInteger lowerTarget = target.multiply(BigInteger.valueOf(guesses - 1));
target = target.multiply(BigInteger.valueOf(guesses));
// Generator's target must exceed block's hashValue threshold
if (hashValue.compareTo(target) >= 0)
return ValidationResult.GENERATOR_NOT_ACCEPTED;
// Odd gen1 comment: "CHECK IF FIRST BLOCK OF USER"
// Each second elapsed allows generator to test a new "target" window against hashValue
if (hashValue.compareTo(lowerTarget) < 0)
return ValidationResult.GENERATOR_NOT_ACCEPTED;
}
// CIYAM ATs
if (this.blockData.getATCount() != 0) {
// Locally generated AT states should be valid so no need to re-execute them
if (this.ourAtStates != this.getATStates()) {
// For old v1 CIYAM ATs we blindly accept them
if (this.blockData.getVersion() < 4) {
this.ourAtStates = this.atStates;
this.ourAtFees = this.blockData.getATFees();
} else {
// Generate local AT states for comparison
this.executeATs();
// XXX do we need to revalidate signatures if transactions list has changed?
}
// Check locally generated AT states against ones received from elsewhere
if (this.ourAtStates.size() != this.blockData.getATCount())
return ValidationResult.AT_STATES_MISMATCH;
if (this.ourAtFees.compareTo(this.blockData.getATFees()) != 0)
return ValidationResult.AT_STATES_MISMATCH;
// Note: this.atStates fully loaded thanks to this.getATStates() call above
for (int s = 0; s < this.atStates.size(); ++s) {
ATStateData ourAtState = this.ourAtStates.get(s);
ATStateData theirAtState = this.atStates.get(s);
if (!ourAtState.getATAddress().equals(theirAtState.getATAddress()))
return ValidationResult.AT_STATES_MISMATCH;
if (!ourAtState.getStateHash().equals(theirAtState.getStateHash()))
return ValidationResult.AT_STATES_MISMATCH;
if (ourAtState.getFees().compareTo(theirAtState.getFees()) != 0)
return ValidationResult.AT_STATES_MISMATCH;
}
}
}
// Check transactions
try {
for (Transaction transaction : this.getTransactions()) {
// GenesisTransactions are not allowed (GenesisBlock overrides isValid() to allow them)
if (transaction instanceof GenesisTransaction)
return ValidationResult.GENESIS_TRANSACTIONS_INVALID;
// Check timestamp and deadline
if (transaction.getTransactionData().getTimestamp() > this.blockData.getTimestamp()
|| transaction.getDeadline() <= this.blockData.getTimestamp())
return ValidationResult.TRANSACTION_TIMESTAMP_INVALID;
// Check transaction is even valid
// NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid
Transaction.ValidationResult validationResult = transaction.isValid();
if (validationResult != Transaction.ValidationResult.OK) {
LOGGER.error("Error during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": "
+ validationResult.name());
return ValidationResult.TRANSACTION_INVALID;
}
// Process transaction to make sure other transactions validate properly
try {
transaction.process();
} catch (Exception e) {
LOGGER.error("Exception during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()), e);
e.printStackTrace();
return ValidationResult.TRANSACTION_PROCESSING_FAILED;
}
}
} catch (DataException e) {
return ValidationResult.TRANSACTION_TIMESTAMP_INVALID;
} finally {
// Discard changes to repository made by test-processing transactions above
try {
this.repository.discardChanges();
} catch (DataException e) {
/*
* discardChanges failure most likely due to prior DataException, so catch discardChanges' DataException and ignore. Prior DataException
* propagates to caller.
*/
}
}
// Block is valid
return ValidationResult.OK;
}
/**
* Execute CIYAM ATs for this block.
* <p>
* This needs to be done locally for all blocks, regardless of origin.<br>
* Typically called by <tt>isValid()</tt> or new block constructor.
* <p>
* After calling, AT-generated transactions are prepended to the block's transactions and AT state data is generated.
* <p>
* Updates <tt>this.ourAtStates</tt> (local version) and <tt>this.ourAtFees</tt> (remote/imported/loaded version).
* <p>
* Note: this method does not store new AT state data into repository - that is handled by <tt>process()</tt>.
* <p>
* This method is not needed if fetching an existing block from the repository as AT state data will be loaded from repository as well.
*
* @see #isValid()
*
* @throws DataException
*
*/
private void executeATs() throws DataException {
// We're expecting a lack of AT state data at this point.
if (this.ourAtStates != null)
throw new IllegalStateException("Attempted to execute ATs when block's local AT state data already exists");
// AT-Transactions generated by running ATs, to be prepended to block's transactions
List<ATTransaction> allATTransactions = new ArrayList<ATTransaction>();
this.ourAtStates = new ArrayList<ATStateData>();
this.ourAtFees = BigDecimal.ZERO.setScale(8);
// Find all executable ATs, ordered by earliest creation date first
List<ATData> executableATs = this.repository.getATRepository().getAllExecutableATs();
// Run each AT, appends AT-Transactions and corresponding AT states, to our lists
for (ATData atData : executableATs) {
AT at = new AT(this.repository, atData);
List<ATTransaction> atTransactions = at.run(this.blockData.getTimestamp());
allATTransactions.addAll(atTransactions);
ATStateData atStateData = at.getATStateData();
this.ourAtStates.add(atStateData);
this.ourAtFees = this.ourAtFees.add(atStateData.getFees());
}
// Prepend our entire AT-Transactions/states to block's transactions
this.transactions.addAll(0, allATTransactions);
// Re-sort
this.transactions.sort(Transaction.getComparator());
// Update transaction count
this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1);
// We've added transactions, so recalculate transactions signature
calcTransactionsSignature();
}
/**
* Process block, and its transactions, adding them to the blockchain.
*
* @throws DataException
*/
public void process() throws DataException {
// Process transactions (we'll link them to this block after saving the block itself)
// AT-generated transactions are already added to our transactions so no special handling is needed here.
List<Transaction> transactions = this.getTransactions();
for (Transaction transaction : transactions)
transaction.process();
// If fees are non-zero then add fees to generator's balance
BigDecimal blockFee = this.blockData.getTotalFees();
if (blockFee.compareTo(BigDecimal.ZERO) > 0)
this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee));
// Process AT fees and save AT states into repository
ATRepository atRepository = this.repository.getATRepository();
for (ATStateData atState : this.getATStates()) {
Account atAccount = new Account(this.repository, atState.getATAddress());
// Subtract AT-generated fees from AT accounts
atAccount.setConfirmedBalance(Asset.QORA, atAccount.getConfirmedBalance(Asset.QORA).subtract(atState.getFees()));
atRepository.save(atState);
}
// Link block into blockchain by fetching signature of highest block and setting that as our reference
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
BlockData latestBlockData = this.repository.getBlockRepository().fromHeight(blockchainHeight);
if (latestBlockData != null)
this.blockData.setReference(latestBlockData.getSignature());
this.blockData.setHeight(blockchainHeight + 1);
this.repository.getBlockRepository().save(this.blockData);
// Link transactions to this block, thus removing them from unconfirmed transactions list.
// Also update "transaction participants" in repository for "transactions involving X" support in API
for (int sequence = 0; sequence < transactions.size(); ++sequence) {
Transaction transaction = transactions.get(sequence);
// Link transaction to this block
BlockTransactionData blockTransactionData = new BlockTransactionData(this.getSignature(), sequence,
transaction.getTransactionData().getSignature());
this.repository.getBlockRepository().save(blockTransactionData);
// No longer unconfirmed
this.repository.getTransactionRepository().confirmTransaction(transaction.getTransactionData().getSignature());
List<Account> participants = transaction.getInvolvedAccounts();
List<String> participantAddresses = participants.stream().map(account -> account.getAddress()).collect(Collectors.toList());
this.repository.getTransactionRepository().saveParticipants(transaction.getTransactionData(), participantAddresses);
}
}
/**
* Removes block from blockchain undoing transactions.
* <p>
* Note: it is up to the caller to re-add any of the block's transactions back to the unconfirmed transactions pile.
*
* @throws DataException
*/
public void orphan() throws DataException {
// Orphan transactions in reverse order, and unlink them from this block
// AT-generated transactions are already added to our transactions so no special handling is needed here.
List<Transaction> transactions = this.getTransactions();
for (int sequence = transactions.size() - 1; sequence >= 0; --sequence) {
Transaction transaction = transactions.get(sequence);
transaction.orphan();
BlockTransactionData blockTransactionData = new BlockTransactionData(this.getSignature(), sequence,
transaction.getTransactionData().getSignature());
this.repository.getBlockRepository().delete(blockTransactionData);
this.repository.getTransactionRepository().deleteParticipants(transaction.getTransactionData());
}
// If fees are non-zero then remove fees from generator's balance
BigDecimal blockFee = this.blockData.getTotalFees();
if (blockFee.compareTo(BigDecimal.ZERO) > 0)
this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).subtract(blockFee));
// Return AT fees and delete AT states from repository
ATRepository atRepository = this.repository.getATRepository();
for (ATStateData atState : this.getATStates()) {
Account atAccount = new Account(this.repository, atState.getATAddress());
// Return AT-generated fees to AT accounts
atAccount.setConfirmedBalance(Asset.QORA, atAccount.getConfirmedBalance(Asset.QORA).add(atState.getFees()));
}
// Delete ATStateData for this height
atRepository.deleteATStates(this.blockData.getHeight());
// Delete block from blockchain
this.repository.getBlockRepository().delete(this.blockData);
}
/**
* Return Qora balance adjusted to within min/max limits.
*/
public static BigDecimal minMaxBalance(BigDecimal balance) {
if (balance.compareTo(Block.MIN_BALANCE) < 0)
return Block.MIN_BALANCE;
if (balance.compareTo(BlockChain.getInstance().getMaxBalance()) > 0)
return BlockChain.getInstance().getMaxBalance();
return balance;
}
}

View File

@@ -0,0 +1,245 @@
package org.qora.block;
import java.math.BigDecimal;
import java.math.MathContext;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
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;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
/**
* Class representing the blockchain as a whole.
*
*/
public class BlockChain {
private static final Logger LOGGER = LogManager.getLogger(BlockChain.class);
public enum FeatureValueType {
height,
timestamp;
}
private static BlockChain instance = null;
// Properties
private BigDecimal unitFee;
private BigDecimal maxBytesPerUnitFee;
private BigDecimal minFeePerByte;
/** Maximum coin supply. */
private BigDecimal maxBalance;;
/** Number of blocks between recalculating block's generating balance. */
private int blockDifficultyInterval;
/** Minimum target time between blocks, in seconds. */
private long minBlockTime;
/** Maximum target time between blocks, in seconds. */
private long maxBlockTime;
/** Maximum acceptable timestamp disagreement offset in milliseconds. */
private long blockTimestampMargin;
/** Map of which blockchain features are enabled when (height/timestamp) */
private Map<String, Map<FeatureValueType, Long>> featureTriggers;
// Constructors, etc.
private BlockChain() {
}
public static BlockChain getInstance() {
if (instance == null)
Settings.getInstance();
return instance;
}
// Getters / setters
public BigDecimal getUnitFee() {
return this.unitFee;
}
public BigDecimal getMaxBytesPerUnitFee() {
return this.maxBytesPerUnitFee;
}
public BigDecimal getMinFeePerByte() {
return this.minFeePerByte;
}
public BigDecimal getMaxBalance() {
return this.maxBalance;
}
public int getBlockDifficultyInterval() {
return this.blockDifficultyInterval;
}
public long getMinBlockTime() {
return this.minBlockTime;
}
public long getMaxBlockTime() {
return this.maxBlockTime;
}
public long getBlockTimestampMargin() {
return this.blockTimestampMargin;
}
private long getFeatureTrigger(String feature, FeatureValueType valueType) {
Map<FeatureValueType, Long> featureTrigger = featureTriggers.get(feature);
if (featureTrigger == null)
return 0;
Long value = featureTrigger.get(valueType);
if (value == null)
return 0;
return value;
}
// Convenience methods for specific blockchain feature triggers
public long getMessageReleaseHeight() {
return getFeatureTrigger("message", FeatureValueType.height);
}
public long getATReleaseHeight() {
return getFeatureTrigger("AT", FeatureValueType.height);
}
public long getPowFixReleaseTimestamp() {
return getFeatureTrigger("powfix", FeatureValueType.timestamp);
}
public long getAssetsReleaseTimestamp() {
return getFeatureTrigger("assets", FeatureValueType.timestamp);
}
public long getVotingReleaseTimestamp() {
return getFeatureTrigger("voting", FeatureValueType.timestamp);
}
public long getArbitraryReleaseTimestamp() {
return getFeatureTrigger("arbitrary", FeatureValueType.timestamp);
}
public long getQoraV2Timestamp() {
return getFeatureTrigger("v2", FeatureValueType.timestamp);
}
// Blockchain config from JSON
public static void fromJSON(JSONObject json) {
Object genesisJson = json.get("genesis");
if (genesisJson == null) {
LOGGER.error("No \"genesis\" entry found in blockchain config");
throw new RuntimeException("No \"genesis\" entry found in blockchain config");
}
GenesisBlock.fromJSON((JSONObject) genesisJson);
// Simple blockchain properties
BigDecimal unitFee = Settings.getJsonBigDecimal(json, "unitFee");
long maxBytesPerUnitFee = (Long) Settings.getTypedJson(json, "maxBytesPerUnitFee", Long.class);
BigDecimal maxBalance = Settings.getJsonBigDecimal(json, "coinSupply");
int blockDifficultyInterval = ((Long) Settings.getTypedJson(json, "blockDifficultyInterval", Long.class)).intValue();
long minBlockTime = 1000L * (Long) Settings.getTypedJson(json, "minBlockTime", Long.class); // config entry in seconds
long maxBlockTime = 1000L * (Long) Settings.getTypedJson(json, "maxBlockTime", Long.class); // config entry in seconds
long blockTimestampMargin = (Long) Settings.getTypedJson(json, "blockTimestampMargin", Long.class); // config entry in milliseconds
// blockchain feature triggers
Map<String, Map<FeatureValueType, Long>> featureTriggers = new HashMap<>();
JSONObject featuresJson = (JSONObject) Settings.getTypedJson(json, "featureTriggers", JSONObject.class);
for (Object feature : featuresJson.keySet()) {
String featureKey = (String) feature;
JSONObject trigger = (JSONObject) Settings.getTypedJson(featuresJson, featureKey, JSONObject.class);
if (!trigger.containsKey("height") && !trigger.containsKey("timestamp")) {
LOGGER.error("Feature trigger \"" + featureKey + "\" must contain \"height\" or \"timestamp\" in blockchain config file");
throw new RuntimeException("Feature trigger \"" + featureKey + "\" must contain \"height\" or \"timestamp\" in blockchain config file");
}
String triggerKey = (String) trigger.keySet().iterator().next();
FeatureValueType featureValueType = FeatureValueType.valueOf(triggerKey);
if (featureValueType == null) {
LOGGER.error("Unrecognised feature trigger value type \"" + triggerKey + "\" for feature \"" + featureKey + "\" in blockchain config file");
throw new RuntimeException(
"Unrecognised feature trigger value type \"" + triggerKey + "\" for feature \"" + featureKey + "\" in blockchain config file");
}
Long value = (Long) Settings.getJsonQuotedLong(trigger, triggerKey);
featureTriggers.put(featureKey, Collections.singletonMap(featureValueType, value));
}
instance = new BlockChain();
instance.unitFee = unitFee;
instance.maxBytesPerUnitFee = BigDecimal.valueOf(maxBytesPerUnitFee).setScale(8);
instance.minFeePerByte = unitFee.divide(instance.maxBytesPerUnitFee, MathContext.DECIMAL32);
instance.maxBalance = maxBalance;
instance.blockDifficultyInterval = blockDifficultyInterval;
instance.minBlockTime = minBlockTime;
instance.maxBlockTime = maxBlockTime;
instance.blockTimestampMargin = blockTimestampMargin;
instance.featureTriggers = featureTriggers;
}
/**
* Some sort start-up/initialization/checking method.
*
* @throws SQLException
*/
public static void validate() throws DataException {
// Check first block is Genesis Block
if (!isGenesisBlockValid())
rebuildBlockchain();
}
private static boolean isGenesisBlockValid() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockRepository blockRepository = repository.getBlockRepository();
int blockchainHeight = blockRepository.getBlockchainHeight();
if (blockchainHeight < 1)
return false;
BlockData blockData = blockRepository.fromHeight(1);
if (blockData == null)
return false;
return GenesisBlock.isGenesisBlock(blockData);
}
}
private static void rebuildBlockchain() throws DataException {
// (Re)build repository
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);
repository.saveChanges();
}
}
}

View File

@@ -0,0 +1,161 @@
package org.qora.block;
import java.util.Arrays;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.account.PrivateKeyAccount;
import org.qora.block.Block.ValidationResult;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.BlockRepository;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import org.qora.utils.Base58;
// Forging new blocks
// How is the private key going to be supplied?
public class BlockGenerator extends Thread {
// Properties
private byte[] generatorPrivateKey;
private PrivateKeyAccount generator;
private Block previousBlock;
private Block newBlock;
private boolean running;
// Other properties
private static final Logger LOGGER = LogManager.getLogger(BlockGenerator.class);
// Constructors
public BlockGenerator(byte[] generatorPrivateKey) {
this.generatorPrivateKey = generatorPrivateKey;
this.previousBlock = null;
this.newBlock = null;
this.running = true;
}
// Main thread loop
@Override
public void run() {
Thread.currentThread().setName("BlockGenerator");
try (final Repository repository = RepositoryManager.getRepository()) {
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
// Wipe existing unconfirmed transactions
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
for (TransactionData transactionData : unconfirmedTransactions) {
LOGGER.trace(String.format("Deleting unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
repository.getTransactionRepository().delete(transactionData);
}
repository.saveChanges();
}
generator = new PrivateKeyAccount(repository, generatorPrivateKey);
// Going to need this a lot...
BlockRepository blockRepository = repository.getBlockRepository();
while (running) {
// Check blockchain hasn't changed
BlockData lastBlockData = blockRepository.getLastBlock();
if (previousBlock == null || !Arrays.equals(previousBlock.getSignature(), lastBlockData.getSignature())) {
previousBlock = new Block(repository, lastBlockData);
newBlock = null;
}
// Do we need to build a potential new block?
if (newBlock == null)
newBlock = new Block(repository, previousBlock.getBlockData(), generator);
// Is new block valid yet? (Before adding unconfirmed transactions)
if (newBlock.isValid() == ValidationResult.OK) {
// Add unconfirmed transactions
addUnconfirmedTransactions(repository, newBlock);
// Sign to create block's signature
newBlock.sign();
// If newBlock is still valid then we can use it
ValidationResult validationResult = newBlock.isValid();
if (validationResult == ValidationResult.OK) {
// Add to blockchain - something else will notice and broadcast new block to network
try {
newBlock.process();
LOGGER.info("Generated new block: " + newBlock.getBlockData().getHeight());
repository.saveChanges();
} catch (DataException e) {
// Unable to process block - report and discard
LOGGER.error("Unable to process newly generated block?", e);
newBlock = null;
}
} else {
// No longer valid? Report and discard
LOGGER.error("Valid, generated block now invalid '" + validationResult.name() + "' after adding unconfirmed transactions?");
newBlock = null;
}
}
// Sleep for a while
try {
repository.discardChanges(); // Free transactional locks, if any
Thread.sleep(1000); // No point sleeping less than this as block timestamp millisecond values must be the same
} catch (InterruptedException e) {
// We've been interrupted - time to exit
return;
}
}
} catch (DataException e) {
LOGGER.warn("Repository issue while running block generator", e);
}
}
private void addUnconfirmedTransactions(Repository repository, Block newBlock) throws DataException {
// Grab all valid unconfirmed transactions (already sorted)
List<TransactionData> unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
for (int i = 0; i < unconfirmedTransactions.size(); ++i) {
TransactionData transactionData = unconfirmedTransactions.get(i);
// Ignore transactions that have timestamp later than block's timestamp (not yet valid)
if (transactionData.getTimestamp() > newBlock.getBlockData().getTimestamp()) {
unconfirmedTransactions.remove(i);
--i;
continue;
}
Transaction transaction = Transaction.fromData(repository, transactionData);
// Ignore transactions that have expired before this block - they will be cleaned up later
if (transaction.getDeadline() <= newBlock.getBlockData().getTimestamp()) {
unconfirmedTransactions.remove(i);
--i;
continue;
}
}
// Discard last-reference changes used to aid transaction validity checks
repository.discardChanges();
// Attempt to add transactions until block is full, or we run out
for (TransactionData transactionData : unconfirmedTransactions)
if (!newBlock.addTransaction(transactionData))
break;
}
public void shutdown() {
this.running = false;
// Interrupt too, absorbed by HSQLDB but could be caught by Thread.sleep()
this.interrupt();
}
}

View File

@@ -0,0 +1,253 @@
package org.qora.block;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.qora.account.GenesisAccount;
import org.qora.crypto.Crypto;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.GenesisTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.settings.Settings;
import org.qora.transaction.Transaction;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
public class GenesisBlock extends Block {
private static final Logger LOGGER = LogManager.getLogger(GenesisBlock.class);
private static final byte[] GENESIS_REFERENCE = new byte[] {
1, 1, 1, 1, 1, 1, 1, 1
}; // NOTE: Neither 64 nor 128 bytes!
private static final byte[] GENESIS_GENERATOR_PUBLIC_KEY = GenesisAccount.PUBLIC_KEY; // NOTE: 8 bytes not 32 bytes!
// Properties
private static BlockData blockData;
private static List<TransactionData> transactionsData;
// Constructors
private GenesisBlock(Repository repository, BlockData blockData, List<TransactionData> transactions) throws DataException {
super(repository, blockData, transactions, Collections.emptyList());
}
public static GenesisBlock getInstance(Repository repository) throws DataException {
return new GenesisBlock(repository, blockData, transactionsData);
}
// Construction from JSON
public static void fromJSON(JSONObject json) {
// Version
int version = 1; // but could be bumped later
// Timestamp
String timestampStr = (String) Settings.getTypedJson(json, "timestamp", String.class);
long timestamp;
if (timestampStr.equals("now"))
timestamp = System.currentTimeMillis();
else
try {
timestamp = Long.parseUnsignedLong(timestampStr);
} catch (NumberFormatException e) {
LOGGER.error("Unable to parse genesis timestamp: " + timestampStr);
throw new RuntimeException("Unable to parse genesis timestamp");
}
// Transactions
JSONArray transactionsJson = (JSONArray) Settings.getTypedJson(json, "transactions", JSONArray.class);
List<TransactionData> transactions = new ArrayList<>();
for (Object transactionObj : transactionsJson) {
if (!(transactionObj instanceof JSONObject)) {
LOGGER.error("Genesis transaction malformed in blockchain config file");
throw new RuntimeException("Genesis transaction malformed in blockchain config file");
}
JSONObject transactionJson = (JSONObject) transactionObj;
String recipient = (String) Settings.getTypedJson(transactionJson, "recipient", String.class);
BigDecimal amount = Settings.getJsonBigDecimal(transactionJson, "amount");
// assetId is optional
if (transactionJson.containsKey("assetId")) {
long assetId = (Long) Settings.getTypedJson(transactionJson, "assetId", Long.class);
// We're into version 4 genesis block territory now
version = 4;
transactions.add(new GenesisTransactionData(recipient, amount, assetId, timestamp));
} else {
transactions.add(new GenesisTransactionData(recipient, amount, timestamp));
}
}
// Generating balance
BigDecimal generatingBalance = Settings.getJsonBigDecimal(json, "generatingBalance");
byte[] reference = GENESIS_REFERENCE;
int transactionCount = transactions.size();
BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
byte[] generatorPublicKey = GENESIS_GENERATOR_PUBLIC_KEY;
byte[] bytesForSignature = getBytesForSignature(version, reference, generatingBalance, generatorPublicKey);
byte[] generatorSignature = calcSignature(bytesForSignature);
byte[] transactionsSignature = generatorSignature;
int height = 1;
int atCount = 0;
BigDecimal atFees = BigDecimal.ZERO.setScale(8);
blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generatorPublicKey, generatorSignature, atCount, atFees);
transactionsData = transactions;
}
// More information
public static boolean isGenesisBlock(BlockData blockData) {
if (blockData.getHeight() != 1)
return false;
byte[] signature = calcSignature(blockData);
// Validate block signature
if (!Arrays.equals(signature, blockData.getGeneratorSignature()))
return false;
// Validate transactions signature
if (!Arrays.equals(signature, blockData.getTransactionsSignature()))
return false;
return true;
}
// Processing
@Override
public boolean addTransaction(TransactionData transactionData) {
// The genesis block has a fixed set of transactions so always refuse.
return false;
}
/**
* Refuse to calculate genesis block's generator signature!
* <p>
* This is not possible as there is no private key for the genesis account and so no way to sign data.
* <p>
* <b>Always throws IllegalStateException.</b>
*
* @throws IllegalStateException
*/
@Override
public void calcGeneratorSignature() {
throw new IllegalStateException("There is no private key for genesis account");
}
/**
* Refuse to calculate genesis block's transactions signature!
* <p>
* This is not possible as there is no private key for the genesis account and so no way to sign data.
* <p>
* <b>Always throws IllegalStateException.</b>
*
* @throws IllegalStateException
*/
@Override
public void calcTransactionsSignature() {
throw new IllegalStateException("There is no private key for genesis account");
}
/**
* Generate genesis block generator/transactions signature.
* <p>
* This is handled differently as there is no private key for the genesis account and so no way to sign data.
* <p>
* Instead we return the SHA-256 digest of the block, duplicated so that the returned byte[] is the same length as normal block signatures.
*
* @return byte[]
*/
private static byte[] calcSignature(byte[] bytes) {
byte[] digest = Crypto.digest(bytes);
return Bytes.concat(digest, digest);
}
private static byte[] getBytesForSignature(int version, byte[] reference, BigDecimal generatingBalance, byte[] generatorPublicKey) {
try {
// Passing expected size to ByteArrayOutputStream avoids reallocation when adding more bytes than default 32.
// See below for explanation of some of the values used to calculated expected size.
ByteArrayOutputStream bytes = new ByteArrayOutputStream(8 + 64 + 8 + 32);
/*
* NOTE: Historic code had genesis block using Longs.toByteArray() compared to standard block's Ints.toByteArray. The subsequent
* Bytes.ensureCapacity(versionBytes, 0, 4) did not truncate versionBytes back to 4 bytes either. This means 8 bytes were used even though
* VERSION_LENGTH is set to 4. Correcting this historic bug will break genesis block signatures!
*/
bytes.write(Longs.toByteArray(version));
/*
* NOTE: Historic code had the reference expanded to only 64 bytes whereas standard block references are 128 bytes. Correcting this historic bug
* will break genesis block signatures!
*/
bytes.write(Bytes.ensureCapacity(reference, 64, 0));
bytes.write(Longs.toByteArray(generatingBalance.longValue()));
// NOTE: Genesis account's public key is only 8 bytes, not the usual 32, so we have to pad.
bytes.write(Bytes.ensureCapacity(generatorPublicKey, 32, 0));
return bytes.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** Convenience method for calculating genesis block signatures from block data */
private static byte[] calcSignature(BlockData blockData) {
byte[] bytes = getBytesForSignature(blockData.getVersion(), blockData.getReference(), blockData.getGeneratingBalance(),
blockData.getGeneratorPublicKey());
return calcSignature(bytes);
}
@Override
public boolean isSignatureValid() {
byte[] signature = calcSignature(this.getBlockData());
// Validate block signature
if (!Arrays.equals(signature, this.getBlockData().getGeneratorSignature()))
return false;
// Validate transactions signature
if (!Arrays.equals(signature, this.getBlockData().getTransactionsSignature()))
return false;
return true;
}
@Override
public ValidationResult isValid() throws DataException {
// Check there is no other block in DB
if (this.repository.getBlockRepository().getBlockchainHeight() != 0)
return ValidationResult.BLOCKCHAIN_NOT_EMPTY;
// Validate transactions
for (Transaction transaction : this.getTransactions())
if (transaction.isValid() != Transaction.ValidationResult.OK)
return ValidationResult.TRANSACTION_INVALID;
return ValidationResult.OK;
}
}

View File

@@ -0,0 +1,75 @@
package org.qora;
import java.security.SecureRandom;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.block.BlockChain;
import org.qora.block.BlockGenerator;
import org.qora.repository.DataException;
import org.qora.repository.RepositoryFactory;
import org.qora.repository.RepositoryManager;
import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qora.utils.Base58;
public class blockgenerator {
private static final Logger LOGGER = LogManager.getLogger(blockgenerator.class);
public static final String connectionUrl = "jdbc:hsqldb:file:db/blockchain;create=true";
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("usage: blockgenerator private-key-base58 | 'RANDOM'");
System.err.println("example: blockgenerator 7Vg53HrETZZuVySMPWJnVwQESS3dV8jCXPL5GDHMCeKS");
System.exit(1);
}
byte[] privateKey;
if (args[0].equalsIgnoreCase("RANDOM")) {
privateKey = new byte[32];
new SecureRandom().nextBytes(privateKey);
} else {
privateKey = Base58.decode(args[0]);
}
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
LOGGER.error("Couldn't connect to repository", e);
System.exit(2);
}
try {
BlockChain.validate();
} catch (DataException e) {
LOGGER.error("Couldn't validate repository", e);
System.exit(2);
}
BlockGenerator blockGenerator = new BlockGenerator(privateKey);
blockGenerator.start();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
blockGenerator.shutdown();
try {
blockGenerator.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
RepositoryManager.closeRepositoryFactory();
} catch (DataException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
}

View File

@@ -0,0 +1,22 @@
package org.qora;
import org.qora.crypto.BrokenMD160;
import com.google.common.hash.HashCode;
@SuppressWarnings("deprecation")
public class brokenmd160 {
public static void main(String args[]) {
if (args.length == 0) {
System.err.println("usage: broken-md160 <hex>\noutputs: hex");
System.exit(1);
}
byte[] raw = HashCode.fromString(args[0]).asBytes();
BrokenMD160 brokenMD160 = new BrokenMD160();
byte[] digest = brokenMD160.digest(raw);
System.out.println(HashCode.fromBytes(digest).toString());
}
}

View File

@@ -0,0 +1,100 @@
package org.qora.controller;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.api.ApiService;
import org.qora.block.BlockChain;
import org.qora.block.BlockGenerator;
import org.qora.repository.DataException;
import org.qora.repository.RepositoryFactory;
import org.qora.repository.RepositoryManager;
import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qora.settings.Settings;
import org.qora.utils.Base58;
public class Controller {
private static final Logger LOGGER = LogManager.getLogger(Controller.class);
public static final String connectionUrl = "jdbc:hsqldb:file:db/blockchain;create=true";
public static final long startTime = System.currentTimeMillis();
private static final Object shutdownLock = new Object();
private static boolean isStopping = false;
private static BlockGenerator blockGenerator;
public static void main(String args[]) {
LOGGER.info("Starting up...");
// Load/check settings, which potentially sets up blockchain config, etc.
Settings.getInstance();
LOGGER.info("Starting repository");
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
LOGGER.error("Unable to start repository", e);
System.exit(1);
}
LOGGER.info("Validating blockchain");
try {
BlockChain.validate();
} catch (DataException e) {
LOGGER.error("Couldn't validate blockchain", e);
System.exit(2);
}
LOGGER.info("Starting block generator");
byte[] privateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
blockGenerator = new BlockGenerator(privateKey);
blockGenerator.start();
LOGGER.info("Starting API");
try {
ApiService apiService = ApiService.getInstance();
apiService.start();
} catch (Exception e) {
LOGGER.error("Unable to start API", e);
System.exit(1);
}
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
Controller.shutdown();
}
});
}
public static void shutdown() {
synchronized (shutdownLock) {
if (!isStopping) {
isStopping = true;
LOGGER.info("Shutting down API");
ApiService.getInstance().stop();
LOGGER.info("Shutting down block generator");
blockGenerator.shutdown();
try {
blockGenerator.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
LOGGER.info("Shutting down repository");
RepositoryManager.closeRepositoryFactory();
} catch (DataException e) {
e.printStackTrace();
}
LOGGER.info("Shutdown complete!");
}
}
}
}

View File

@@ -0,0 +1,303 @@
package org.qora.crosschain;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.CheckpointManager;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.PeerGroup;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.VerificationException;
import org.bitcoinj.core.listeners.NewBestBlockListener;
import org.bitcoinj.net.discovery.DnsDiscovery;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.store.SPVBlockStore;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.KeyChainGroup;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
import org.qora.settings.Settings;
public class BTC {
private static class RollbackBlockChain extends BlockChain {
public RollbackBlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException {
super(params, blockStore);
}
@Override
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
super.setChainHead(chainHead);
}
}
private static class UpdateableCheckpointManager extends CheckpointManager implements NewBestBlockListener {
private static final int checkpointInterval = 500;
private static final String minimalTestNet3TextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n";
private static final String minimalMainNetTextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n";
public UpdateableCheckpointManager(NetworkParameters params) throws IOException {
super(params, getMinimalTextFileStream(params));
}
public UpdateableCheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException {
super(params, inputStream);
}
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) {
if (params == MainNetParams.get())
return new ByteArrayInputStream(minimalMainNetTextFile.getBytes());
if (params == TestNet3Params.get())
return new ByteArrayInputStream(minimalTestNet3TextFile.getBytes());
throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer");
}
@Override
public void notifyNewBestBlock(StoredBlock block) throws VerificationException {
int height = block.getHeight();
if (height % checkpointInterval == 0)
checkpoints.put(block.getHeader().getTimeSeconds(), block);
}
public void saveAsText(File textFile) throws FileNotFoundException {
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) {
writer.println("TXT CHECKPOINTS 1");
writer.println("0"); // Number of signatures to read. Do this later.
writer.println(checkpoints.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
for (StoredBlock block : checkpoints.values()) {
block.serializeCompact(buffer);
writer.println(CheckpointManager.BASE64.encode(buffer.array()));
buffer.position(0);
}
}
}
@SuppressWarnings("unused")
public void saveAsBinary(File file) throws IOException {
try (final FileOutputStream fileOutputStream = new FileOutputStream(file, false)) {
MessageDigest digest = Sha256Hash.newDigest();
try (final DigestOutputStream digestOutputStream = new DigestOutputStream(fileOutputStream, digest)) {
digestOutputStream.on(false);
try (final DataOutputStream dataOutputStream = new DataOutputStream(digestOutputStream)) {
dataOutputStream.writeBytes("CHECKPOINTS 1");
dataOutputStream.writeInt(0); // Number of signatures to read. Do this later.
digestOutputStream.on(true);
dataOutputStream.writeInt(checkpoints.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
for (StoredBlock block : checkpoints.values()) {
block.serializeCompact(buffer);
dataOutputStream.write(buffer.array());
buffer.position(0);
}
}
}
}
}
}
private static BTC instance;
private static final Object instanceLock = new Object();
private static File directory;
private static String chainFileName;
private static String checkpointsFileName;
private static NetworkParameters params;
private static PeerGroup peerGroup;
private static BlockStore blockStore;
private static RollbackBlockChain chain;
private static UpdateableCheckpointManager manager;
private BTC() {
// Start wallet
if (Settings.getInstance().useBitcoinTestNet()) {
params = TestNet3Params.get();
chainFileName = "bitcoinj-testnet.spvchain";
checkpointsFileName = "checkpoints-testnet.txt";
} else {
params = MainNetParams.get();
chainFileName = "bitcoinj.spvchain";
checkpointsFileName = "checkpoints.txt";
}
directory = new File("Qora-BTC");
if (!directory.exists())
directory.mkdirs();
File chainFile = new File(directory, chainFileName);
try {
blockStore = new SPVBlockStore(params, chainFile);
} catch (BlockStoreException e) {
throw new RuntimeException("Failed to open/create BTC SPVBlockStore", e);
}
File checkpointsFile = new File(directory, checkpointsFileName);
try (InputStream checkpointsStream = new FileInputStream(checkpointsFile)) {
manager = new UpdateableCheckpointManager(params, checkpointsStream);
} catch (FileNotFoundException e) {
// Construct with no checkpoints then
try {
manager = new UpdateableCheckpointManager(params);
} catch (IOException e2) {
throw new RuntimeException("Failed to create new BTC checkpoints", e2);
}
} catch (IOException e) {
throw new RuntimeException("Failed to load BTC checkpoints", e);
}
try {
chain = new RollbackBlockChain(params, blockStore);
} catch (BlockStoreException e) {
throw new RuntimeException("Failed to construct BTC blockchain", e);
}
peerGroup = new PeerGroup(params, chain);
peerGroup.setUserAgent("qqq", "1.0");
peerGroup.addPeerDiscovery(new DnsDiscovery(params));
peerGroup.start();
}
public static BTC getInstance() {
if (instance == null)
synchronized (instanceLock) {
if (instance == null)
instance = new BTC();
}
return instance;
}
public void shutdown() {
synchronized (instanceLock) {
if (instance == null)
return;
instance = null;
}
peerGroup.stop();
try {
blockStore.close();
} catch (BlockStoreException e) {
// What can we do?
}
}
protected Wallet createEmptyWallet() {
ECKey dummyKey = new ECKey();
KeyChainGroup keyChainGroup = new KeyChainGroup(params);
keyChainGroup.importKeys(dummyKey);
Wallet wallet = new Wallet(params, keyChainGroup);
wallet.removeKey(dummyKey);
return wallet;
}
public void watch(String base58Address, long startTime) throws InterruptedException, ExecutionException, TimeoutException, BlockStoreException {
Wallet wallet = createEmptyWallet();
WalletCoinsReceivedEventListener coinsReceivedListener = new WalletCoinsReceivedEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
System.out.println("Coins received via transaction " + tx.getHashAsString());
}
};
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
Address address = Address.fromBase58(params, base58Address);
wallet.addWatchedAddress(address, startTime);
StoredBlock checkpoint = manager.getCheckpointBefore(startTime);
blockStore.put(checkpoint);
blockStore.setChainHead(checkpoint);
chain.setChainHead(checkpoint);
chain.addWallet(wallet);
peerGroup.addWallet(wallet);
peerGroup.setFastCatchupTimeSecs(startTime);
System.out.println("Starting download...");
peerGroup.downloadBlockChain();
List<TransactionOutput> outputs = wallet.getWatchedOutputs(true);
peerGroup.removeWallet(wallet);
chain.removeWallet(wallet);
for (TransactionOutput output : outputs)
System.out.println(output.toString());
}
public void watch(Script script) {
// wallet.addWatchedScripts(scripts);
}
public void updateCheckpoints() {
final long now = new Date().getTime() / 1000;
try {
StoredBlock checkpoint = manager.getCheckpointBefore(now);
blockStore.put(checkpoint);
blockStore.setChainHead(checkpoint);
chain.setChainHead(checkpoint);
} catch (BlockStoreException e) {
throw new RuntimeException("Failed to update BTC checkpoints", e);
}
peerGroup.setFastCatchupTimeSecs(now);
chain.addNewBestBlockListener(Threading.SAME_THREAD, manager);
peerGroup.downloadBlockChain();
try {
manager.saveAsText(new File(directory, checkpointsFileName));
} catch (FileNotFoundException e) {
throw new RuntimeException("Failed to save updated BTC checkpoints", e);
}
}
}

View File

@@ -0,0 +1,329 @@
package org.qora.crypto;
/**
* <b>BROKEN RIPEMD160</b>
* <p>
* <b>DO NOT USE in future code</b> as this implementation is BROKEN and returns incorrect digests for some inputs.
* <p>
* It is only "grand-fathered" here to produce correct QORA addresses.
*/
@Deprecated
public class BrokenMD160 {
private static final int[][] ArgArray = {
{ 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, 11, 13, 6, 7, 14, 9, 13, 15, 14,
8, 13, 6, 5, 12, 7, 5, 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 },
{ 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, 9, 7, 15, 11, 8, 6, 6, 14, 12, 13,
5, 14, 13, 13, 7, 5, 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 } };
private static final int[][] IndexArray = {
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6,
13, 11, 5, 12, 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 },
{ 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12,
2, 10, 0, 4, 13, 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 } };
private int[] MDbuf;
public BrokenMD160() {
MDbuf = new int[5];
MDbuf[0] = 0x67452301;
MDbuf[1] = 0xefcdab89;
MDbuf[2] = 0x98badcfe;
MDbuf[3] = 0x10325476;
MDbuf[4] = 0xc3d2e1f0;
working = new int[16];
working_ptr = 0;
msglen = 0;
}
public void reset() {
MDbuf = new int[5];
MDbuf[0] = 0x67452301;
MDbuf[1] = 0xefcdab89;
MDbuf[2] = 0x98badcfe;
MDbuf[3] = 0x10325476;
MDbuf[4] = 0xc3d2e1f0;
working = new int[16];
working_ptr = 0;
msglen = 0;
}
private void compress(int[] X) {
int index = 0;
int a, b, c, d, e;
int A, B, C, D, E;
int temp, s;
A = a = MDbuf[0];
B = b = MDbuf[1];
C = c = MDbuf[2];
D = d = MDbuf[3];
E = e = MDbuf[4];
for (; index < 16; index++) {
// The 16 FF functions - round 1 */
temp = a + (b ^ c ^ d) + X[IndexArray[0][index]];
a = e;
e = d;
d = (c << 10) | (c >>> 22);
c = b;
s = ArgArray[0][index];
b = ((temp << s) | (temp >>> (32 - s))) + a;
// The 16 JJJ functions - parallel round 1 */
temp = A + (B ^ (C | ~D)) + X[IndexArray[1][index]] + 0x50a28be6;
A = E;
E = D;
D = (C << 10) | (C >>> 22);
C = B;
s = ArgArray[1][index];
B = ((temp << s) | (temp >>> (32 - s))) + A;
}
for (; index < 32; index++) {
// The 16 GG functions - round 2 */
temp = a + ((b & c) | (~b & d)) + X[IndexArray[0][index]] + 0x5a827999;
a = e;
e = d;
d = (c << 10) | (c >>> 22);
c = b;
s = ArgArray[0][index];
b = ((temp << s) | (temp >>> (32 - s))) + a;
// The 16 III functions - parallel round 2 */
temp = A + ((B & D) | (C & ~D)) + X[IndexArray[1][index]] + 0x5c4dd124;
A = E;
E = D;
D = (C << 10) | (C >>> 22);
C = B;
s = ArgArray[1][index];
B = ((temp << s) | (temp >>> (32 - s))) + A;
}
for (; index < 48; index++) {
// The 16 HH functions - round 3 */
temp = a + ((b | ~c) ^ d) + X[IndexArray[0][index]] + 0x6ed9eba1;
a = e;
e = d;
d = (c << 10) | (c >>> 22);
c = b;
s = ArgArray[0][index];
b = ((temp << s) | (temp >>> (32 - s))) + a;
// The 16 HHH functions - parallel round 3 */
temp = A + ((B | ~C) ^ D) + X[IndexArray[1][index]] + 0x6d703ef3;
A = E;
E = D;
D = (C << 10) | (C >>> 22);
C = B;
s = ArgArray[1][index];
B = ((temp << s) | (temp >>> (32 - s))) + A;
}
for (; index < 64; index++) {
// The 16 II functions - round 4 */
temp = a + ((b & d) | (c & ~d)) + X[IndexArray[0][index]] + 0x8f1bbcdc;
a = e;
e = d;
d = (c << 10) | (c >>> 22);
c = b;
s = ArgArray[0][index];
b = ((temp << s) | (temp >>> (32 - s))) + a;
// The 16 GGG functions - parallel round 4 */
temp = A + ((B & C) | (~B & D)) + X[IndexArray[1][index]] + 0x7a6d76e9;
A = E;
E = D;
D = (C << 10) | (C >>> 22);
C = B;
s = ArgArray[1][index];
B = ((temp << s) | (temp >>> (32 - s))) + A;
}
for (; index < 80; index++) {
// The 16 JJ functions - round 5 */
temp = a + (b ^ (c | ~d)) + X[IndexArray[0][index]] + 0xa953fd4e;
a = e;
e = d;
d = (c << 10) | (c >>> 22);
c = b;
s = ArgArray[0][index];
b = ((temp << s) | (temp >>> (32 - s))) + a;
// The 16 FFF functions - parallel round 5 */
temp = A + (B ^ C ^ D) + X[IndexArray[1][index]];
A = E;
E = D;
D = (C << 10) | (C >>> 22);
C = B;
s = ArgArray[1][index];
B = ((temp << s) | (temp >>> (32 - s))) + A;
}
/* combine results */
D += c + MDbuf[1]; /* final result for MDbuf[0] */
MDbuf[1] = MDbuf[2] + d + E;
MDbuf[2] = MDbuf[3] + e + A;
MDbuf[3] = MDbuf[4] + a + B;
MDbuf[4] = MDbuf[0] + b + C;
MDbuf[0] = D;
}
private void MDfinish(int[] array, int lswlen, int mswlen) {
int[] X = array; /* message words */
/* append the bit m_n == 1 */
X[(lswlen >> 2) & 15] ^= 1 << (((lswlen & 3) << 3) + 7);
if ((lswlen & 63) > 55) {
/* length goes to next block */
compress(X);
for (int i = 0; i < 14; i++) {
X[i] = 0;
}
}
/* append length in bits */
X[14] = lswlen << 3;
X[15] = (lswlen >> 29) | (mswlen << 3);
compress(X);
}
private int[] working;
private int working_ptr;
private int msglen;
public void update(byte input) {
working[working_ptr >> 2] ^= ((int) input) << ((working_ptr & 3) << 3);
working_ptr++;
if (working_ptr == 64) {
compress(working);
for (int j = 0; j < 16; j++) {
working[j] = 0;
}
working_ptr = 0;
}
msglen++;
}
public void update(byte[] input) {
for (int i = 0; i < input.length; i++) {
working[working_ptr >> 2] ^= ((int) input[i]) << ((working_ptr & 3) << 3);
working_ptr++;
if (working_ptr == 64) {
compress(working);
for (int j = 0; j < 16; j++) {
working[j] = 0;
}
working_ptr = 0;
}
}
msglen += input.length;
}
public void update(byte[] input, int offset, int len) {
if (offset + len >= input.length) {
for (int i = offset; i < input.length; i++) {
working[working_ptr >> 2] ^= ((int) input[i]) << ((working_ptr & 3) << 3);
working_ptr++;
if (working_ptr == 64) {
compress(working);
for (int j = 0; j < 16; j++) {
working[j] = 0;
}
working_ptr = 0;
}
}
msglen += input.length - offset;
} else {
for (int i = offset; i < offset + len; i++) {
working[working_ptr >> 2] ^= ((int) input[i]) << ((working_ptr & 3) << 3);
working_ptr++;
if (working_ptr == 64) {
compress(working);
for (int j = 0; j < 16; j++) {
working[j] = 0;
}
working_ptr = 0;
}
}
msglen += len;
}
}
public void update(String s) {
byte[] bytearray = new byte[s.length()];
for (int i = 0; i < bytearray.length; i++) {
bytearray[i] = (byte) s.charAt(i);
}
update(bytearray);
}
public byte[] digestBin() {
MDfinish(working, msglen, 0);
byte[] res = new byte[20];
for (int i = 0; i < 20; i++) {
res[i] = (byte) ((MDbuf[i >> 2] >>> ((i & 3) << 3)) & 0x000000FF);
}
return res;
}
public byte[] digest(byte[] input) {
update(input);
return digestBin();
}
public String digest() {
MDfinish(working, msglen, 0);
byte[] res = new byte[20];
for (int i = 0; i < 20; i++) {
res[i] = (byte) ((MDbuf[i >> 2] >>> ((i & 3) << 3)) & 0x000000FF);
}
String hex = "";
for (int i = 0; i < res.length; i++) {
hex += byteToHex(res[i]);
}
return hex;
}
public byte[] digest(byte[] input, int offset, int len) {
update(input, offset, len);
return digestBin();
}
public int[] intdigest() {
int[] res = new int[5];
for (int i = 0; i < 5; i++) {
res[i] = MDbuf[i];
}
return res;
}
public static String byteToHex(byte b) {
byte top = (byte) (((256 + b) / 16) & 15);
byte bottom = (byte) ((256 + b) & 15);
String res;
if (top > 9) {
res = "" + (char) ('a' + (top - 10));
} else {
res = "" + (char) ('0' + top);
}
if (bottom > 9) {
res += (char) ('a' + (bottom - 10));
} else {
res += (char) ('0' + bottom);
}
return res;
}
public static String RIPEMD160String(String txt) {
BrokenMD160 r = new BrokenMD160();
r.update(txt);
return r.digest();
}
}

View File

@@ -0,0 +1,109 @@
package org.qora.crypto;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import org.qora.account.Account;
import org.qora.utils.Base58;
public class Crypto {
public static final byte ADDRESS_VERSION = 58;
public static final byte AT_ADDRESS_VERSION = 23;
/**
* Returns 32-byte SHA-256 digest of message passed in input.
*
* @param input
* variable-length byte[] message
* @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
*/
public static byte[] digest(byte[] input) {
if (input == null)
return null;
try {
// SHA2-256
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
return sha256.digest(input);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
/**
* Returns 32-byte digest of two rounds of SHA-256 on message passed in input.
*
* @param input
* variable-length byte[] message
* @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
*/
public static byte[] doubleDigest(byte[] input) {
return digest(digest(input));
}
@SuppressWarnings("deprecation")
private static String toAddress(byte addressVersion, byte[] input) {
// 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);
// Create address data using above hash and addressVersion (prepended)
byte[] addressBytes = new byte[inputHash.length + 1];
System.arraycopy(inputHash, 0, addressBytes, 1, inputHash.length);
addressBytes[0] = addressVersion;
// Generate checksum
byte[] checksum = doubleDigest(addressBytes);
// Append checksum
byte[] addressWithChecksum = new byte[addressBytes.length + 4];
System.arraycopy(addressBytes, 0, addressWithChecksum, 0, addressBytes.length);
System.arraycopy(checksum, 0, addressWithChecksum, addressBytes.length, 4);
// Return Base58-encoded
return Base58.encode(addressWithChecksum);
}
public static String toAddress(byte[] publicKey) {
return toAddress(ADDRESS_VERSION, publicKey);
}
public static String toATAddress(byte[] signature) {
return toAddress(AT_ADDRESS_VERSION, signature);
}
public static boolean isValidAddress(String address) {
byte[] addressBytes;
try {
// Attempt Base58 decoding
addressBytes = Base58.decode(address);
} catch (NumberFormatException e) {
return false;
}
// Check address length
if (addressBytes.length != Account.ADDRESS_LENGTH)
return false;
// Check by address type
switch (addressBytes[0]) {
case ADDRESS_VERSION:
case AT_ADDRESS_VERSION:
byte[] addressWithoutChecksum = Arrays.copyOf(addressBytes, addressBytes.length - 4);
byte[] passedChecksum = Arrays.copyOfRange(addressBytes, addressBytes.length - 4, addressBytes.length);
byte[] generatedChecksum = Arrays.copyOf(doubleDigest(addressWithoutChecksum), 4);
return Arrays.equals(passedChecksum, generatedChecksum);
default:
return false;
}
}
}

View File

@@ -0,0 +1,36 @@
package org.qora.crypto;
//Punisher.NaCl;
public class CryptoBytes
{
public static Boolean ConstantTimeEquals(byte[] x, int xOffset, byte[] y, int yOffset, int length) throws Exception
{
if (x == null) throw new Exception("x");
if (xOffset < 0) throw new Exception("xOffset" + "xOffset < 0");
if (y == null) throw new Exception("y");
if (yOffset < 0) throw new Exception("yOffset" + "yOffset < 0");
if (length < 0) throw new Exception("length" + "length < 0");
if (x.length - xOffset < length) throw new Exception("xOffset + length > x.Length");
if (y.length - yOffset < length) throw new Exception("yOffset + length > y.Length");
return InternalConstantTimeEquals(x, xOffset, y, yOffset, length);
}
public static boolean InternalConstantTimeEquals(byte[] x, int xOffset, byte[] y, int yOffset, int length)
{
int result = 0;
for (int i = 0; i < length; i++)
{
result |= x[xOffset + i] ^ y[yOffset + i];
}
return result == 0; // Check const time
}
public static void Wipe(byte[] data)
{
for (int i = 0; i < data.length; i++)
data[i] = 0;
}
}

View File

@@ -0,0 +1,195 @@
package org.qora.crypto;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.qora.utils.Pair;
import org.whispersystems.curve25519.java.*;
public class Ed25519 {
public static byte[] getSharedSecret(byte[] public_key, byte[] private_key)
{
byte[] shared_secret = new byte[32];
byte[] e = new byte[32];
int i;
int[] x1 = new int[10];
int[] x2 = new int[10];
int[] z2 = new int[10];
int[] x3 = new int[10];
int[] z3 = new int[10];
int[] tmp0 = new int[10];
int[] tmp1 = new int[10];
int pos;
int swap;
int b;
/* copy the private key and make sure it's valid */
for (i = 0; i < 32; ++i) {
e[i] = private_key[i];
}
e[0] &= 248;
e[31] &= 63;
e[31] |= 64;
/* unpack the public key and convert edwards to montgomery */
/* due to CodesInChaos: montgomeryX = (edwardsY + 1)*inverse(1 - edwardsY) mod p */
fe_frombytes.fe_frombytes(x1, public_key);
fe_1.fe_1(tmp1);
fe_add.fe_add(tmp0, x1, tmp1);
fe_sub.fe_sub(tmp1, tmp1, x1);
fe_invert.fe_invert(tmp1, tmp1);
fe_mul.fe_mul(x1, tmp0, tmp1);
fe_1.fe_1(x2);
fe_0.fe_0(z2);
fe_copy.fe_copy(x3, x1);
fe_1.fe_1(z3);
swap = 0;
for (pos = 254; pos >= 0; --pos) {
b = e[pos / 8] >> (pos & 7);
b &= 1;
swap ^= b;
fe_cswap.fe_cswap(x2, x3, swap);
fe_cswap.fe_cswap(z2, z3, swap);
swap = b;
/* from montgomery.h */
fe_sub.fe_sub(tmp0, x3, z3);
fe_sub.fe_sub(tmp1, x2, z2);
fe_add.fe_add(x2, x2, z2);
fe_add.fe_add(z2, x3, z3);
fe_mul.fe_mul(z3, tmp0, x2);
fe_mul.fe_mul(z2, z2, tmp1);
fe_sq.fe_sq(tmp0, tmp1);
fe_sq.fe_sq(tmp1, x2);
fe_add.fe_add(x3, z3, z2);
fe_sub.fe_sub(z2, z3, z2);
fe_mul.fe_mul(x2, tmp1, tmp0);
fe_sub.fe_sub(tmp1, tmp1, tmp0);
fe_sq.fe_sq(z2, z2);
fe_mul121666.fe_mul121666(z3, tmp1);
fe_sq.fe_sq(x3, x3);
fe_add.fe_add(tmp0, tmp0, z3);
fe_mul.fe_mul(z3, x1, z2);
fe_mul.fe_mul(z2, tmp1, tmp0);
}
fe_cswap.fe_cswap(x2, x3, swap);
fe_cswap.fe_cswap(z2, z3, swap);
fe_invert.fe_invert(z2, z2);
fe_mul.fe_mul(x2, x2, z2);
fe_tobytes.fe_tobytes(shared_secret, x2);
return shared_secret;
}
public static boolean verify(byte[] signature, byte[] message, byte[] publicKey) throws Exception
{
byte[] h = new byte[64];
byte[] checker = new byte[32];
ge_p3 A = new ge_p3();
ge_p2 R = new ge_p2();
if ((signature[63] & 224) != 0) {
return false;
}
if (ge_frombytes.ge_frombytes_negate_vartime(A, publicKey) != 0) {
return false;
}
MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
sha512.update(signature, 0, 32);
sha512.update(publicKey, 0, 32);
sha512.update(message, 0, message.length);
h = sha512.digest();
sc_reduce.sc_reduce(h);
byte[] sm32 = new byte[32];
System.arraycopy(signature, 32, sm32, 0, 32);
ge_double_scalarmult.ge_double_scalarmult_vartime(R, h, A, sm32);
ge_tobytes.ge_tobytes(checker, R);
Boolean result = CryptoBytes.ConstantTimeEquals(checker, 0, signature, 0, 32);
return result;
}
public static byte[] sign(Pair<byte[],byte[]> keyPair, byte[] message) throws NoSuchAlgorithmException
{
byte[] private_key = keyPair.getA();
byte[] public_key = keyPair.getB();
byte[] signature = new byte[64];
byte[] hram = new byte[64];
byte[] r = new byte[64];
ge_p3 R = new ge_p3();
MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
sha512.update(private_key, 32, 32);
sha512.update(message, 0, message.length);
r = sha512.digest();
sc_reduce.sc_reduce(r);
ge_scalarmult_base.ge_scalarmult_base(R, r);
ge_p3_tobytes.ge_p3_tobytes(signature, R);
sha512 = MessageDigest.getInstance("SHA-512");
sha512.update(signature, 0, 32);
sha512.update(public_key, 0, 32);
sha512.update(message, 0, message.length);
hram = sha512.digest();
sc_reduce.sc_reduce(hram);
byte[] sm32 = new byte[32];
sc_muladd.sc_muladd(sm32, hram, private_key, r);
System.arraycopy(sm32, 0, signature, 32, 32);
CryptoBytes.Wipe(sm32);
return signature;
}
public static Pair<byte[], byte[]> createKeyPair(byte[] seed)
{
byte[] private_key = new byte[64];
byte[] public_key = new byte[32];
ge_p3 A = new ge_p3();
sha512(seed, 32, private_key);
private_key[0] &= 248;
private_key[31] &= 63;
private_key[31] |= 64;
ge_scalarmult_base.ge_scalarmult_base(A, private_key);
ge_p3_tobytes.ge_p3_tobytes(public_key, A);
return new Pair<byte[], byte[]>(private_key, public_key);
}
public static void sha512(byte[] in, long length, byte[] out) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
messageDigest.update(in, 0, (int)length);
byte[] digest = messageDigest.digest();
System.arraycopy(digest, 0, out, 0, digest.length);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,43 @@
package org.qora.data;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class PaymentData {
// Properties
protected String recipient;
protected long assetId;
protected BigDecimal amount;
// Constructors
// For JAX-RS
protected PaymentData() {
}
public PaymentData(String recipient, long assetId, BigDecimal amount) {
this.recipient = recipient;
this.assetId = assetId;
this.amount = amount;
}
// Getters/setters
public String getRecipient() {
return this.recipient;
}
public long getAssetId() {
return this.assetId;
}
public BigDecimal getAmount() {
return this.amount;
}
}

View File

@@ -0,0 +1,48 @@
package org.qora.data.account;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class AccountBalanceData {
// Properties
private String address;
private long assetId;
private BigDecimal balance;
// Constructors
// necessary for JAX-RS serialization
@SuppressWarnings("unused")
private AccountBalanceData() {
}
public AccountBalanceData(String address, long assetId, BigDecimal balance) {
this.address = address;
this.assetId = assetId;
this.balance = balance;
}
// Getters/Setters
public String getAddress() {
return this.address;
}
public long getAssetId() {
return this.assetId;
}
public BigDecimal getBalance() {
return this.balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}

View File

@@ -0,0 +1,59 @@
package org.qora.data.account;
public class AccountData {
// Properties
protected String address;
protected byte[] reference;
protected byte[] publicKey;
// Constructors
public AccountData(String address, byte[] reference, byte[] publicKey) {
this.address = address;
this.reference = reference;
this.publicKey = publicKey;
}
public AccountData(String address) {
this(address, null, null);
}
// Getters/Setters
public String getAddress() {
return this.address;
}
public byte[] getReference() {
return this.reference;
}
public void setReference(byte[] reference) {
this.reference = reference;
}
public byte[] getPublicKey() {
return this.publicKey;
}
public void setPublicKey(byte[] publicKey) {
this.publicKey = publicKey;
}
// Comparison
@Override
public boolean equals(Object b) {
if (!(b instanceof AccountData))
return false;
return this.getAddress().equals(((AccountData) b).getAddress());
}
@Override
public int hashCode() {
return this.getAddress().hashCode();
}
}

View File

@@ -0,0 +1,75 @@
package org.qora.data.asset;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class AssetData {
// Properties
private Long assetId;
private String owner;
private String name;
private String description;
private long quantity;
private boolean isDivisible;
private byte[] reference;
// Constructors
// necessary for JAX-RS serialization
protected AssetData() {
}
// NOTE: key is Long, not long, because it can be null if asset ID/key not yet assigned.
public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) {
this.assetId = assetId;
this.owner = owner;
this.name = name;
this.description = description;
this.quantity = quantity;
this.isDivisible = isDivisible;
this.reference = reference;
}
// New asset with unassigned assetId
public AssetData(String owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) {
this(null, owner, name, description, quantity, isDivisible, reference);
}
// Getters/Setters
public Long getAssetId() {
return this.assetId;
}
public void setAssetId(Long assetId) {
this.assetId = assetId;
}
public String getOwner() {
return this.owner;
}
public String getName() {
return this.name;
}
public String getDescription() {
return this.description;
}
public long getQuantity() {
return this.quantity;
}
public boolean getIsDivisible() {
return this.isDivisible;
}
public byte[] getReference() {
return this.reference;
}
}

View File

@@ -0,0 +1,125 @@
package org.qora.data.asset;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class OrderData implements Comparable<OrderData> {
// Properties
private byte[] orderId;
private byte[] creatorPublicKey;
@Schema(description = "asset on offer to give by order creator")
private long haveAssetId;
@Schema(description = "asset wanted to receive by order creator")
private long wantAssetId;
@Schema(description = "amount of \"have\" asset to trade")
private BigDecimal amount;
@Schema(description = "amount of \"want\" asset to receive per unit of \"have\" asset traded")
private BigDecimal price;
@Schema(description = "how much \"have\" asset has traded")
private BigDecimal fulfilled;
private long timestamp;
@Schema(description = "has this order been cancelled for further trades?")
private boolean isClosed;
@Schema(description = "has this order been fully traded?")
private boolean isFulfilled;
// Constructors
// necessary for JAX-RS serialization
protected OrderData() {
}
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price,
long timestamp, boolean isClosed, boolean isFulfilled) {
this.orderId = orderId;
this.creatorPublicKey = creatorPublicKey;
this.haveAssetId = haveAssetId;
this.wantAssetId = wantAssetId;
this.amount = amount;
this.fulfilled = fulfilled;
this.price = price;
this.timestamp = timestamp;
this.isClosed = isClosed;
this.isFulfilled = isFulfilled;
}
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, long timestamp) {
this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), price, timestamp, false, false);
}
// Getters/setters
public byte[] getOrderId() {
return this.orderId;
}
public byte[] getCreatorPublicKey() {
return this.creatorPublicKey;
}
public long getHaveAssetId() {
return this.haveAssetId;
}
public long getWantAssetId() {
return this.wantAssetId;
}
public BigDecimal getAmount() {
return this.amount;
}
public BigDecimal getFulfilled() {
return this.fulfilled;
}
public void setFulfilled(BigDecimal fulfilled) {
this.fulfilled = fulfilled;
}
public BigDecimal getPrice() {
return this.price;
}
public long getTimestamp() {
return this.timestamp;
}
public boolean getIsClosed() {
return this.isClosed;
}
public void setIsClosed(boolean isClosed) {
this.isClosed = isClosed;
}
public boolean getIsFulfilled() {
return this.isFulfilled;
}
public void setIsFulfilled(boolean isFulfilled) {
this.isFulfilled = isFulfilled;
}
@Override
public int compareTo(OrderData orderData) {
// Compare using prices
return this.price.compareTo(orderData.getPrice());
}
}

View File

@@ -0,0 +1,71 @@
package org.qora.data.asset;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeData {
// Properties
@Schema(name = "initiatingOrderId", description = "ID of order that caused trade")
@XmlElement(name = "initiatingOrderId")
private byte[] initiator;
@Schema(name = "targetOrderId", description = "ID of order that matched")
@XmlElement(name = "targetOrderId")
private byte[] target;
@Schema(name = "targetAmount", description = "amount traded from target order")
@XmlElement(name = "targetAmount")
private BigDecimal amount;
@Schema(name = "initiatorAmount", description = "amount traded from initiating order")
@XmlElement(name = "initiatorAmount")
private BigDecimal price;
@Schema(description = "when trade happened")
private long timestamp;
// Constructors
// necessary for JAX-RS serialization
protected TradeData() {
}
public TradeData(byte[] initiator, byte[] target, BigDecimal amount, BigDecimal price, long timestamp) {
this.initiator = initiator;
this.target = target;
this.amount = amount;
this.price = price;
this.timestamp = timestamp;
}
// Getters/setters
public byte[] getInitiator() {
return this.initiator;
}
public byte[] getTarget() {
return this.target;
}
public BigDecimal getAmount() {
return this.amount;
}
public BigDecimal getPrice() {
return this.price;
}
public long getTimestamp() {
return this.timestamp;
}
}

View File

@@ -0,0 +1,123 @@
package org.qora.data.at;
import java.math.BigDecimal;
public class ATData {
// Properties
private String ATAddress;
private byte[] creatorPublicKey;
private long creation;
private int version;
private long assetId;
private byte[] codeBytes;
private boolean isSleeping;
private Integer sleepUntilHeight;
private boolean isFinished;
private boolean hadFatalError;
private boolean isFrozen;
private BigDecimal frozenBalance;
// Constructors
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping,
Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, BigDecimal frozenBalance) {
this.ATAddress = ATAddress;
this.creatorPublicKey = creatorPublicKey;
this.creation = creation;
this.version = version;
this.assetId = assetId;
this.codeBytes = codeBytes;
this.isSleeping = isSleeping;
this.sleepUntilHeight = sleepUntilHeight;
this.isFinished = isFinished;
this.hadFatalError = hadFatalError;
this.isFrozen = isFrozen;
this.frozenBalance = frozenBalance;
}
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping,
Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
this(ATAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
(BigDecimal) null);
// Convert Long frozenBalance to BigDecimal
if (frozenBalance != null)
this.frozenBalance = BigDecimal.valueOf(frozenBalance, 8);
}
// Getters / setters
public String getATAddress() {
return this.ATAddress;
}
public byte[] getCreatorPublicKey() {
return this.creatorPublicKey;
}
public long getCreation() {
return this.creation;
}
public int getVersion() {
return this.version;
}
public long getAssetId() {
return this.assetId;
}
public byte[] getCodeBytes() {
return this.codeBytes;
}
public boolean getIsSleeping() {
return this.isSleeping;
}
public void setIsSleeping(boolean isSleeping) {
this.isSleeping = isSleeping;
}
public Integer getSleepUntilHeight() {
return this.sleepUntilHeight;
}
public void setSleepUntilHeight(Integer sleepUntilHeight) {
this.sleepUntilHeight = sleepUntilHeight;
}
public boolean getIsFinished() {
return this.isFinished;
}
public void setIsFinished(boolean isFinished) {
this.isFinished = isFinished;
}
public boolean getHadFatalError() {
return this.hadFatalError;
}
public void setHadFatalError(boolean hadFatalError) {
this.hadFatalError = hadFatalError;
}
public boolean getIsFrozen() {
return this.isFrozen;
}
public void setIsFrozen(boolean isFrozen) {
this.isFrozen = isFrozen;
}
public BigDecimal getFrozenBalance() {
return this.frozenBalance;
}
public void setFrozenBalance(BigDecimal frozenBalance) {
this.frozenBalance = frozenBalance;
}
}

View File

@@ -0,0 +1,73 @@
package org.qora.data.at;
import java.math.BigDecimal;
public class ATStateData {
// Properties
private String ATAddress;
private Integer height;
private Long creation;
private byte[] stateData;
private byte[] stateHash;
private BigDecimal fees;
// Constructors
/** Create new ATStateData */
public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, BigDecimal fees) {
this.ATAddress = ATAddress;
this.height = height;
this.creation = creation;
this.stateData = stateData;
this.stateHash = stateHash;
this.fees = fees;
}
/** For recreating per-block ATStateData from repository where not all info is needed */
public ATStateData(String ATAddress, int height, byte[] stateHash, BigDecimal fees) {
this(ATAddress, height, null, null, stateHash, fees);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash) {
this(ATAddress, null, null, null, stateHash, null);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash, BigDecimal fees) {
this(ATAddress, null, null, null, stateHash, fees);
}
// Getters / setters
public String getATAddress() {
return this.ATAddress;
}
public Integer getHeight() {
return this.height;
}
// Likely to be used when block received over network is attached to blockchain
public void setHeight(Integer height) {
this.height = height;
}
public Long getCreation() {
return this.creation;
}
public byte[] getStateData() {
return this.stateData;
}
public byte[] getStateHash() {
return this.stateHash;
}
public BigDecimal getFees() {
return this.fees;
}
}

View File

@@ -0,0 +1,160 @@
package org.qora.data.block;
import com.google.common.primitives.Bytes;
import java.io.Serializable;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import org.qora.crypto.Crypto;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockData implements Serializable {
private static final long serialVersionUID = -7678329659124664620L;
// Properties
private byte[] signature;
private int version;
private byte[] reference;
private int transactionCount;
private BigDecimal totalFees;
private byte[] transactionsSignature;
private Integer height;
private long timestamp;
private BigDecimal generatingBalance;
private byte[] generatorPublicKey;
private byte[] generatorSignature;
private int atCount;
private BigDecimal atFees;
// Constructors
// necessary for JAX-RS serialization
protected BlockData() {
}
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp,
BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees) {
this.version = version;
this.reference = reference;
this.transactionCount = transactionCount;
this.totalFees = totalFees;
this.transactionsSignature = transactionsSignature;
this.height = height;
this.timestamp = timestamp;
this.generatingBalance = generatingBalance;
this.generatorPublicKey = generatorPublicKey;
this.generatorSignature = generatorSignature;
this.atCount = atCount;
this.atFees = atFees;
if (this.generatorSignature != null && this.transactionsSignature != null)
this.signature = Bytes.concat(this.generatorSignature, this.transactionsSignature);
else
this.signature = null;
}
// Getters/setters
public byte[] getSignature() {
return this.signature;
}
public void setSignature(byte[] signature) {
this.signature = signature;
}
public int getVersion() {
return this.version;
}
public byte[] getReference() {
return this.reference;
}
public void setReference(byte[] reference) {
this.reference = reference;
}
public int getTransactionCount() {
return this.transactionCount;
}
public void setTransactionCount(int transactionCount) {
this.transactionCount = transactionCount;
}
public BigDecimal getTotalFees() {
return this.totalFees;
}
public void setTotalFees(BigDecimal totalFees) {
this.totalFees = totalFees;
}
public byte[] getTransactionsSignature() {
return this.transactionsSignature;
}
public void setTransactionsSignature(byte[] transactionsSignature) {
this.transactionsSignature = transactionsSignature;
}
public Integer getHeight() {
return this.height;
}
public void setHeight(Integer height) {
this.height = height;
}
public long getTimestamp() {
return this.timestamp;
}
public BigDecimal getGeneratingBalance() {
return this.generatingBalance;
}
public byte[] getGeneratorPublicKey() {
return this.generatorPublicKey;
}
public byte[] getGeneratorSignature() {
return this.generatorSignature;
}
public void setGeneratorSignature(byte[] generatorSignature) {
this.generatorSignature = generatorSignature;
}
public int getATCount() {
return this.atCount;
}
public void setATCount(int atCount) {
this.atCount = atCount;
}
public BigDecimal getATFees() {
return this.atFees;
}
public void setATFees(BigDecimal atFees) {
this.atFees = atFees;
}
// JAXB special
@XmlElement(name = "generatorAddress")
protected String getGeneratorAddress() {
return Crypto.toAddress(this.generatorPublicKey);
}
}

View File

@@ -0,0 +1,32 @@
package org.qora.data.block;
public class BlockTransactionData {
// Properties
private byte[] blockSignature;
private int sequence;
private byte[] transactionSignature;
// Constructors
public BlockTransactionData(byte[] blockSignature, int sequence, byte[] transactionSignature) {
this.blockSignature = blockSignature;
this.sequence = sequence;
this.transactionSignature = transactionSignature;
}
// Getters/setters
public byte[] getBlockSignature() {
return this.blockSignature;
}
public int getSequence() {
return this.sequence;
}
public byte[] getTransactionSignature() {
return this.transactionSignature;
}
}

View File

@@ -0,0 +1,99 @@
package org.qora.data.naming;
import java.math.BigDecimal;
public class NameData {
// Properties
private byte[] registrantPublicKey;
private String owner;
private String name;
private String data;
private long registered;
private Long updated;
private byte[] reference;
private boolean isForSale;
private BigDecimal salePrice;
// Constructors
public NameData(byte[] registrantPublicKey, String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale,
BigDecimal salePrice) {
this.registrantPublicKey = registrantPublicKey;
this.owner = owner;
this.name = name;
this.data = data;
this.registered = registered;
this.updated = updated;
this.reference = reference;
this.isForSale = isForSale;
this.salePrice = salePrice;
}
public NameData(byte[] registrantPublicKey, String owner, String name, String data, long registered, byte[] reference) {
this(registrantPublicKey, owner, name, data, registered, null, reference, false, null);
}
// Getters / setters
public byte[] getRegistrantPublicKey() {
return this.registrantPublicKey;
}
public String getOwner() {
return this.owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getName() {
return this.name;
}
public String getData() {
return this.data;
}
public void setData(String data) {
this.data = data;
}
public long getRegistered() {
return this.registered;
}
public Long getUpdated() {
return this.updated;
}
public void setUpdated(Long updated) {
this.updated = updated;
}
public byte[] getReference() {
return this.reference;
}
public void setReference(byte[] reference) {
this.reference = reference;
}
public boolean getIsForSale() {
return this.isForSale;
}
public void setIsForSale(boolean isForSale) {
this.isForSale = isForSale;
}
public BigDecimal getSalePrice() {
return this.salePrice;
}
public void setSalePrice(BigDecimal salePrice) {
this.salePrice = salePrice;
}
}

View File

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

View File

@@ -0,0 +1,69 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.account.GenesisAccount;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class ATTransactionData extends TransactionData {
// Properties
private String atAddress;
private String recipient;
private BigDecimal amount;
private Long assetId;
private byte[] message;
// Constructors
// For JAX-RS
protected ATTransactionData() {
}
public ATTransactionData(String atAddress, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp,
byte[] reference, byte[] signature) {
super(TransactionType.AT, fee, GenesisAccount.PUBLIC_KEY, timestamp, reference, signature);
this.atAddress = atAddress;
this.recipient = recipient;
this.amount = amount;
this.assetId = assetId;
this.message = message;
}
public ATTransactionData(String atAddress, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp,
byte[] reference) {
this(atAddress, recipient, amount, assetId, message, fee, timestamp, reference, null);
}
// Getters/Setters
public String getATAddress() {
return this.atAddress;
}
public String getRecipient() {
return this.recipient;
}
public BigDecimal getAmount() {
return this.amount;
}
public Long getAssetId() {
return this.assetId;
}
public byte[] getMessage() {
return this.message;
}
}

View File

@@ -0,0 +1,104 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.data.PaymentData;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class ArbitraryTransactionData extends TransactionData {
// "data" field types
public enum DataType {
RAW_DATA,
DATA_HASH;
}
// Properties
private int version;
private byte[] senderPublicKey;
private int service;
private byte[] data;
private DataType dataType;
private List<PaymentData> payments;
// Constructors
// For JAX-RS
protected ArbitraryTransactionData() {
}
/** Reconstructing a V3 arbitrary transaction with signature */
public ArbitraryTransactionData(int version, byte[] senderPublicKey, int service, byte[] data, DataType dataType, List<PaymentData> payments,
BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(TransactionType.ARBITRARY, fee, senderPublicKey, timestamp, reference, signature);
this.version = version;
this.senderPublicKey = senderPublicKey;
this.service = service;
this.data = data;
this.dataType = dataType;
this.payments = payments;
}
/** Constructing a new V3 arbitrary transaction without signature */
public ArbitraryTransactionData(int version, byte[] senderPublicKey, int service, byte[] data, DataType dataType, List<PaymentData> payments,
BigDecimal fee, long timestamp, byte[] reference) {
this(version, senderPublicKey, service, data, dataType, payments, fee, timestamp, reference, null);
}
/** Reconstructing a V1 arbitrary transaction with signature */
public ArbitraryTransactionData(int version, byte[] senderPublicKey, int service, byte[] data, DataType dataType, BigDecimal fee, long timestamp,
byte[] reference, byte[] signature) {
this(version, senderPublicKey, service, data, dataType, null, fee, timestamp, reference, signature);
}
/** Constructing a new V1 arbitrary transaction without signature */
public ArbitraryTransactionData(int version, byte[] senderPublicKey, int service, byte[] data, DataType dataType, BigDecimal fee, long timestamp,
byte[] reference) {
this(version, senderPublicKey, service, data, dataType, null, fee, timestamp, reference, null);
}
// Getters/Setters
public int getVersion() {
return this.version;
}
public byte[] getSenderPublicKey() {
return this.senderPublicKey;
}
public int getService() {
return this.service;
}
public byte[] getData() {
return this.data;
}
public void setData(byte[] data) {
this.data = data;
}
public DataType getDataType() {
return this.dataType;
}
public void setDataType(DataType dataType) {
this.dataType = dataType;
}
public List<PaymentData> getPayments() {
return this.payments;
}
}

View File

@@ -0,0 +1,81 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class BuyNameTransactionData extends TransactionData {
// Properties
private byte[] buyerPublicKey;
private String name;
private BigDecimal amount;
private String seller;
private byte[] nameReference;
// Constructors
// For JAX-RS
protected BuyNameTransactionData() {
}
public BuyNameTransactionData(byte[] buyerPublicKey, String name, BigDecimal amount, String seller, byte[] nameReference, BigDecimal fee, long timestamp,
byte[] reference, byte[] signature) {
super(TransactionType.BUY_NAME, fee, buyerPublicKey, timestamp, reference, signature);
this.buyerPublicKey = buyerPublicKey;
this.name = name;
this.amount = amount;
this.seller = seller;
this.nameReference = nameReference;
}
public BuyNameTransactionData(byte[] buyerPublicKey, String name, BigDecimal amount, String seller, BigDecimal fee, long timestamp, byte[] reference,
byte[] signature) {
this(buyerPublicKey, name, amount, seller, null, fee, timestamp, reference, signature);
}
public BuyNameTransactionData(byte[] buyerPublicKey, String name, BigDecimal amount, String seller, byte[] nameReference, BigDecimal fee, long timestamp,
byte[] reference) {
this(buyerPublicKey, name, amount, seller, nameReference, fee, timestamp, reference, null);
}
public BuyNameTransactionData(byte[] buyerPublicKey, String name, BigDecimal amount, String seller, BigDecimal fee, long timestamp, byte[] reference) {
this(buyerPublicKey, name, amount, seller, null, fee, timestamp, reference, null);
}
// Getters / setters
public byte[] getBuyerPublicKey() {
return this.buyerPublicKey;
}
public String getName() {
return this.name;
}
public BigDecimal getAmount() {
return this.amount;
}
public String getSeller() {
return this.seller;
}
public byte[] getNameReference() {
return this.nameReference;
}
public void setNameReference(byte[] nameReference) {
this.nameReference = nameReference;
}
}

View File

@@ -0,0 +1,42 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class CancelOrderTransactionData extends TransactionData {
// Properties
private byte[] orderId;
// Constructors
// For JAX-RS
protected CancelOrderTransactionData() {
}
public CancelOrderTransactionData(byte[] creatorPublicKey, byte[] orderId, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(Transaction.TransactionType.CANCEL_ASSET_ORDER, fee, creatorPublicKey, timestamp, reference, signature);
this.orderId = orderId;
}
public CancelOrderTransactionData(byte[] creatorPublicKey, byte[] orderId, BigDecimal fee, long timestamp, byte[] reference) {
this(creatorPublicKey, orderId, fee, timestamp, reference, null);
}
// Getters/Setters
public byte[] getOrderId() {
return this.orderId;
}
}

View File

@@ -0,0 +1,48 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class CancelSellNameTransactionData extends TransactionData {
// Properties
private byte[] ownerPublicKey;
private String name;
// Constructors
// For JAX-RS
protected CancelSellNameTransactionData() {
}
public CancelSellNameTransactionData(byte[] ownerPublicKey, String name, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(TransactionType.CANCEL_SELL_NAME, fee, ownerPublicKey, timestamp, reference, signature);
this.ownerPublicKey = ownerPublicKey;
this.name = name;
}
public CancelSellNameTransactionData(byte[] ownerPublicKey, String name, BigDecimal fee, long timestamp, byte[] reference) {
this(ownerPublicKey, name, fee, timestamp, reference, null);
}
// Getters / setters
public byte[] getOwnerPublicKey() {
return this.ownerPublicKey;
}
public String getName() {
return this.name;
}
}

View File

@@ -0,0 +1,62 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class CreateOrderTransactionData extends TransactionData {
// Properties
private long haveAssetId;
private long wantAssetId;
private BigDecimal amount;
private BigDecimal price;
// Constructors
// For JAX-RS
protected CreateOrderTransactionData() {
}
public CreateOrderTransactionData(byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, BigDecimal fee,
long timestamp, byte[] reference, byte[] signature) {
super(TransactionType.CREATE_ASSET_ORDER, fee, creatorPublicKey, timestamp, reference, signature);
this.haveAssetId = haveAssetId;
this.wantAssetId = wantAssetId;
this.amount = amount;
this.price = price;
}
public CreateOrderTransactionData(byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, BigDecimal fee,
long timestamp, byte[] reference) {
this(creatorPublicKey, haveAssetId, wantAssetId, amount, price, fee, timestamp, reference, null);
}
// Getters/Setters
public long getHaveAssetId() {
return this.haveAssetId;
}
public long getWantAssetId() {
return this.wantAssetId;
}
public BigDecimal getAmount() {
return this.amount;
}
public BigDecimal getPrice() {
return this.price;
}
}

View File

@@ -0,0 +1,64 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.data.voting.PollOptionData;
import org.qora.transaction.Transaction;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class CreatePollTransactionData extends TransactionData {
// Properties
private String owner;
private String pollName;
private String description;
private List<PollOptionData> pollOptions;
// Constructors
// For JAX-RS
protected CreatePollTransactionData() {
}
public CreatePollTransactionData(byte[] creatorPublicKey, String owner, String pollName, String description, List<PollOptionData> pollOptions,
BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(Transaction.TransactionType.CREATE_POLL, fee, creatorPublicKey, timestamp, reference, signature);
this.owner = owner;
this.pollName = pollName;
this.description = description;
this.pollOptions = pollOptions;
}
public CreatePollTransactionData(byte[] creatorPublicKey, String owner, String pollName, String description, List<PollOptionData> pollOptions,
BigDecimal fee, long timestamp, byte[] reference) {
this(creatorPublicKey, owner, pollName, description, pollOptions, fee, timestamp, reference, null);
}
// Getters/setters
public String getOwner() {
return this.owner;
}
public String getPollName() {
return this.pollName;
}
public String getDescription() {
return this.description;
}
public List<PollOptionData> getPollOptions() {
return this.pollOptions;
}
}

View File

@@ -0,0 +1,95 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class DeployATTransactionData extends TransactionData {
// Properties
private String name;
private String description;
private String ATType;
private String tags;
private byte[] creationBytes;
private BigDecimal amount;
private long assetId;
private String ATAddress;
// Constructors
// For JAX-RS
protected DeployATTransactionData() {
}
public DeployATTransactionData(String ATAddress, byte[] creatorPublicKey, String name, String description, String ATType, String tags, byte[] creationBytes,
BigDecimal amount, long assetId, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(TransactionType.DEPLOY_AT, fee, creatorPublicKey, timestamp, reference, signature);
this.name = name;
this.description = description;
this.ATType = ATType;
this.tags = tags;
this.amount = amount;
this.assetId = assetId;
this.creationBytes = creationBytes;
this.ATAddress = ATAddress;
}
public DeployATTransactionData(byte[] creatorPublicKey, String name, String description, String ATType, String tags, byte[] creationBytes,
BigDecimal amount, long assetId, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
this(null, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, assetId, fee, timestamp, reference, signature);
}
public DeployATTransactionData(byte[] creatorPublicKey, String name, String description, String ATType, String tags, byte[] creationBytes,
BigDecimal amount, long assetId, BigDecimal fee, long timestamp, byte[] reference) {
this(null, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, assetId, fee, timestamp, reference, null);
}
// Getters/Setters
public String getName() {
return this.name;
}
public String getDescription() {
return this.description;
}
public String getATType() {
return this.ATType;
}
public String getTags() {
return this.tags;
}
public byte[] getCreationBytes() {
return this.creationBytes;
}
public BigDecimal getAmount() {
return this.amount;
}
public long getAssetId() {
return this.assetId;
}
public String getATAddress() {
return this.ATAddress;
}
public void setATAddress(String ATAddress) {
this.ATAddress = ATAddress;
}
}

View File

@@ -0,0 +1,69 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.account.GenesisAccount;
import org.qora.asset.Asset;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(
allOf = {
TransactionData.class
}
)
public class GenesisTransactionData extends TransactionData {
// Properties
private String recipient;
private BigDecimal amount;
private long assetId;
// Constructors
// For JAX-RS
protected GenesisTransactionData() {
}
public GenesisTransactionData(String recipient, BigDecimal amount, long assetId, long timestamp, byte[] signature) {
// Zero fee
super(TransactionType.GENESIS, BigDecimal.ZERO, GenesisAccount.PUBLIC_KEY, timestamp, null, signature);
this.recipient = recipient;
this.amount = amount;
this.assetId = assetId;
}
public GenesisTransactionData(String recipient, BigDecimal amount, long timestamp, byte[] signature) {
this(recipient, amount, Asset.QORA, timestamp, signature);
}
public GenesisTransactionData(String recipient, BigDecimal amount, long assetId, long timestamp) {
this(recipient, amount, assetId, timestamp, null);
}
public GenesisTransactionData(String recipient, BigDecimal amount, long timestamp) {
this(recipient, amount, Asset.QORA, timestamp, null);
}
// Getters/Setters
public String getRecipient() {
return this.recipient;
}
public BigDecimal getAmount() {
return this.amount;
}
public long getAssetId() {
return this.assetId;
}
}

View File

@@ -0,0 +1,97 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
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;
private String assetName;
private String description;
private long quantity;
private boolean isDivisible;
// Constructors
// For JAX-RS
protected IssueAssetTransactionData() {
super(TransactionType.ISSUE_ASSET);
}
public IssueAssetTransactionData(Long assetId, byte[] issuerPublicKey, String owner, String assetName, String description, long quantity,
boolean isDivisible, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(TransactionType.ISSUE_ASSET, fee, issuerPublicKey, timestamp, reference, signature);
this.assetId = assetId;
this.issuerPublicKey = issuerPublicKey;
this.owner = owner;
this.assetName = assetName;
this.description = description;
this.quantity = quantity;
this.isDivisible = isDivisible;
}
public IssueAssetTransactionData(byte[] issuerPublicKey, String owner, String assetName, String description, long quantity, boolean isDivisible,
BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
this(null, issuerPublicKey, owner, assetName, description, quantity, isDivisible, fee, timestamp, reference, signature);
}
public IssueAssetTransactionData(byte[] issuerPublicKey, String owner, String assetName, String description, long quantity, boolean isDivisible,
BigDecimal fee, long timestamp, byte[] reference) {
this(null, issuerPublicKey, owner, assetName, description, quantity, isDivisible, fee, timestamp, reference, null);
}
// Getters/Setters
public Long getAssetId() {
return this.assetId;
}
public void setAssetId(Long assetId) {
this.assetId = assetId;
}
public byte[] getIssuerPublicKey() {
return this.issuerPublicKey;
}
public String getOwner() {
return this.owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getAssetName() {
return this.assetName;
}
public String getDescription() {
return this.description;
}
public long getQuantity() {
return this.quantity;
}
public boolean getIsDivisible() {
return this.isDivisible;
}
}

View File

@@ -0,0 +1,92 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.asset.Asset;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class MessageTransactionData extends TransactionData {
// Properties
private int version;
private byte[] senderPublicKey;
private String recipient;
private Long assetId;
private BigDecimal amount;
private byte[] data;
private boolean isText;
private boolean isEncrypted;
// Constructors
// For JAX-RS
protected MessageTransactionData() {
}
public MessageTransactionData(int version, byte[] senderPublicKey, String recipient, Long assetId, BigDecimal amount, byte[] data, boolean isText,
boolean isEncrypted, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(TransactionType.MESSAGE, fee, senderPublicKey, timestamp, reference, signature);
this.version = version;
this.senderPublicKey = senderPublicKey;
this.recipient = recipient;
if (assetId != null)
this.assetId = assetId;
else
this.assetId = Asset.QORA;
this.amount = amount;
this.data = data;
this.isText = isText;
this.isEncrypted = isEncrypted;
}
public MessageTransactionData(int version, byte[] senderPublicKey, String recipient, Long assetId, BigDecimal amount, byte[] data, boolean isText,
boolean isEncrypted, BigDecimal fee, long timestamp, byte[] reference) {
this(version, senderPublicKey, recipient, assetId, amount, data, isText, isEncrypted, fee, timestamp, reference, null);
}
// Getters/Setters
public int getVersion() {
return this.version;
}
public byte[] getSenderPublicKey() {
return this.senderPublicKey;
}
public String getRecipient() {
return this.recipient;
}
public Long getAssetId() {
return this.assetId;
}
public BigDecimal getAmount() {
return this.amount;
}
public byte[] getData() {
return this.data;
}
public boolean getIsText() {
return this.isText;
}
public boolean getIsEncrypted() {
return this.isEncrypted;
}
}

View File

@@ -0,0 +1,50 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.data.PaymentData;
import org.qora.transaction.Transaction;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class MultiPaymentTransactionData extends TransactionData {
// Properties
private byte[] senderPublicKey;
private List<PaymentData> payments;
// Constructors
// For JAX-RS
protected MultiPaymentTransactionData() {
}
public MultiPaymentTransactionData(byte[] senderPublicKey, List<PaymentData> payments, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(Transaction.TransactionType.MULTIPAYMENT, fee, senderPublicKey, timestamp, reference, signature);
this.senderPublicKey = senderPublicKey;
this.payments = payments;
}
public MultiPaymentTransactionData(byte[] senderPublicKey, List<PaymentData> payments, BigDecimal fee, long timestamp, byte[] reference) {
this(senderPublicKey, payments, fee, timestamp, reference, null);
}
// Getters/setters
public byte[] getSenderPublicKey() {
return this.senderPublicKey;
}
public List<PaymentData> getPayments() {
return this.payments;
}
}

View File

@@ -0,0 +1,64 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema( allOf = { TransactionData.class } )
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 = org.qora.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,
byte[] signature) {
super(TransactionType.PAYMENT, fee, senderPublicKey, timestamp, reference, signature);
this.senderPublicKey = senderPublicKey;
this.recipient = recipient;
this.amount = amount;
}
public PaymentTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference) {
this(senderPublicKey, recipient, amount, fee, timestamp, reference, null);
}
// Getters/Setters
public byte[] getSenderPublicKey() {
return this.senderPublicKey;
}
public String getRecipient() {
return this.recipient;
}
public BigDecimal getAmount() {
return this.amount;
}
}

View File

@@ -0,0 +1,66 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
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")
private String owner;
@Schema(description = "requested name", example = "my-name")
private String name;
@Schema(description = "simple name-related info in JSON format", example = "{ \"age\": 30 }")
private String data;
// Constructors
// For JAX-RS
protected RegisterNameTransactionData() {
super(TransactionType.REGISTER_NAME);
}
public RegisterNameTransactionData(byte[] registrantPublicKey, String owner, String name, String data, BigDecimal fee, long timestamp, byte[] reference,
byte[] signature) {
super(TransactionType.REGISTER_NAME, fee, registrantPublicKey, timestamp, reference, signature);
this.registrantPublicKey = registrantPublicKey;
this.owner = owner;
this.name = name;
this.data = data;
}
public RegisterNameTransactionData(byte[] registrantPublicKey, String owner, String name, String data, BigDecimal fee, long timestamp, byte[] reference) {
this(registrantPublicKey, owner, name, data, fee, timestamp, reference, null);
}
// Getters / setters
public byte[] getRegistrantPublicKey() {
return this.registrantPublicKey;
}
public String getOwner() {
return this.owner;
}
public String getName() {
return this.name;
}
public String getData() {
return this.data;
}
}

View File

@@ -0,0 +1,54 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class SellNameTransactionData extends TransactionData {
// Properties
private byte[] ownerPublicKey;
private String name;
private BigDecimal amount;
// Constructors
// For JAX-RS
protected SellNameTransactionData() {
}
public SellNameTransactionData(byte[] ownerPublicKey, String name, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(TransactionType.SELL_NAME, fee, ownerPublicKey, timestamp, reference, signature);
this.ownerPublicKey = ownerPublicKey;
this.name = name;
this.amount = amount;
}
public SellNameTransactionData(byte[] ownerPublicKey, String name, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference) {
this(ownerPublicKey, name, amount, fee, timestamp, reference, null);
}
// Getters / setters
public byte[] getOwnerPublicKey() {
return this.ownerPublicKey;
}
public String getName() {
return this.name;
}
public BigDecimal getAmount() {
return this.amount;
}
}

View File

@@ -0,0 +1,150 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import org.eclipse.persistence.oxm.annotations.XmlClassExtractor;
import org.qora.api.TransactionClassExtractor;
import org.qora.crypto.Crypto;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
/*
* If you encounter an error like:
*
* MessageBodyWriter not found for <some class>
*
* then chances are that class is missing a no-argument constructor!
*/
@XmlClassExtractor(TransactionClassExtractor.class)
@XmlSeeAlso({ArbitraryTransactionData.class, ATTransactionData.class, BuyNameTransactionData.class, CancelOrderTransactionData.class, CancelSellNameTransactionData.class,
CreateOrderTransactionData.class, CreatePollTransactionData.class, DeployATTransactionData.class, GenesisTransactionData.class, IssueAssetTransactionData.class,
MessageTransactionData.class, MultiPaymentTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, SellNameTransactionData.class,
TransferAssetTransactionData.class, UpdateNameTransactionData.class, VoteOnPollTransactionData.class})
//All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public abstract class TransactionData {
// Properties shared with all transaction types
@Schema(accessMode = AccessMode.READ_ONLY, hidden = true)
protected TransactionType type;
@XmlTransient // represented in transaction-specific properties
@Schema(hidden = true)
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")
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")
protected byte[] signature;
// Constructors
// For JAX-RS
protected TransactionData() {
}
// For JAX-RS
protected TransactionData(TransactionType type) {
this.type = type;
}
public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference, byte[] signature) {
this.fee = fee;
this.type = type;
this.creatorPublicKey = creatorPublicKey;
this.timestamp = timestamp;
this.reference = reference;
this.signature = signature;
}
public TransactionData(TransactionType type, BigDecimal fee, byte[] creatorPublicKey, long timestamp, byte[] reference) {
this(type, fee, creatorPublicKey, timestamp, reference, null);
}
// Getters/setters
public TransactionType getType() {
return this.type;
}
public byte[] getCreatorPublicKey() {
return this.creatorPublicKey;
}
public long getTimestamp() {
return this.timestamp;
}
public byte[] getReference() {
return this.reference;
}
public BigDecimal getFee() {
return this.fee;
}
public byte[] getSignature() {
return this.signature;
}
public void setSignature(byte[] signature) {
this.signature = signature;
}
// JAXB special
@XmlElement(name = "creatorAddress")
protected String getCreatorAddress() {
return Crypto.toAddress(this.creatorPublicKey);
}
@XmlTransient
public void setCreatorPublicKey(byte[] creatorPublicKey) {
this.creatorPublicKey = creatorPublicKey;
}
// Comparison
@Override
public int hashCode() {
byte[] bytes = this.signature;
// No signature? Use reference instead
if (bytes == null)
bytes = this.reference;
return new BigInteger(bytes).intValue();
}
@Override
public boolean equals(Object other) {
// If we don't have a signature then fail
if (this.signature == null)
return false;
if (!(other instanceof TransactionData))
return false;
TransactionData otherTransactionData = (TransactionData) other;
// If other transactionData has no signature then fail
if (otherTransactionData.signature == null)
return false;
return Arrays.equals(this.signature, otherTransactionData.signature);
}
}

View File

@@ -0,0 +1,62 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class TransferAssetTransactionData extends TransactionData {
// Properties
private byte[] senderPublicKey;
private String recipient;
private BigDecimal amount;
private long assetId;
// Constructors
// For JAX-RS
protected TransferAssetTransactionData() {
}
public TransferAssetTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, long assetId, BigDecimal fee, long timestamp,
byte[] reference, byte[] signature) {
super(TransactionType.TRANSFER_ASSET, fee, senderPublicKey, timestamp, reference, signature);
this.senderPublicKey = senderPublicKey;
this.recipient = recipient;
this.amount = amount;
this.assetId = assetId;
}
public TransferAssetTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, long assetId, BigDecimal fee, long timestamp,
byte[] reference) {
this(senderPublicKey, recipient, amount, assetId, fee, timestamp, reference, null);
}
// Getters/setters
public byte[] getSenderPublicKey() {
return this.senderPublicKey;
}
public String getRecipient() {
return this.recipient;
}
public BigDecimal getAmount() {
return this.amount;
}
public long getAssetId() {
return this.assetId;
}
}

View File

@@ -0,0 +1,77 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class UpdateNameTransactionData extends TransactionData {
// Properties
private byte[] ownerPublicKey;
private String newOwner;
private String name;
private String newData;
private byte[] nameReference;
// Constructors
// For JAX-RS
protected UpdateNameTransactionData() {
}
public UpdateNameTransactionData(byte[] ownerPublicKey, String newOwner, String name, String newData, byte[] nameReference, BigDecimal fee, long timestamp,
byte[] reference, byte[] signature) {
super(TransactionType.UPDATE_NAME, fee, ownerPublicKey, timestamp, reference, signature);
this.ownerPublicKey = ownerPublicKey;
this.newOwner = newOwner;
this.name = name;
this.newData = newData;
this.nameReference = nameReference;
}
public UpdateNameTransactionData(byte[] ownerPublicKey, String newOwner, String name, String newData, BigDecimal fee, long timestamp, byte[] reference,
byte[] signature) {
this(ownerPublicKey, newOwner, name, newData, null, fee, timestamp, reference, signature);
}
public UpdateNameTransactionData(byte[] ownerPublicKey, String newOwner, String name, String newData, byte[] nameReference, BigDecimal fee, long timestamp,
byte[] reference) {
this(ownerPublicKey, newOwner, name, newData, nameReference, fee, timestamp, reference, null);
}
// Getters / setters
public byte[] getOwnerPublicKey() {
return this.ownerPublicKey;
}
public String getNewOwner() {
return this.newOwner;
}
public String getName() {
return this.name;
}
public String getNewData() {
return this.newData;
}
public byte[] getNameReference() {
return this.nameReference;
}
public void setNameReference(byte[] nameReference) {
this.nameReference = nameReference;
}
}

View File

@@ -0,0 +1,70 @@
package org.qora.data.transaction;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class VoteOnPollTransactionData extends TransactionData {
// Properties
private byte[] voterPublicKey;
private String pollName;
private int optionIndex;
private Integer previousOptionIndex;
// Constructors
// For JAX-RS
protected VoteOnPollTransactionData() {
}
public VoteOnPollTransactionData(byte[] voterPublicKey, String pollName, int optionIndex, Integer previousOptionIndex, BigDecimal fee, long timestamp,
byte[] reference, byte[] signature) {
super(TransactionType.VOTE_ON_POLL, fee, voterPublicKey, timestamp, reference, signature);
this.voterPublicKey = voterPublicKey;
this.pollName = pollName;
this.optionIndex = optionIndex;
this.previousOptionIndex = previousOptionIndex;
}
public VoteOnPollTransactionData(byte[] voterPublicKey, String pollName, int optionIndex, BigDecimal fee, long timestamp, byte[] reference,
byte[] signature) {
this(voterPublicKey, pollName, optionIndex, null, fee, timestamp, reference, signature);
}
public VoteOnPollTransactionData(byte[] voterPublicKey, String pollName, int optionIndex, BigDecimal fee, long timestamp, byte[] reference) {
this(voterPublicKey, pollName, optionIndex, null, fee, timestamp, reference, null);
}
// Getters / setters
public byte[] getVoterPublicKey() {
return this.voterPublicKey;
}
public String getPollName() {
return this.pollName;
}
public int getOptionIndex() {
return this.optionIndex;
}
public Integer getPreviousOptionIndex() {
return this.previousOptionIndex;
}
public void setPreviousOptionIndex(Integer previousOptionIndex) {
this.previousOptionIndex = previousOptionIndex;
}
}

View File

@@ -0,0 +1,58 @@
package org.qora.data.voting;
import java.util.List;
import org.qora.data.voting.PollOptionData;
public class PollData {
// Properties
private byte[] creatorPublicKey;
private String owner;
private String pollName;
private String description;
private List<PollOptionData> pollOptions;
private long published;
// Constructors
public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List<PollOptionData> pollOptions, long published) {
this.creatorPublicKey = creatorPublicKey;
this.owner = owner;
this.pollName = pollName;
this.description = description;
this.pollOptions = pollOptions;
this.published = published;
}
// Getters/setters
public byte[] getCreatorPublicKey() {
return this.creatorPublicKey;
}
public String getOwner() {
return this.owner;
}
public String getPollName() {
return this.pollName;
}
public String getDescription() {
return this.description;
}
public List<PollOptionData> getPollOptions() {
return this.pollOptions;
}
public long getPublished() {
return this.published;
}
public void setPublished(long published) {
this.published = published;
}
}

View File

@@ -0,0 +1,29 @@
package org.qora.data.voting;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
//All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class PollOptionData {
// Properties
private String optionName;
// Constructors
// For JAX-RS
protected PollOptionData() {
}
public PollOptionData(String optionName) {
this.optionName = optionName;
}
// Getters/setters
public String getOptionName() {
return this.optionName;
}
}

View File

@@ -0,0 +1,32 @@
package org.qora.data.voting;
public class VoteOnPollData {
// Properties
private String pollName;
private byte[] voterPublicKey;
private int optionIndex;
// Constructors
public VoteOnPollData(String pollName, byte[] voterPublicKey, int optionIndex) {
this.pollName = pollName;
this.voterPublicKey = voterPublicKey;
this.optionIndex = optionIndex;
}
// Getters/setters
public String getPollName() {
return this.pollName;
}
public byte[] getVoterPublicKey() {
return this.voterPublicKey;
}
public int getOptionIndex() {
return this.optionIndex;
}
}

View File

@@ -0,0 +1,52 @@
package org.qora.globalization;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Providing multi-language BIP39 word lists, downloaded from https://github.com/bitcoin/bips/tree/master/bip-0039 */
public enum BIP39WordList {
INSTANCE;
private Logger LOGGER = LogManager.getLogger(BIP39WordList.class);
private Map<String, List<String>> wordListsByLang;
private BIP39WordList() {
wordListsByLang = new HashMap<>();
}
public synchronized List<String> getByLang(String lang) {
List<String> wordList = wordListsByLang.get(lang);
if (wordList == null) {
ClassLoader loader = this.getClass().getClassLoader();
try (InputStream inputStream = loader.getResourceAsStream("BIP39/wordlist_" + lang + ".txt")) {
if (inputStream == null) {
LOGGER.warn("Can't locate '" + lang + "' BIP39 wordlist");
return null;
}
wordList = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.toList());
} catch (IOException e) {
LOGGER.warn("Error reading '" + lang + "' BIP39 wordlist", e);
return null;
}
wordListsByLang.put(lang, wordList);
}
return Collections.unmodifiableList(wordList);
}
}

View File

@@ -0,0 +1,58 @@
package org.qora.globalization;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingFormatArgumentException;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public enum Translator {
INSTANCE;
private final Logger LOGGER = LogManager.getLogger(Translator.class);
private final String DEFAULT_LANG = Locale.getDefault().getLanguage();
private final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
private synchronized ResourceBundle getOrLoadResourceBundle(String className, String lang) {
final String bundleKey = className + ":" + lang;
ResourceBundle resourceBundle = resourceBundles.get(bundleKey);
if (resourceBundle != null)
return resourceBundle;
try {
resourceBundle = ResourceBundle.getBundle("i18n." + className, Locale.forLanguageTag(lang));
} catch (MissingResourceException e) {
LOGGER.warn("Can't locate '" + lang + "' translation resource bundle for " + className, e);
return null;
}
resourceBundles.put(bundleKey, resourceBundle);
return resourceBundle;
}
public String translate(final String className, final String key) {
return this.translate(className, DEFAULT_LANG, key);
}
public String translate(final String className, final String lang, final String key, final Object... args) {
ResourceBundle resourceBundle = getOrLoadResourceBundle(className, lang);
if (resourceBundle == null || !resourceBundle.containsKey(key))
return "!!" + lang + ":" + className + "." + key + "!!";
String template = resourceBundle.getString(key);
try {
return String.format(template, args);
} catch (MissingFormatArgumentException e) {
return template;
}
}
}

View File

@@ -0,0 +1,198 @@
package org.qora.naming;
import org.qora.account.Account;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.data.naming.NameData;
import org.qora.data.transaction.BuyNameTransactionData;
import org.qora.data.transaction.CancelSellNameTransactionData;
import org.qora.data.transaction.RegisterNameTransactionData;
import org.qora.data.transaction.SellNameTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.data.transaction.UpdateNameTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
public class Name {
// Properties
private Repository repository;
private NameData nameData;
// Useful constants
public static final int MAX_NAME_SIZE = 400;
public static final int MAX_DATA_SIZE = 4000;
// Constructors
/**
* Construct Name business object using info from register name transaction.
*
* @param repository
* @param registerNameTransactionData
*/
public Name(Repository repository, RegisterNameTransactionData registerNameTransactionData) {
this.repository = repository;
this.nameData = new NameData(registerNameTransactionData.getRegistrantPublicKey(), registerNameTransactionData.getOwner(),
registerNameTransactionData.getName(), registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(),
registerNameTransactionData.getSignature());
}
/**
* Construct Name business object using existing name in repository.
*
* @param repository
* @param name
* @throws DataException
*/
public Name(Repository repository, String name) throws DataException {
this.repository = repository;
this.nameData = this.repository.getNameRepository().fromName(name);
}
// Processing
public void register() throws DataException {
this.repository.getNameRepository().save(this.nameData);
}
public void unregister() throws DataException {
this.repository.getNameRepository().delete(this.nameData.getName());
}
private void revert() throws DataException {
TransactionData previousTransactionData = this.repository.getTransactionRepository().fromSignature(this.nameData.getReference());
if (previousTransactionData == null)
throw new DataException("Unable to revert name transaction as referenced transaction not found in repository");
switch (previousTransactionData.getType()) {
case REGISTER_NAME:
RegisterNameTransactionData previousRegisterNameTransactionData = (RegisterNameTransactionData) previousTransactionData;
this.nameData.setOwner(previousRegisterNameTransactionData.getOwner());
this.nameData.setData(previousRegisterNameTransactionData.getData());
break;
case UPDATE_NAME:
UpdateNameTransactionData previousUpdateNameTransactionData = (UpdateNameTransactionData) previousTransactionData;
this.nameData.setData(previousUpdateNameTransactionData.getNewData());
this.nameData.setOwner(previousUpdateNameTransactionData.getNewOwner());
break;
case BUY_NAME:
BuyNameTransactionData previousBuyNameTransactionData = (BuyNameTransactionData) previousTransactionData;
Account buyer = new PublicKeyAccount(this.repository, previousBuyNameTransactionData.getBuyerPublicKey());
this.nameData.setOwner(buyer.getAddress());
break;
default:
throw new IllegalStateException("Unable to revert name transaction due to unsupported referenced transaction");
}
}
public void update(UpdateNameTransactionData updateNameTransactionData) throws DataException {
// Update reference in transaction data
updateNameTransactionData.setNameReference(this.nameData.getReference());
// New name reference is this transaction's signature
this.nameData.setReference(updateNameTransactionData.getSignature());
// Update Name's owner and data
this.nameData.setOwner(updateNameTransactionData.getNewOwner());
this.nameData.setData(updateNameTransactionData.getNewData());
// Save updated name data
this.repository.getNameRepository().save(this.nameData);
}
public void revert(UpdateNameTransactionData updateNameTransactionData) throws DataException {
// Previous name reference is taken from this transaction's cached copy
this.nameData.setReference(updateNameTransactionData.getNameReference());
// Previous Name's owner and/or data taken from referenced transaction
this.revert();
// Save reverted name data
this.repository.getNameRepository().save(this.nameData);
}
public void sell(SellNameTransactionData sellNameTransactionData) throws DataException {
// Mark as for-sale and set price
this.nameData.setIsForSale(true);
this.nameData.setSalePrice(sellNameTransactionData.getAmount());
// Save sale info into repository
this.repository.getNameRepository().save(this.nameData);
}
public void unsell(SellNameTransactionData sellNameTransactionData) throws DataException {
// Mark not for-sale and unset price
this.nameData.setIsForSale(false);
this.nameData.setSalePrice(null);
// Save no-sale info into repository
this.repository.getNameRepository().save(this.nameData);
}
public void sell(CancelSellNameTransactionData cancelSellNameTransactionData) throws DataException {
// Mark not for-sale but leave price in case we want to orphan
this.nameData.setIsForSale(false);
// Save sale info into repository
this.repository.getNameRepository().save(this.nameData);
}
public void unsell(CancelSellNameTransactionData cancelSellNameTransactionData) throws DataException {
// Mark as for-sale using existing price
this.nameData.setIsForSale(true);
// Save no-sale info into repository
this.repository.getNameRepository().save(this.nameData);
}
public void buy(BuyNameTransactionData buyNameTransactionData) throws DataException {
// Mark not for-sale but leave price in case we want to orphan
this.nameData.setIsForSale(false);
// Update seller's balance
Account seller = new Account(this.repository, this.nameData.getOwner());
seller.setConfirmedBalance(Asset.QORA, seller.getConfirmedBalance(Asset.QORA).add(buyNameTransactionData.getAmount()));
// Set new owner
Account buyer = new PublicKeyAccount(this.repository, buyNameTransactionData.getBuyerPublicKey());
this.nameData.setOwner(buyer.getAddress());
// Update buyer's balance
buyer.setConfirmedBalance(Asset.QORA, buyer.getConfirmedBalance(Asset.QORA).subtract(buyNameTransactionData.getAmount()));
// Update reference in transaction data
buyNameTransactionData.setNameReference(this.nameData.getReference());
// New name reference is this transaction's signature
this.nameData.setReference(buyNameTransactionData.getSignature());
// Save updated name data
this.repository.getNameRepository().save(this.nameData);
}
public void unbuy(BuyNameTransactionData buyNameTransactionData) throws DataException {
// Mark as for-sale using existing price
this.nameData.setIsForSale(true);
// Previous name reference is taken from this transaction's cached copy
this.nameData.setReference(buyNameTransactionData.getNameReference());
// Revert buyer's balance
Account buyer = new PublicKeyAccount(this.repository, buyNameTransactionData.getBuyerPublicKey());
buyer.setConfirmedBalance(Asset.QORA, buyer.getConfirmedBalance(Asset.QORA).add(buyNameTransactionData.getAmount()));
// Previous Name's owner and/or data taken from referenced transaction
this.revert();
// Revert seller's balance
Account seller = new Account(this.repository, this.nameData.getOwner());
seller.setConfirmedBalance(Asset.QORA, seller.getConfirmedBalance(Asset.QORA).subtract(buyNameTransactionData.getAmount()));
// Save reverted name data
this.repository.getNameRepository().save(this.nameData);
}
}

View File

@@ -0,0 +1,57 @@
package org.qora;
import org.qora.block.Block;
import org.qora.block.BlockChain;
import org.qora.controller.Controller;
import org.qora.data.block.BlockData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryFactory;
import org.qora.repository.RepositoryManager;
import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
public class orphan {
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("usage: orphan <new-blockchain-tip-height>");
System.exit(1);
}
int targetHeight = Integer.parseInt(args[0]);
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
System.err.println("Couldn't connect to repository: " + e.getMessage());
System.exit(2);
}
try {
BlockChain.validate();
} catch (DataException e) {
System.err.println("Couldn't validate repository: " + e.getMessage());
System.exit(2);
}
try (final Repository repository = RepositoryManager.getRepository()) {
for (int height = repository.getBlockRepository().getBlockchainHeight(); height > targetHeight; --height) {
System.out.println("Orphaning block " + height);
BlockData blockData = repository.getBlockRepository().fromHeight(height);
Block block = new Block(repository, blockData);
block.orphan();
repository.saveChanges();
}
} catch (DataException e) {
e.printStackTrace();
}
try {
RepositoryManager.closeRepositoryFactory();
} catch (DataException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,177 @@
package org.qora.payment;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.qora.account.Account;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.crypto.Crypto;
import org.qora.data.PaymentData;
import org.qora.data.asset.AssetData;
import org.qora.data.at.ATData;
import org.qora.repository.AssetRepository;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.transaction.Transaction.ValidationResult;
public class Payment {
// Properties
private Repository repository;
// Constructors
public Payment(Repository repository) {
this.repository = repository;
}
// Processing
// Validate multiple payments
public ValidationResult isValid(byte[] senderPublicKey, List<PaymentData> payments, BigDecimal fee, boolean isZeroAmountValid) throws DataException {
AssetRepository assetRepository = this.repository.getAssetRepository();
// Check fee is positive
if (fee.compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_FEE;
// Total up payment amounts by assetId
Map<Long, BigDecimal> amountsByAssetId = new HashMap<Long, BigDecimal>();
// Add transaction fee to start with
amountsByAssetId.put(Asset.QORA, fee);
// Check payments, and calculate amount total by assetId
for (PaymentData paymentData : payments) {
// Check amount is zero or positive
if (paymentData.getAmount().compareTo(BigDecimal.ZERO) < 0)
return ValidationResult.NEGATIVE_AMOUNT;
// Optional zero-amount check
if (!isZeroAmountValid && paymentData.getAmount().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_AMOUNT;
// Check recipient address is valid
if (!Crypto.isValidAddress(paymentData.getRecipient()))
return ValidationResult.INVALID_ADDRESS;
// Do not allow payments to finished/dead ATs
ATData atData = this.repository.getATRepository().fromATAddress(paymentData.getRecipient());
if (atData != null && atData.getIsFinished())
return ValidationResult.AT_IS_FINISHED;
AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId());
// Check asset even exists
if (assetData == null)
return ValidationResult.ASSET_DOES_NOT_EXIST;
// If we're sending to an AT then assetId must match AT's assetId
if (atData != null && atData.getAssetId() != paymentData.getAssetId())
return ValidationResult.ASSET_DOES_NOT_MATCH_AT;
// Check asset amount is integer if asset is not divisible
if (!assetData.getIsDivisible() && paymentData.getAmount().stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_AMOUNT;
// Set or add amount into amounts-by-asset map
amountsByAssetId.compute(paymentData.getAssetId(), (assetId, amount) -> amount == null ? amount : amount.add(paymentData.getAmount()));
}
// Check sender has enough of each asset
Account sender = new PublicKeyAccount(this.repository, senderPublicKey);
for (Entry<Long, BigDecimal> pair : amountsByAssetId.entrySet())
if (sender.getConfirmedBalance(pair.getKey()).compareTo(pair.getValue()) < 0)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
public ValidationResult isValid(byte[] senderPublicKey, List<PaymentData> payments, BigDecimal fee) throws DataException {
return isValid(senderPublicKey, payments, fee, false);
}
// Single payment forms
public ValidationResult isValid(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, boolean isZeroAmountValid) throws DataException {
return isValid(senderPublicKey, Collections.singletonList(paymentData), fee, isZeroAmountValid);
}
public ValidationResult isValid(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee) throws DataException {
return isValid(senderPublicKey, paymentData, fee, false);
}
public void process(byte[] senderPublicKey, List<PaymentData> payments, BigDecimal fee, byte[] signature, boolean alwaysInitializeRecipientReference)
throws DataException {
Account sender = new PublicKeyAccount(this.repository, senderPublicKey);
// Update sender's balance due to fee
sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).subtract(fee));
// Update sender's reference
sender.setLastReference(signature);
// Process all payments
for (PaymentData paymentData : payments) {
Account recipient = new Account(this.repository, paymentData.getRecipient());
long assetId = paymentData.getAssetId();
BigDecimal amount = paymentData.getAmount();
// Update sender's balance due to amount
sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount));
// Update recipient's balance
recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount));
// For QORA amounts only: if recipient has no reference yet, then this is their starting reference
if ((alwaysInitializeRecipientReference || assetId == Asset.QORA) && recipient.getLastReference() == null)
recipient.setLastReference(signature);
}
}
public void process(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature, boolean alwaysInitializeRecipientReference)
throws DataException {
process(senderPublicKey, Collections.singletonList(paymentData), fee, signature, alwaysInitializeRecipientReference);
}
public void orphan(byte[] senderPublicKey, List<PaymentData> payments, BigDecimal fee, byte[] signature, byte[] reference,
boolean alwaysUninitializeRecipientReference) throws DataException {
Account sender = new PublicKeyAccount(this.repository, senderPublicKey);
// Update sender's balance due to fee
sender.setConfirmedBalance(Asset.QORA, sender.getConfirmedBalance(Asset.QORA).add(fee));
// Update sender's reference
sender.setLastReference(reference);
// Orphan all payments
for (PaymentData paymentData : payments) {
Account recipient = new Account(this.repository, paymentData.getRecipient());
long assetId = paymentData.getAssetId();
BigDecimal amount = paymentData.getAmount();
// Update sender's balance due to amount
sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount));
// Update recipient's balance
recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount));
/*
* For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own
* (which would have changed their last reference) thus this is their first reference so remove it.
*/
if ((alwaysUninitializeRecipientReference || assetId == Asset.QORA) && Arrays.equals(recipient.getLastReference(), signature))
recipient.setLastReference(null);
}
}
public void orphan(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature, byte[] reference,
boolean alwaysUninitializeRecipientReference) throws DataException {
orphan(senderPublicKey, Collections.singletonList(paymentData), fee, signature, reference, alwaysUninitializeRecipientReference);
}
}

View File

@@ -0,0 +1,85 @@
package org.qora.repository;
import java.util.List;
import org.qora.data.at.ATData;
import org.qora.data.at.ATStateData;
public interface ATRepository {
// CIYAM AutomatedTransactions
/** Returns ATData using AT's address or null if none found */
public ATData fromATAddress(String atAddress) throws DataException;
/** Returns list of executable ATs, empty if none found */
public List<ATData> getAllExecutableATs() throws DataException;
/** Returns creation block height given AT's address or null if not found */
public Integer getATCreationBlockHeight(String atAddress) throws DataException;
/** Saves ATData into repository */
public void save(ATData atData) throws DataException;
/** Removes an AT from repository, including associated ATStateData */
public void delete(String atAddress) throws DataException;
// AT States
/**
* Returns ATStateData for an AT at given height.
*
* @param atAddress
* - AT's address
* @param height
* - block height
* @return ATStateData for AT at given height or null if none found
*/
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException;
/**
* Returns latest ATStateData for an AT.
* <p>
* As ATs don't necessarily run every block, this will return the <tt>ATStateData</tt> with the greatest height.
*
* @param atAddress
* - AT's address
* @return ATStateData for AT with greatest height or null if none found
*/
public ATStateData getLatestATState(String atAddress) throws DataException;
/**
* Returns all ATStateData for a given block height.
* <p>
* Unlike <tt>getATState</tt>, only returns ATStateData saved at the given height.
*
* @param height
* - block height
* @return list of ATStateData for given height, empty list if none found
* @throws DataException
*/
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException;
/**
* Save ATStateData into repository.
* <p>
* Note: Requires at least these <tt>ATStateData</tt> properties to be filled, or an <tt>IllegalArgumentException</tt> will be thrown:
* <p>
* <ul>
* <li><tt>creation</tt></li>
* <li><tt>stateHash</tt></li>
* <li><tt>height</tt></li>
* </ul>
*
* @param atStateData
* @throws IllegalArgumentException
*/
public void save(ATStateData atStateData) throws DataException;
/** Delete AT's state data at this height */
public void delete(String atAddress, int height) throws DataException;
/** Delete state data for all ATs at this height */
public void deleteATStates(int height) throws DataException;
}

View File

@@ -0,0 +1,32 @@
package org.qora.repository;
import java.util.List;
import org.qora.data.account.AccountBalanceData;
import org.qora.data.account.AccountData;
public interface AccountRepository {
// General account
public void create(AccountData accountData) throws DataException;
public AccountData getAccount(String address) throws DataException;
public void save(AccountData accountData) throws DataException;
public void delete(String address) throws DataException;
// Account balances
public AccountBalanceData getBalance(String address, long assetId) throws DataException;
public List<AccountBalanceData> getAllBalances(String address) throws DataException;
public List<AccountBalanceData> getAssetBalances(long assetId) throws DataException;
public void save(AccountBalanceData accountBalanceData) throws DataException;
public void delete(String address, long assetId) throws DataException;
}

View File

@@ -0,0 +1,50 @@
package org.qora.repository;
import java.util.List;
import org.qora.data.asset.AssetData;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.TradeData;
public interface AssetRepository {
// Assets
public AssetData fromAssetId(long assetId) throws DataException;
public AssetData fromAssetName(String assetName) throws DataException;
public boolean assetExists(long assetId) throws DataException;
public boolean assetExists(String assetName) throws DataException;
public List<AssetData> getAllAssets() throws DataException;
// For a list of asset holders, see AccountRepository.getAssetBalances
public void save(AssetData assetData) throws DataException;
public void delete(long assetId) throws DataException;
// Orders
public OrderData fromOrderId(byte[] orderId) throws DataException;
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId) throws DataException;
public void save(OrderData orderData) throws DataException;
public void delete(byte[] orderId) throws DataException;
// Trades
public List<TradeData> getTrades(long haveAssetId, long wantAssetId) throws DataException;
/** Returns TradeData for trades where orderId was involved, i.e. either initiating OR target order */
public List<TradeData> getOrdersTrades(byte[] orderId) throws DataException;
public void save(TradeData tradeData) throws DataException;
public void delete(TradeData tradeData) throws DataException;
}

View File

@@ -0,0 +1,116 @@
package org.qora.repository;
import java.util.List;
import org.qora.data.block.BlockData;
import org.qora.data.block.BlockTransactionData;
import org.qora.data.transaction.TransactionData;
public interface BlockRepository {
/**
* Returns BlockData from repository using block signature.
*
* @param signature
* @return block data, or null if not found in blockchain.
* @throws DataException
*/
public BlockData fromSignature(byte[] signature) throws DataException;
/**
* Returns BlockData from repository using block reference.
*
* @param reference
* @return block data, or null if not found in blockchain.
* @throws DataException
*/
public BlockData fromReference(byte[] reference) throws DataException;
/**
* Returns BlockData from repository using block height.
*
* @param height
* @return block data, or null if not found in blockchain.
* @throws DataException
*/
public BlockData fromHeight(int height) throws DataException;
/**
* Return height of block in blockchain using block's signature.
*
* @param signature
* @return height, or 0 if not found in blockchain.
* @throws DataException
*/
public int getHeightFromSignature(byte[] signature) throws DataException;
/**
* Return highest block height from repository.
*
* @return height, or 0 if there are no blocks in DB (not very likely).
*/
public int getBlockchainHeight() throws DataException;
/**
* Return highest block in blockchain.
*
* @return highest block's data
* @throws DataException
*/
public BlockData getLastBlock() throws DataException;
/**
* Returns block's transactions given block's signature.
* <p>
* This is typically used by Block.getTransactions() which uses lazy-loading of transactions.
*
* @param signature
* @return list of transactions, or null if block not found in blockchain.
* @throws DataException
*/
public List<TransactionData> getTransactionsFromSignature(byte[] signature) throws DataException;
/**
* Saves block into repository.
*
* @param blockData
* @throws DataException
*/
public void save(BlockData blockData) throws DataException;
/**
* Deletes block from repository.
*
* @param blockData
* @throws DataException
*/
public void delete(BlockData blockData) throws DataException;
/**
* Saves a block-transaction mapping into the repository.
* <p>
* This essentially links a transaction to a specific block.<br>
* Transactions cannot be mapped to more than one block, so attempts will result in a DataException.
* <p>
* Note: it is the responsibility of the caller to maintain contiguous "sequence" values
* for all transactions mapped to a block.
*
* @param blockTransactionData
* @throws DataException
*/
public void save(BlockTransactionData blockTransactionData) throws DataException;
/**
* Deletes a block-transaction mapping from the repository.
* <p>
* This essentially unlinks a transaction from a specific block.
* <p>
* Note: it is the responsibility of the caller to maintain contiguous "sequence" values
* for all transactions mapped to a block.
*
* @param blockTransactionData
* @throws DataException
*/
public void delete(BlockTransactionData blockTransactionData) throws DataException;
}

View File

@@ -0,0 +1,22 @@
package org.qora.repository;
public class DataException extends Exception {
private static final long serialVersionUID = -3963965667288257605L;
public DataException() {
}
public DataException(String message) {
super(message);
}
public DataException(String message, Throwable cause) {
super(message, cause);
}
public DataException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,15 @@
package org.qora.repository;
import org.qora.data.naming.NameData;
public interface NameRepository {
public NameData fromName(String name) throws DataException;
public boolean nameExists(String name) throws DataException;
public void save(NameData nameData) throws DataException;
public void delete(String name) throws DataException;
}

View File

@@ -0,0 +1,28 @@
package org.qora.repository;
public interface Repository extends AutoCloseable {
public ATRepository getATRepository();
public AccountRepository getAccountRepository();
public AssetRepository getAssetRepository();
public BlockRepository getBlockRepository();
public NameRepository getNameRepository();
public TransactionRepository getTransactionRepository();
public VotingRepository getVotingRepository();
public void saveChanges() throws DataException;
public void discardChanges() throws DataException;
@Override
public void close() throws DataException;
public void rebuild() throws DataException;
}

View File

@@ -0,0 +1,9 @@
package org.qora.repository;
public interface RepositoryFactory {
public Repository getRepository() throws DataException;
public void close() throws DataException;
}

Some files were not shown because too many files have changed in this diff Show More