package qora.transaction; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.json.simple.JSONObject; import com.google.common.primitives.Bytes; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; import database.DBSet; import qora.account.Account; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; import qora.assets.Asset; import qora.assets.Order; import qora.crypto.Crypto; public class CreateOrderTransaction extends Transaction { private static final int CREATOR_LENGTH = 32; private static final int HAVE_LENGTH = 8; private static final int WANT_LENGTH = 8; private static final int AMOUNT_LENGTH = 12; private static final int PRICE_LENGTH = 12; private static final int REFERENCE_LENGTH = 64; private static final int FEE_LENGTH = 8; private static final int SIGNATURE_LENGTH = 64; private static final int BASE_LENGTH = TIMESTAMP_LENGTH + REFERENCE_LENGTH + CREATOR_LENGTH + HAVE_LENGTH + WANT_LENGTH + AMOUNT_LENGTH + PRICE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH; private PublicKeyAccount creator; private Order order; public CreateOrderTransaction(PublicKeyAccount creator, long have, long want, BigDecimal amount, BigDecimal price, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { super(CREATE_ORDER_TRANSACTION, fee, timestamp, reference, signature); this.creator = creator; this.order = new Order(new BigInteger(this.signature), creator, have, want, amount, price, timestamp); } //GETTERS/SETTERS public Order getOrder() { return this.order; } //PARSE CONVERT public static Transaction Parse(byte[] data) throws Exception { //CHECK IF WE MATCH BLOCK LENGTH if(data.length < BASE_LENGTH) { throw new Exception("Data does not match block length"); } int position = 0; //READ TIMESTAMP byte[] timestampBytes = Arrays.copyOfRange(data, position, position + TIMESTAMP_LENGTH); long timestamp = Longs.fromByteArray(timestampBytes); position += TIMESTAMP_LENGTH; //READ REFERENCE byte[] reference = Arrays.copyOfRange(data, position, position + REFERENCE_LENGTH); position += REFERENCE_LENGTH; //READ CREATOR byte[] creatorBytes = Arrays.copyOfRange(data, position, position + CREATOR_LENGTH); PublicKeyAccount creator = new PublicKeyAccount(creatorBytes); position += CREATOR_LENGTH; //READ HAVE byte[] haveBytes = Arrays.copyOfRange(data, position, position + HAVE_LENGTH); long have = Longs.fromByteArray(haveBytes); position += HAVE_LENGTH; //READ WANT byte[] wantBytes = Arrays.copyOfRange(data, position, position + WANT_LENGTH); long want = Longs.fromByteArray(wantBytes); position += WANT_LENGTH; //READ AMOUNT byte[] amountBytes = Arrays.copyOfRange(data, position, position + AMOUNT_LENGTH); BigDecimal amount = new BigDecimal(new BigInteger(amountBytes), 8); position += AMOUNT_LENGTH; //READ PRICE byte[] priceBytes = Arrays.copyOfRange(data, position, position + PRICE_LENGTH); BigDecimal price = new BigDecimal(new BigInteger(priceBytes), 8); position += PRICE_LENGTH; //READ FEE byte[] feeBytes = Arrays.copyOfRange(data, position, position + FEE_LENGTH); BigDecimal fee = new BigDecimal(new BigInteger(feeBytes), 8); position += FEE_LENGTH; //READ SIGNATURE byte[] signatureBytes = Arrays.copyOfRange(data, position, position + SIGNATURE_LENGTH); return new CreateOrderTransaction(creator, have, want, amount, price, fee, timestamp, reference, signatureBytes); } @SuppressWarnings("unchecked") @Override public JSONObject toJson() { //GET BASE JSONObject transaction = this.getJsonBase(); //ADD CREATOR/ORDER transaction.put("creator", this.creator.getAddress()); JSONObject order = new JSONObject(); order.put("have", this.order.getHave()); order.put("want", this.order.getWant()); order.put("amount", this.order.getAmount().toPlainString()); order.put("price", this.order.getPrice().toPlainString()); transaction.put("order", order); return transaction; } @Override public byte[] toBytes() { byte[] data = new byte[0]; //WRITE TYPE byte[] typeBytes = Ints.toByteArray(CREATE_ORDER_TRANSACTION); typeBytes = Bytes.ensureCapacity(typeBytes, TYPE_LENGTH, 0); data = Bytes.concat(data, typeBytes); //WRITE TIMESTAMP byte[] timestampBytes = Longs.toByteArray(this.timestamp); timestampBytes = Bytes.ensureCapacity(timestampBytes, TIMESTAMP_LENGTH, 0); data = Bytes.concat(data, timestampBytes); //WRITE REFERENCE data = Bytes.concat(data, this.reference); //WRITE CREATOR data = Bytes.concat(data, this.creator.getPublicKey()); //WRITE HAVE byte[] haveBytes = Longs.toByteArray(this.order.getHave()); haveBytes = Bytes.ensureCapacity(haveBytes, HAVE_LENGTH, 0); data = Bytes.concat(data, haveBytes); //WRITE WANT byte[] wantBytes = Longs.toByteArray(this.order.getWant()); wantBytes = Bytes.ensureCapacity(wantBytes, WANT_LENGTH, 0); data = Bytes.concat(data, wantBytes); //WRITE AMOUNT byte[] amountBytes = this.order.getAmount().unscaledValue().toByteArray(); byte[] fill = new byte[AMOUNT_LENGTH - amountBytes.length]; amountBytes = Bytes.concat(fill, amountBytes); data = Bytes.concat(data, amountBytes); //WRITE PRICE byte[] priceBytes = this.order.getPrice().unscaledValue().toByteArray(); fill = new byte[PRICE_LENGTH - priceBytes.length]; priceBytes = Bytes.concat(fill, priceBytes); data = Bytes.concat(data, priceBytes); //WRITE FEE byte[] feeBytes = this.fee.unscaledValue().toByteArray(); fill = new byte[FEE_LENGTH - feeBytes.length]; feeBytes = Bytes.concat(fill, feeBytes); data = Bytes.concat(data, feeBytes); //SIGNATURE data = Bytes.concat(data, this.signature); return data; } @Override public int getDataLength() { return TYPE_LENGTH + BASE_LENGTH; } //VALIDATE public boolean isSignatureValid() { byte[] data = new byte[0]; //WRITE TYPE byte[] typeBytes = Ints.toByteArray(CREATE_ORDER_TRANSACTION); typeBytes = Bytes.ensureCapacity(typeBytes, TYPE_LENGTH, 0); data = Bytes.concat(data, typeBytes); //WRITE TIMESTAMP byte[] timestampBytes = Longs.toByteArray(this.timestamp); timestampBytes = Bytes.ensureCapacity(timestampBytes, TIMESTAMP_LENGTH, 0); data = Bytes.concat(data, timestampBytes); //WRITE REFERENCE data = Bytes.concat(data, this.reference); //WRITE CREATOR data = Bytes.concat(data, this.creator.getPublicKey()); //WRITE HAVE byte[] haveBytes = Longs.toByteArray(this.order.getHave()); haveBytes = Bytes.ensureCapacity(haveBytes, HAVE_LENGTH, 0); data = Bytes.concat(data, haveBytes); //WRITE WANT byte[] wantBytes = Longs.toByteArray(this.order.getWant()); wantBytes = Bytes.ensureCapacity(wantBytes, WANT_LENGTH, 0); data = Bytes.concat(data, wantBytes); //WRITE AMOUNT byte[] amountBytes = this.order.getAmount().unscaledValue().toByteArray(); byte[] fill = new byte[AMOUNT_LENGTH - amountBytes.length]; amountBytes = Bytes.concat(fill, amountBytes); data = Bytes.concat(data, amountBytes); //WRITE PRICE byte[] priceBytes = this.order.getPrice().unscaledValue().toByteArray(); fill = new byte[FEE_LENGTH - priceBytes.length]; priceBytes = Bytes.concat(fill, priceBytes); data = Bytes.concat(data, priceBytes); //WRITE FEE byte[] feeBytes = this.fee.unscaledValue().toByteArray(); fill = new byte[FEE_LENGTH - feeBytes.length]; feeBytes = Bytes.concat(fill, feeBytes); data = Bytes.concat(data, feeBytes); return Crypto.getInstance().verify(this.creator.getPublicKey(), this.signature, data); } @Override public int isValid(DBSet db) { //CHECK IF ASSETS NOT THE SAME if(this.order.getHave() == this.order.getWant()) { return HAVE_EQUALS_WANT; } //CHECK IF AMOUNT POSITIVE if(this.order.getAmount().compareTo(BigDecimal.ZERO) <= 0) { return NEGATIVE_AMOUNT; } //CHECCK IF PRICE POSITIVE if(this.order.getPrice().compareTo(BigDecimal.ZERO) <= 0) { return NEGATIVE_PRICE; } //REMOVE FEE DBSet fork = db.fork(); this.creator.setConfirmedBalance(this.creator.getConfirmedBalance(fork).subtract(this.fee), fork); //CHECK IF SENDER HAS ENOUGH ASSET BALANCE if(this.creator.getConfirmedBalance(this.order.getHave(), fork).compareTo(this.order.getAmount()) == -1) { return NO_BALANCE; } //CHECK IF HAVE IS NOT DIVISBLE if(!this.order.getHaveAsset(db).isDivisible()) { //CHECK IF AMOUNT DOES NOT HAVE ANY DECIMALS if(this.order.getAmount().stripTrailingZeros().scale() > 0) { //AMOUNT HAS DECIMALS return INVALID_AMOUNT; } } //CHECK IF WANT EXISTS Asset wantAsset = this.order.getWantAsset(db); if(wantAsset == null) { //WANT DOES NOT EXIST return ASSET_DOES_NOT_EXIST; } //CHECK IF WANT IS NOT DIVISIBLE if(!wantAsset.isDivisible()) { //CHECK IF TOTAL RETURN DOES NOT HAVE ANY DECIMALS if(this.order.getAmount().multiply(this.order.getPrice()).stripTrailingZeros().scale() > 0) { return INVALID_RETURN; } } //CHECK IF REFERENCE IS OKE if(!Arrays.equals(this.creator.getLastReference(db), this.reference)) { return INVALID_REFERENCE; } //CHECK IF FEE IS POSITIVE if(this.fee.compareTo(BigDecimal.ZERO) <= 0) { return NEGATIVE_FEE; } return VALIDATE_OKE; } //PROCESS/ORPHAN @Override public void process(DBSet db) { //UPDATE CREATOR this.creator.setConfirmedBalance(this.creator.getConfirmedBalance(db).subtract(this.fee), db); //UPDATE REFERENCE OF CREATOR this.creator.setLastReference(this.signature, db); //PROCESS ORDER this.order.copy().process(db, this); } @Override public void orphan(DBSet db) { //UPDATE CREATOR this.creator.setConfirmedBalance(this.creator.getConfirmedBalance(db).add(this.fee), db); //UPDATE REFERENCE OF CREATOR this.creator.setLastReference(this.reference, db); //ORPHAN ORDER this.order.copy().orphan(db); } @Override public Account getCreator() { return this.creator; } @Override public List<Account> getInvolvedAccounts() { List<Account> accounts = new ArrayList<Account>(); accounts.add(this.creator); return accounts; } @Override public boolean isInvolved(Account account) { String address = account.getAddress(); if(address.equals(this.creator.getAddress())) { return true; } return false; } @Override public BigDecimal getAmount(Account account) { if(account.getAddress().equals(this.creator.getAddress())) { return BigDecimal.ZERO.setScale(8).subtract(this.fee); } return BigDecimal.ZERO; } public static byte[] generateSignature(DBSet db, PrivateKeyAccount creator, long have, long want, BigDecimal amount, BigDecimal price, BigDecimal fee, long timestamp) { byte[] data = new byte[0]; Order order = new Order(null, creator, have, want, amount, price, timestamp); //WRITE TYPE byte[] typeBytes = Ints.toByteArray(CREATE_ORDER_TRANSACTION); typeBytes = Bytes.ensureCapacity(typeBytes, TYPE_LENGTH, 0); data = Bytes.concat(data, typeBytes); //WRITE TIMESTAMP byte[] timestampBytes = Longs.toByteArray(timestamp); timestampBytes = Bytes.ensureCapacity(timestampBytes, TIMESTAMP_LENGTH, 0); data = Bytes.concat(data, timestampBytes); //WRITE REFERENCE data = Bytes.concat(data, creator.getLastReference(db)); //WRITE CREATOR data = Bytes.concat(data, creator.getPublicKey()); //WRITE HAVE byte[] haveBytes = Longs.toByteArray(order.getHave()); haveBytes = Bytes.ensureCapacity(haveBytes, HAVE_LENGTH, 0); data = Bytes.concat(data, haveBytes); //WRITE WANT byte[] wantBytes = Longs.toByteArray(order.getWant()); wantBytes = Bytes.ensureCapacity(wantBytes, WANT_LENGTH, 0); data = Bytes.concat(data, wantBytes); //WRITE AMOUNT byte[] amountBytes = order.getAmount().unscaledValue().toByteArray(); byte[] fill = new byte[AMOUNT_LENGTH - amountBytes.length]; amountBytes = Bytes.concat(fill, amountBytes); data = Bytes.concat(data, amountBytes); //WRITE PRICE byte[] priceBytes = order.getPrice().unscaledValue().toByteArray(); fill = new byte[FEE_LENGTH - priceBytes.length]; priceBytes = Bytes.concat(fill, priceBytes); data = Bytes.concat(data, priceBytes); //WRITE FEE byte[] feeBytes = fee.unscaledValue().toByteArray(); fill = new byte[FEE_LENGTH - feeBytes.length]; feeBytes = Bytes.concat(fill, feeBytes); data = Bytes.concat(data, feeBytes); return Crypto.getInstance().sign(creator, data); } }