package qora.transaction; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import ntp.NTP; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import qora.account.Account; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; import qora.crypto.Crypto; import qora.payment.Payment; 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 MultiPaymentTransaction extends Transaction { private static final int REFERENCE_LENGTH = 64; private static final int SENDER_LENGTH = 32; private static final int PAYMENTS_SIZE_LENGTH = 4; 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 + PAYMENTS_SIZE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH; private PublicKeyAccount sender; private List<Payment> payments; public MultiPaymentTransaction(PublicKeyAccount sender, List<Payment> payments, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { super(MULTI_PAYMENT_TRANSACTION, fee, timestamp, reference, signature); this.sender = sender; this.payments = payments; } //GETTERS/SETTERS public Account getSender() { return this.sender; } public List<Payment> getPayments() { return this.payments; } //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 PAYMENTS SIZE byte[] paymentsLengthBytes = Arrays.copyOfRange(data, position, position + PAYMENTS_SIZE_LENGTH); int paymentsLength = Ints.fromByteArray(paymentsLengthBytes); position += PAYMENTS_SIZE_LENGTH; if(paymentsLength < 1 || paymentsLength > 400) { throw new Exception("Invalid payments length"); } //READ PAYMENTS List<Payment> payments = new ArrayList<Payment>(); for(int i=0; i<paymentsLength; i++) { Payment payment = Payment.parse(Arrays.copyOfRange(data, position, position + Payment.BASE_LENGTH)); payments.add(payment); position += Payment.BASE_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 MultiPaymentTransaction(sender, payments, fee, timestamp, reference, signatureBytes); } @SuppressWarnings("unchecked") @Override public JSONObject toJson() { //GET BASE JSONObject transaction = this.getJsonBase(); //ADD SENDER/PAYMENTS transaction.put("sender", this.sender.getAddress()); JSONArray payments = new JSONArray(); for(Payment payment: this.payments) { payments.add(payment.toJson()); } transaction.put("payments", payments); return transaction; } @Override public byte[] toBytes() { byte[] data = new byte[0]; //WRITE TYPE byte[] typeBytes = Ints.toByteArray(MULTI_PAYMENT_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 PAYMENTS SIZE int paymentsLength = this.payments.size(); byte[] paymentsLengthBytes = Ints.toByteArray(paymentsLength); data = Bytes.concat(data, paymentsLengthBytes); //WRITE PAYMENTS for(Payment payment: this.payments) { data = Bytes.concat(data, payment.toBytes()); } //WRITE FEE byte[] feeBytes = this.fee.unscaledValue().toByteArray(); byte[] 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() { int paymentsLength = 0; for(Payment payment: this.getPayments()) { paymentsLength += payment.getDataLength(); } return TYPE_LENGTH + BASE_LENGTH + paymentsLength; } //VALIDATE public boolean isSignatureValid() { byte[] data = new byte[0]; //WRITE TYPE byte[] typeBytes = Ints.toByteArray(MULTI_PAYMENT_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 PAYMENTS SIZE int paymentsLength = this.payments.size(); byte[] paymentsLengthBytes = Ints.toByteArray(paymentsLength); data = Bytes.concat(data, paymentsLengthBytes); //WRITE PAYMENTS for(Payment payment: this.payments) { data = Bytes.concat(payment.toBytes()); } //WRITE FEE byte[] feeBytes = this.fee.unscaledValue().toByteArray(); byte[] 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 PAYMENTS SIZE if(this.payments.size() < 1 || this.payments.size() > 400) { return INVALID_PAYMENTS_LENGTH; } //REMOVE FEE DBSet fork = db.fork(); this.sender.setConfirmedBalance(this.sender.getConfirmedBalance(fork).subtract(this.fee), fork); //CHECK PAYMENTS for(Payment payment: this.payments) { //CHECK IF RECIPIENT IS VALID ADDRESS if(!Crypto.getInstance().isValidAddress(payment.getRecipient().getAddress())) { return INVALID_ADDRESS; } //CHECK IF AMOUNT IS POSITIVE if(payment.getAmount().compareTo(BigDecimal.ZERO) <= 0) { return NEGATIVE_AMOUNT; } //CHECK IF SENDER HAS ENOUGH ASSET BALANCE if(this.sender.getConfirmedBalance(payment.getAsset(), fork).compareTo(payment.getAmount()) == -1) { return NO_BALANCE; } //CHECK IF AMOUNT IS DIVISIBLE if(!db.getAssetMap().get(payment.getAsset()).isDivisible()) { //CHECK IF AMOUNT DOES NOT HAVE ANY DECIMALS if(payment.getAmount().stripTrailingZeros().scale() > 0) { //AMOUNT HAS DECIMALS return INVALID_AMOUNT; } } //PROCESS PAYMENT IN FORK payment.process(this.sender, fork); } //CHECK IF REFERENCE IS OKE if(!Arrays.equals(this.sender.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 SENDER this.sender.setConfirmedBalance(this.sender.getConfirmedBalance(db).subtract(this.fee), db); //UPDATE REFERENCE OF SENDER this.sender.setLastReference(this.signature, db); //PROCESS PAYMENTS for(Payment payment: this.payments) { payment.process(this.sender, db); //UPDATE REFERENCE OF RECIPIENT if(Arrays.equals(payment.getRecipient().getLastReference(db), new byte[0])) { payment.getRecipient().setLastReference(this.signature, db); } } } @Override public void orphan(DBSet db) { //UPDATE SENDER this.sender.setConfirmedBalance(this.sender.getConfirmedBalance(db).add(this.fee), db); //UPDATE REFERENCE OF SENDER this.sender.setLastReference(this.reference, db); //ORPHAN PAYMENTS for(Payment payment: this.payments) { payment.orphan(this.sender, db); //UPDATE REFERENCE OF RECIPIENT if(Arrays.equals(payment.getRecipient().getLastReference(db), this.signature)) { payment.getRecipient().removeReference(db); } } } //REST @Override public Account getCreator() { return this.sender; } @Override public List<Account> getInvolvedAccounts() { List<Account> accounts = new ArrayList<Account>(); accounts.add(this.sender); for(Payment payment: this.payments) { accounts.add(payment.getRecipient()); } return accounts; } @Override public boolean isInvolved(Account account) { String address = account.getAddress(); for(Account involved: this.getInvolvedAccounts()) { if(address.equals(involved.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); } //CHECK PAYMENTS for(Payment payment: this.payments) { //IF QORA ASSET if(payment.getAsset() == BalanceMap.QORA_KEY) { //IF SENDER if(address.equals(this.sender.getAddress())) { amount = amount.subtract(payment.getAmount()); } //IF RECIPIENT if(address.equals(payment.getRecipient().getAddress())) { amount = amount.add(payment.getAmount()); } } } return amount; } public static byte[] generateSignature(DBSet db, PrivateKeyAccount sender, List<Payment> payments, BigDecimal fee, long timestamp) { byte[] data = new byte[0]; //WRITE TYPE byte[] typeBytes = Ints.toByteArray(MULTI_PAYMENT_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 PAYMENTS SIZE int paymentsLength = payments.size(); byte[] paymentsLengthBytes = Ints.toByteArray(paymentsLength); data = Bytes.concat(data, paymentsLengthBytes); //WRITE PAYMENTS for(Payment payment: payments) { data = Bytes.concat(payment.toBytes()); } //WRITE FEE byte[] feeBytes = fee.unscaledValue().toByteArray(); byte[] fill = new byte[FEE_LENGTH - feeBytes.length]; feeBytes = Bytes.concat(fill, feeBytes); data = Bytes.concat(data, feeBytes); //SIGN return Crypto.getInstance().sign(sender, data); } }