graphenej/src/main/java/com/luminiasoft/bitshares/Transaction.java

197 lines
7.5 KiB
Java

package com.luminiasoft.bitshares;
import com.google.common.primitives.Bytes;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.luminiasoft.bitshares.interfaces.ByteSerializable;
import com.luminiasoft.bitshares.interfaces.JsonSerializable;
import org.bitcoinj.core.DumpedPrivateKey;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Utils;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
/**
* Class used to represent a generic graphene transaction.
*/
public class Transaction implements ByteSerializable, JsonSerializable {
private final String TAG = this.getClass().getName();
public static final String KEY_EXPIRATION = "expiration";
public static final String KEY_SIGNATURES = "signatures";
public static final String KEY_OPERATIONS = "operations";
public static final String KEY_EXTENSIONS = "extensions";
public static final String KEY_REF_BLOCK_NUM = "ref_block_num";
public static final String KEY_REF_BLOCK_PREFIX = "ref_block_prefix";
private ECKey privateKey;
private BlockData blockData;
private List<BaseOperation> operations;
private List<Extension> extensions;
/**
* Transaction constructor.
* @param wif: The user's private key in the base58 format.
* @param block_data: Block data containing important information used to sign a transaction.
* @param operation_list: List of operations to include in the transaction.
*/
public Transaction(String wif, BlockData block_data, List<BaseOperation> operation_list){
this.privateKey = DumpedPrivateKey.fromBase58(null, wif).getKey();
this.blockData = block_data;
this.operations = operation_list;
this.extensions = new ArrayList<Extension>();
}
/**
* Transaction constructor.
* @param privateKey : Instance of a ECKey containing the private key that will be used to sign this transaction.
* @param blockData : Block data containing important information used to sign a transaction.
* @param operationList : List of operations to include in the transaction.
*/
public Transaction(ECKey privateKey, BlockData blockData, List<BaseOperation> operationList){
this.privateKey = privateKey;
this.blockData = blockData;
this.operations = operationList;
this.extensions = new ArrayList<Extension>();
}
public ECKey getPrivateKey(){
return this.privateKey;
}
public List<BaseOperation> getOperations(){ return this.operations; }
/**
* Obtains a signature of this transaction.
* @return: A valid signature of the current transaction.
*/
public byte[] getSignature(){
byte[] serializedTransaction = this.toBytes();
Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(serializedTransaction));
boolean isCanonical = false;
int recId = -1;
ECKey.ECDSASignature sig = null;
while(!isCanonical) {
sig = privateKey.sign(hash);
if(!sig.isCanonical()){
// Signature was not canonical, retrying
continue;
}else{
// Signature is canonical
isCanonical = true;
}
// Now we have to work backwards to figure out the recId needed to recover the signature.
for (int i = 0; i < 4; i++) {
ECKey k = ECKey.recoverFromSignature(i, sig, hash, privateKey.isCompressed());
if (k != null && k.getPubKeyPoint().equals(privateKey.getPubKeyPoint())) {
recId = i;
break;
}
}
}
int headerByte = recId + 27 + (privateKey.isCompressed() ? 4 : 0);
byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S
sigData[0] = (byte)headerByte;
System.arraycopy(Utils.bigIntegerToBytes(sig.r, 32), 0, sigData, 1, 32);
System.arraycopy(Utils.bigIntegerToBytes(sig.s, 32), 0, sigData, 33, 32);
return sigData;
}
/**
* Method that creates a serialized byte array with compact information about this transaction
* that is needed for the creation of a signature.
* @return: byte array with serialized information about this transaction.
*/
public byte[] toBytes(){
// Creating a List of Bytes and adding the first bytes from the chain apiId
List<Byte> byteArray = new ArrayList<Byte>();
byteArray.addAll(Bytes.asList(Util.hexToBytes(Chains.BITSHARES.CHAIN_ID)));
// Adding the block data
byteArray.addAll(Bytes.asList(this.blockData.toBytes()));
// Adding the number of operations
byteArray.add((byte) this.operations.size());
// Adding all the operations
for(BaseOperation operation : operations){
byteArray.add(operation.getId());
byteArray.addAll(Bytes.asList(operation.toBytes()));
}
//Adding the number of extensions
byteArray.add((byte) this.extensions.size());
for(Extension extension : extensions){
//TODO: Implement the extension serialization
}
// Adding a last zero byte to match the result obtained by the python-graphenelib code
// I'm not exactly sure what's the meaning of this last zero byte, but for now I'll just
// leave it here and work on signing the transaction.
//TODO: Investigate the origin and meaning of this last byte.
byteArray.add((byte) 0 );
return Bytes.toArray(byteArray);
}
@Override
public String toJsonString() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionSerializer());
return gsonBuilder.create().toJson(this);
}
@Override
public JsonObject toJsonObject() {
JsonObject obj = new JsonObject();
// Formatting expiration time
Date expirationTime = new Date(blockData.getRelativeExpiration() * 1000);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
// Adding expiration
obj.addProperty(KEY_EXPIRATION, dateFormat.format(expirationTime));
// Adding signatures
JsonArray signatureArray = new JsonArray();
signatureArray.add(Util.bytesToHex(getSignature()));
obj.add(KEY_SIGNATURES, signatureArray);
JsonArray operationsArray = new JsonArray();
for(BaseOperation operation : operations){
operationsArray.add(operation.toJsonObject());
}
// Adding operations
obj.add(KEY_OPERATIONS, operationsArray);
// Adding extensions
obj.add(KEY_EXTENSIONS, new JsonArray());
// Adding block data
obj.addProperty(KEY_REF_BLOCK_NUM, blockData.getRefBlockNum());
obj.addProperty(KEY_REF_BLOCK_PREFIX, blockData.getRefBlockPrefix());
return obj;
}
class TransactionSerializer implements JsonSerializer<Transaction> {
@Override
public JsonElement serialize(Transaction transaction, Type type, JsonSerializationContext jsonSerializationContext) {
return transaction.toJsonObject();
}
}
}