package cy.agorise.graphenej; import com.google.common.primitives.Bytes; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; 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.ParsePosition; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.TimeZone; import cy.agorise.graphenej.interfaces.ByteSerializable; import cy.agorise.graphenej.interfaces.JsonSerializable; import cy.agorise.graphenej.operations.CustomOperation; import cy.agorise.graphenej.operations.LimitOrderCreateOperation; import cy.agorise.graphenej.operations.TransferOperation; /** * Class used to represent a generic Graphene transaction. */ public class Transaction implements ByteSerializable, JsonSerializable { /* Default expiration time */ public static final int DEFAULT_EXPIRATION_TIME = 30; /* Constant field names used for serialization/deserialization purposes */ 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"; // Using the bitshares mainnet chain id by default private byte[] chainId = Util.hexToBytes(Chains.BITSHARES.CHAIN_ID); private ECKey privateKey; private BlockData blockData; private List operations; private Extensions extensions; /** * Transaction constructor * @param chainId The chain id * @param privateKey Private key used to sign this transaction * @param blockData Block data * @param operations List of operations contained in this transaction */ public Transaction(byte[] chainId, ECKey privateKey, BlockData blockData, List operations){ this.chainId = chainId; this.privateKey = privateKey; this.blockData = blockData; this.operations = operations; this.extensions = new Extensions(); } /** * 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 operationList){ this(Util.hexToBytes(Chains.BITSHARES.CHAIN_ID), privateKey, blockData, operationList); } /** * 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 operation_list){ this(DumpedPrivateKey.fromBase58(null, wif).getKey(), block_data, operation_list); } /** * Constructor used to build a Transaction object without a private key. This kind of object * is used to represent a transaction data that we don't intend to serialize and sign. * @param blockData Block data instance, containing information about the location of this transaction in the blockchain. * @param operationList The list of operations included in this transaction. */ public Transaction(BlockData blockData, List operationList){ this.blockData = blockData; this.operations = operationList; this.extensions = new Extensions(); } /** * Block data getter * @param blockData New block data */ public void setBlockData(BlockData blockData){ this.blockData = blockData; } /** * Block data setter * @return BlockData instance */ public BlockData getBlockData(){ return this.blockData; } /** * Updates the fees for all operations in this transaction. * @param fees: New fees to apply */ public void setFees(List fees){ for(int i = 0; i < operations.size(); i++) operations.get(i).setFee(fees.get(i)); } public ECKey getPrivateKey(){ return this.privateKey; } public List getOperations(){ return this.operations; } /** * This method is used to query whether the instance has a private key. * @return */ public boolean hasPrivateKey(){ return this.privateKey != null; } /** * Obtains a signature of this transaction. Please note that due to the current reliance on * bitcoinj to generate the signatures, and due to the fact that it uses deterministic * ecdsa signatures, we are slightly modifying the expiration time of the transaction while * we look for a signature that will be accepted by the graphene network. * * This should then be called before any other serialization method. * @return: A valid signature of the current transaction. */ public byte[] getGrapheneSignature(){ boolean isGrapheneCanonical = false; byte[] sigData = null; while(!isGrapheneCanonical) { byte[] serializedTransaction = this.toBytes(); Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(serializedTransaction)); int recId = -1; ECKey.ECDSASignature sig = privateKey.sign(hash); // 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; } } sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S int headerByte = recId + 27 + (privateKey.isCompressed() ? 4 : 0); 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); // Further "canonicality" tests if(((sigData[0] & 0x80) != 0) || (sigData[0] == 0) || ((sigData[1] & 0x80) != 0) || ((sigData[32] & 0x80) != 0) || (sigData[32] == 0) || ((sigData[33] & 0x80) != 0)){ this.blockData.setExpiration(this.blockData.getExpiration() + 1); }else{ isGrapheneCanonical = true; } } return sigData; } public void setChainId(String chainId){ this.chainId = Util.hexToBytes(chainId); } public void setChainId(byte[] chainId){ this.chainId = chainId; } public byte[] getChainId(){ return this.chainId; } /** * 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 byteArray = new ArrayList(); byteArray.addAll(Bytes.asList(chainId)); // 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 extensions byte byteArray.addAll(Bytes.asList(this.extensions.toBytes())); 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(); // Getting the signature before anything else, // since this might change the transaction expiration data slightly byte[] signature = null; try{ signature = getGrapheneSignature(); }catch(Exception e){ System.out.println("Could not generate signature"); } // Formatting expiration time Date expirationTime = new Date(blockData.getExpiration() * 1000); SimpleDateFormat dateFormat = new SimpleDateFormat(Util.TIME_DATE_FORMAT); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); // Adding expiration obj.addProperty(KEY_EXPIRATION, dateFormat.format(expirationTime)); if(signature != null){ // Adding signature JsonArray signatureArray = new JsonArray(); signatureArray.add(Util.bytesToHex(signature)); 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; } /** * Method that will return a hash of this transaction's data. The hash covers only the transaction * attributes and not the signature or the chain id. * * @return A hash of the serialized transaction. */ public byte[] getHash(){ byte[] txBytes = toBytes(); byte[] toHash = Arrays.copyOfRange(txBytes, 32, txBytes.length); //Tx data only, without chain id Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(toHash)); return Arrays.copyOfRange(hash.getBytes(), 0, 20); // The hash is only the first 20 bytes } /** * Class used to encapsulate the procedure to be followed when converting a transaction from a * java object to its JSON string format representation. */ public static class TransactionSerializer implements JsonSerializer { @Override public JsonElement serialize(Transaction transaction, Type type, JsonSerializationContext jsonSerializationContext) { return transaction.toJsonObject(); } } /** * Static inner class used to encapsulate the procedure to be followed when converting a transaction from its * JSON string format representation into a java object instance. */ public static class TransactionDeserializer implements JsonDeserializer { @Override public Transaction deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { JsonObject jsonObject = json.getAsJsonObject(); // Parsing block data information int refBlockNum = jsonObject.get(KEY_REF_BLOCK_NUM).getAsInt(); long refBlockPrefix = jsonObject.get(KEY_REF_BLOCK_PREFIX).getAsLong(); String expiration = jsonObject.get(KEY_EXPIRATION).getAsString(); SimpleDateFormat dateFormat = new SimpleDateFormat(Util.TIME_DATE_FORMAT); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); Date expirationDate = dateFormat.parse(expiration, new ParsePosition(0)); long relativeExpiration = expirationDate.getTime() / 1000; BlockData blockData = new BlockData(refBlockNum, refBlockPrefix, relativeExpiration); // Parsing operation list BaseOperation operation = null; ArrayList operationList = new ArrayList<>(); try { for (JsonElement jsonOperation : jsonObject.get(KEY_OPERATIONS).getAsJsonArray()) { int operationId = jsonOperation.getAsJsonArray().get(0).getAsInt(); if (operationId == OperationType.TRANSFER_OPERATION.ordinal()) { operation = context.deserialize(jsonOperation, TransferOperation.class); } else if (operationId == OperationType.LIMIT_ORDER_CREATE_OPERATION.ordinal()) { operation = context.deserialize(jsonOperation, LimitOrderCreateOperation.class); } else if (operationId == OperationType.LIMIT_ORDER_CANCEL_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.CALL_ORDER_UPDATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.FILL_ORDER_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ACCOUNT_CREATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ACCOUNT_UPDATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ACCOUNT_WHITELIST_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ACCOUNT_UPGRADE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ACCOUNT_TRANSFER_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_CREATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_UPDATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_UPDATE_BITASSET_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_UPDATE_FEED_PRODUCERS_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_ISSUE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_RESERVE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_FUND_FEE_POOL_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_SETTLE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_GLOBAL_SETTLE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_PUBLISH_FEED_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.WITNESS_CREATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.WITNESS_UPDATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.PROPOSAL_CREATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.PROPOSAL_UPDATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.PROPOSAL_DELETE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.WITHDRAW_PERMISSION_CREATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.WITHDRAW_PERMISSION_UPDATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.WITHDRAW_PERMISSION_CLAIM_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.WITHDRAW_PERMISSION_DELETE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.COMMITTEE_MEMBER_CREATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.COMMITTEE_MEMBER_UPDATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.COMMITTEE_MEMBER_UPDATE_GLOBAL_PARAMETERS_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.VESTING_BALANCE_CREATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.VESTING_BALANCE_WITHDRAW_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.WORKER_CREATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.CUSTOM_OPERATION.ordinal()) { operation = context.deserialize(jsonOperation, CustomOperation.class); } else if (operationId == OperationType.ASSERT_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.BALANCE_CLAIM_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.OVERRIDE_TRANSFER_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.TRANSFER_TO_BLIND_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.BLIND_TRANSFER_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.TRANSFER_FROM_BLIND_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_SETTLE_CANCEL_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_CLAIM_FEES_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.FBA_DISTRIBUTE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.BID_COLLATERAL_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.EXECUTE_BID_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_CLAIM_POOL_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_UPDATE_ISSUER_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.HTLC_CREATE_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.HTLC_REDEEM_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.HTLC_REDEEMED_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.HTLC_EXTEND_OPERATION.ordinal()) { //TODO: Add operation deserialization support } else if (operationId == OperationType.HTLC_REFUND_OPERATION.ordinal()) { //TODO: Add operation deserialization support } if (operation != null) operationList.add(operation); operation = null; } return new Transaction(blockData, operationList); }catch(Exception e){ System.out.println("Exception. Msg: "+e.getMessage()); for(StackTraceElement el : e.getStackTrace()){ System.out.println(el.getFileName()+"#"+el.getMethodName()+":"+el.getLineNumber()); } } return new Transaction(blockData, operationList); } } @Override public String toString() { return this.toJsonString(); } }