package qora.transaction; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Arrays; import java.util.List; import ntp.NTP; import org.json.simple.JSONObject; import qora.account.Account; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; import qora.crypto.Base58; import qora.crypto.Crypto; import com.google.common.primitives.Bytes; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; import database.BalanceMap; import database.DBSet; public class TransferAssetTransaction extends Transaction { private static final int REFERENCE_LENGTH = 64; private static final int SENDER_LENGTH = 32; private static final int RECIPIENT_LENGTH = Account.ADDRESS_LENGTH; private static final int KEY_LENGTH = 8; private static final int AMOUNT_LENGTH = 12; private static final int FEE_LENGTH = 8; private static final int SIGNATURE_LENGTH = 64; private static final int BASE_LENGTH = TIMESTAMP_LENGTH + REFERENCE_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + KEY_LENGTH + AMOUNT_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH; private PublicKeyAccount sender; private Account recipient; private BigDecimal amount; private long key; public TransferAssetTransaction(PublicKeyAccount sender, Account recipient, long key, BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { super(TRANSFER_ASSET_TRANSACTION, fee, timestamp, reference, signature); this.sender = sender; this.recipient = recipient; this.amount = amount; this.key = key; } //GETTERS/SETTERS public Account getSender() { return this.sender; } public Account getRecipient() { return this.recipient; } public BigDecimal getAmount() { return this.amount; } public long getKey() { return this.key; } //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 SENDER byte[] senderBytes = Arrays.copyOfRange(data, position, position + SENDER_LENGTH); PublicKeyAccount sender = new PublicKeyAccount(senderBytes); position += SENDER_LENGTH; //READ RECIPIENT byte[] recipientBytes = Arrays.copyOfRange(data, position, position + RECIPIENT_LENGTH); Account recipient = new Account(Base58.encode(recipientBytes)); position += RECIPIENT_LENGTH; //READ KEY byte[] keyBytes = Arrays.copyOfRange(data, position, position + KEY_LENGTH); long key = Longs.fromByteArray(keyBytes); position += KEY_LENGTH; //READ AMOUNT byte[] amountBytes = Arrays.copyOfRange(data, position, position + AMOUNT_LENGTH); BigDecimal amount = new BigDecimal(new BigInteger(amountBytes), 8); position += AMOUNT_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 TransferAssetTransaction(sender, recipient, key, amount, fee, timestamp, reference, signatureBytes); } @SuppressWarnings("unchecked") @Override public JSONObject toJson() { //GET BASE JSONObject transaction = this.getJsonBase(); //ADD SENDER/RECIPIENT/AMOUNT/ASSET transaction.put("sender", this.sender.getAddress()); transaction.put("recipient", this.recipient.getAddress()); transaction.put("asset", this.key); transaction.put("amount", this.amount.toPlainString()); return transaction; } @Override public byte[] toBytes() { byte[] data = new byte[0]; //WRITE TYPE byte[] typeBytes = Ints.toByteArray(TRANSFER_ASSET_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 SENDER data = Bytes.concat(data , this.sender.getPublicKey()); //WRITE RECIPIENT data = Bytes.concat(data, Base58.decode(this.recipient.getAddress())); //WRITE KEY byte[] keyBytes = Longs.toByteArray(this.key); keyBytes = Bytes.ensureCapacity(keyBytes, KEY_LENGTH, 0); data = Bytes.concat(data, keyBytes); //WRITE AMOUNT byte[] amountBytes = this.amount.unscaledValue().toByteArray(); byte[] fill = new byte[AMOUNT_LENGTH - amountBytes.length]; amountBytes = Bytes.concat(fill, amountBytes); data = Bytes.concat(data, amountBytes); //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(TRANSFER_ASSET_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 SENDER data = Bytes.concat(data , this.sender.getPublicKey()); //WRITE RECIPIENT data = Bytes.concat(data, Base58.decode(this.recipient.getAddress())); //WRITE KEY byte[] keyBytes = Longs.toByteArray(this.key); keyBytes = Bytes.ensureCapacity(keyBytes, KEY_LENGTH, 0); data = Bytes.concat(data, keyBytes); //WRITE AMOUNT byte[] amountBytes = this.amount.unscaledValue().toByteArray(); byte[] fill = new byte[AMOUNT_LENGTH - amountBytes.length]; amountBytes = Bytes.concat(fill, amountBytes); data = Bytes.concat(data, amountBytes); //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.sender.getPublicKey(), this.signature, data); } @Override public int isValid(DBSet db) { //CHECK IF RELEASED if(NTP.getTime() < ASSETS_RELEASE) { return NOT_YET_RELEASED; } //CHECK IF RECIPIENT IS VALID ADDRESS if(!Crypto.getInstance().isValidAddress(this.recipient.getAddress())) { return INVALID_ADDRESS; } //REMOVE FEE DBSet fork = db.fork(); this.sender.setConfirmedBalance(this.sender.getConfirmedBalance(fork).subtract(this.fee), fork); //CHECK IF SENDER HAS ENOUGH ASSET BALANCE if(this.sender.getConfirmedBalance(this.key, fork).compareTo(this.amount) == -1) { return NO_BALANCE; } //CHECK IF AMOUNT IS DIVISIBLE if(!db.getAssetMap().get(this.key).isDivisible()) { //CHECK IF AMOUNT DOES NOT HAVE ANY DECIMALS if(this.getAmount().stripTrailingZeros().scale() > 0) { //AMOUNT HAS DECIMALS return INVALID_AMOUNT; } } //CHECK IF REFERENCE IS OKE if(!Arrays.equals(this.sender.getLastReference(db), this.reference)) { return INVALID_REFERENCE; } //CHECK IF AMOUNT IS POSITIVE if(this.amount.compareTo(BigDecimal.ZERO) <= 0) { return NEGATIVE_AMOUNT; } //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 SENDER this.sender.setConfirmedBalance(this.sender.getConfirmedBalance(db).subtract(this.fee), db); this.sender.setConfirmedBalance(this.key, this.sender.getConfirmedBalance(this.key, db).subtract(this.amount), db); //UPDATE RECIPIENT this.recipient.setConfirmedBalance(this.key, this.recipient.getConfirmedBalance(this.key, db).add(this.amount), db); //UPDATE REFERENCE OF SENDER this.sender.setLastReference(this.signature, db); //UPDATE REFERENCE OF RECIPIENT if(this.key == BalanceMap.QORA_KEY) { if(Arrays.equals(this.recipient.getLastReference(db), new byte[0])) { this.recipient.setLastReference(this.signature, db); } } } @Override public void orphan(DBSet db) { //UPDATE SENDER this.sender.setConfirmedBalance(this.sender.getConfirmedBalance(db).add(this.fee), db); this.sender.setConfirmedBalance(this.key, this.sender.getConfirmedBalance(this.key, db).add(this.amount), db); //UPDATE RECIPIENT this.recipient.setConfirmedBalance(this.key, this.recipient.getConfirmedBalance(this.key, db).subtract(this.amount), db); //UPDATE REFERENCE OF SENDER this.sender.setLastReference(this.reference, db); //UPDATE REFERENCE OF RECIPIENT if(this.key == BalanceMap.QORA_KEY) { if(Arrays.equals(this.recipient.getLastReference(db), this.signature)) { this.recipient.removeReference(db); } } } //REST @Override public Account getCreator() { return this.sender; } @Override public List<Account> getInvolvedAccounts() { return Arrays.asList(this.sender, this.recipient); } @Override public boolean isInvolved(Account account) { String address = account.getAddress(); if(address.equals(sender.getAddress()) || address.equals(recipient.getAddress())) { return true; } return false; } @Override public BigDecimal getAmount(Account account) { BigDecimal amount = BigDecimal.ZERO.setScale(8); String address = account.getAddress(); //IF SENDER if(address.equals(this.sender.getAddress())) { amount = amount.subtract(this.fee); } //IF QORA ASSET if(this.key == BalanceMap.QORA_KEY) { //IF SENDER if(address.equals(this.sender.getAddress())) { amount = amount.subtract(this.amount); } //IF RECIPIENT if(address.equals(this.recipient.getAddress())) { amount = amount.add(this.amount); } } return amount; } public static byte[] generateSignature(PrivateKeyAccount sender, Account recipient, long key, BigDecimal amount, BigDecimal fee, long timestamp) { return generateSignature(DBSet.getInstance(), sender, recipient, key, amount, fee, timestamp); } public static byte[] generateSignature(DBSet db, PrivateKeyAccount sender, Account recipient, long key, BigDecimal amount, BigDecimal fee, long timestamp) { byte[] data = new byte[0]; //WRITE TYPE byte[] typeBytes = Ints.toByteArray(TRANSFER_ASSET_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, sender.getLastReference(db)); //WRITE SENDER data = Bytes.concat(data , sender.getPublicKey()); //WRITE RECIPIENT data = Bytes.concat(data, Base58.decode(recipient.getAddress())); //WRITE KEY byte[] keyBytes = Longs.toByteArray(key); keyBytes = Bytes.ensureCapacity(keyBytes, KEY_LENGTH, 0); data = Bytes.concat(data, keyBytes); //WRITE AMOUNT byte[] amountBytes = amount.unscaledValue().toByteArray(); byte[] fill = new byte[AMOUNT_LENGTH - amountBytes.length]; amountBytes = Bytes.concat(fill, amountBytes); data = Bytes.concat(data, amountBytes); //WRITE FEE byte[] feeBytes = fee.unscaledValue().toByteArray(); fill = new byte[FEE_LENGTH - feeBytes.length]; feeBytes = Bytes.concat(fill, feeBytes); data = Bytes.concat(data, feeBytes); //SIGN return Crypto.getInstance().sign(sender, data); } }