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);
}
}