package qora.transaction; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.StandardCharsets; 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 qora.account.Account; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; import qora.crypto.Base58; import qora.crypto.Crypto; import qora.naming.Name; import qora.naming.NameSale; import database.DBSet; public class BuyNameTransaction extends Transaction { private static final int BUYER_LENGTH = 32; private static final int SELLER_LENGTH = 25; 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 + BUYER_LENGTH + SELLER_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH; private PublicKeyAccount buyer; private NameSale nameSale; private Account seller; public BuyNameTransaction(PublicKeyAccount buyer, NameSale nameSale, Account seller, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { super(BUY_NAME_TRANSACTION, fee, timestamp, reference, signature); this.buyer = buyer; this.nameSale = nameSale; this.seller = seller; } //GETTERS/SETTERS public PublicKeyAccount getBuyer() { return this.buyer; } public NameSale getNameSale() { return this.nameSale; } public Account getSeller() { return this.seller; } //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 BUYER byte[] buyerBytes = Arrays.copyOfRange(data, position, position + BUYER_LENGTH); PublicKeyAccount buyer = new PublicKeyAccount(buyerBytes); position += BUYER_LENGTH; //READ NAMESALE NameSale nameSale = NameSale.Parse(Arrays.copyOfRange(data, position, data.length)); position += nameSale.getDataLength(); //READ SELLER byte[] recipientBytes = Arrays.copyOfRange(data, position, position + SELLER_LENGTH); Account seller = new Account(Base58.encode(recipientBytes)); position += SELLER_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 BuyNameTransaction(buyer, nameSale, seller, fee, timestamp, reference, signatureBytes); } @SuppressWarnings("unchecked") @Override public JSONObject toJson() { //GET BASE JSONObject transaction = this.getJsonBase(); //ADD REGISTRANT/NAME/VALUE transaction.put("buyer", this.buyer.getAddress()); transaction.put("name", this.nameSale.getKey()); transaction.put("amount", this.nameSale.getAmount()); transaction.put("seller", this.seller.getAddress()); return transaction; } @Override public byte[] toBytes() { byte[] data = new byte[0]; //WRITE TYPE byte[] typeBytes = Ints.toByteArray(BUY_NAME_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 BUYER data = Bytes.concat(data, this.buyer.getPublicKey()); //WRITE NAME SALE data = Bytes.concat(data, this.nameSale.toBytes()); //WRITE SELLER data = Bytes.concat(data, Base58.decode(this.seller.getAddress())); //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() { return TYPE_LENGTH + BASE_LENGTH + this.nameSale.getDataLength(); } //VALIDATE @Override public boolean isSignatureValid() { byte[] data = new byte[0]; //WRITE TYPE byte[] typeBytes = Ints.toByteArray(BUY_NAME_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 BUYER data = Bytes.concat(data, this.buyer.getPublicKey()); //WRITE NAME SALE data = Bytes.concat(data, this.nameSale.toBytes()); //WRITE SELLER data = Bytes.concat(data, Base58.decode(this.seller.getAddress())); //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.buyer.getPublicKey(), this.signature, data); } @Override public int isValid(DBSet db) { //CHECK NAME LENGTH int nameLength = this.nameSale.getKey().getBytes(StandardCharsets.UTF_8).length; if(nameLength > 400 || nameLength < 1) { return INVALID_NAME_LENGTH; } //CHECK IF NAME EXISTS Name name = this.nameSale.getName(db); if(name == null) { return NAME_DOES_NOT_EXIST; } //CHECK IF BUYER IS OWNER if(name.getOwner().getAddress().equals(this.buyer.getAddress())) { return BUYER_ALREADY_OWNER; } //CHECK IF NAME FOR SALE ALREADY if(!db.getNameExchangeMap().contains(this.nameSale.getKey())) { return NAME_NOT_FOR_SALE; } //CHECK IF SELLER IS SELLER if(!name.getOwner().getAddress().equals(this.seller.getAddress())) { return INVALID_SELLER; } //CHECK IF BUYER HAS ENOUGH MONEY if(this.buyer.getBalance(1, db).compareTo(this.nameSale.getAmount().add(this.fee)) == -1) { return NO_BALANCE; } //CHECK IF PRICE MATCHES NameSale nameSale = db.getNameExchangeMap().getNameSale(this.nameSale.getKey()); if(!this.nameSale.getAmount().equals(nameSale.getAmount())) { return INVALID_AMOUNT; } //CHECK IF REFERENCE IS OKE if(!Arrays.equals(this.buyer.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 BUYER this.buyer.setConfirmedBalance(this.buyer.getConfirmedBalance(db).subtract(this.fee).subtract(this.nameSale.getAmount()), db); //UPDATE SELLER Name name = this.nameSale.getName(db); this.seller.setConfirmedBalance(this.seller.getConfirmedBalance(db).add(this.nameSale.getAmount()), db); //UPDATE REFERENCE OF BUYER this.buyer.setLastReference(this.signature, db); //UPDATE NAME OWNER (NEW OBJECT FOR PREVENTING CACHE ERRORS) name = new Name(this.buyer, name.getName(), name.getValue()); db.getNameMap().add(name); //DELETE NAME SALE FROM DATABASE db.getNameExchangeMap().delete(this.nameSale.getKey()); } @Override public void orphan(DBSet db) { //UPDATE BUYER this.buyer.setConfirmedBalance(this.buyer.getConfirmedBalance(db).add(this.fee).add(this.nameSale.getAmount()), db); //UPDATE SELLER this.seller.setConfirmedBalance(this.seller.getConfirmedBalance(db).subtract(this.nameSale.getAmount()), db); //UPDATE REFERENCE OF OWNER this.buyer.setLastReference(this.reference, db); //UPDATE NAME OWNER (NEW OBJECT FOR PREVENTING CACHE ERRORS) Name name = this.nameSale.getName(db); name = new Name(this.seller, name.getName(), name.getValue()); db.getNameMap().add(name); //RESTORE NAMESALE db.getNameExchangeMap().add(this.nameSale); } @Override public Account getCreator() { return this.buyer; } @Override public List<Account> getInvolvedAccounts() { List<Account> accounts = new ArrayList<Account>(); accounts.add(this.buyer); accounts.add(this.getSeller()); return accounts; } @Override public boolean isInvolved(Account account) { String address = account.getAddress(); if(address.equals(this.buyer.getAddress())) { return true; } if(address.equals(this.getSeller().getAddress())) { return true; } return false; } @Override public BigDecimal getAmount(Account account) { String address = account.getAddress(); if(address.equals(this.buyer.getAddress())) { return BigDecimal.ZERO.setScale(8).subtract(this.fee).subtract(this.nameSale.getAmount()); } if(address.equals(this.getSeller().getAddress())) { return this.nameSale.getAmount(); } return BigDecimal.ZERO.setScale(8); } public static byte[] generateSignature(DBSet db, PrivateKeyAccount buyer, NameSale nameSale, Account seller, BigDecimal fee, long timestamp) { byte[] data = new byte[0]; //WRITE TYPE byte[] typeBytes = Ints.toByteArray(BUY_NAME_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, buyer.getLastReference(db)); //WRITE BUYER data = Bytes.concat(data, buyer.getPublicKey()); //WRITE NAME SALE data = Bytes.concat(data, nameSale.toBytes()); //WRITE SELLER data = Bytes.concat(data, Base58.decode(seller.getAddress())); //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); return Crypto.getInstance().sign(buyer, data); } }