diff --git a/src/main/java/com/luminiasoft/bitshares/Authority.java b/src/main/java/com/luminiasoft/bitshares/Authority.java index eae9224..a9e9b18 100644 --- a/src/main/java/com/luminiasoft/bitshares/Authority.java +++ b/src/main/java/com/luminiasoft/bitshares/Authority.java @@ -2,6 +2,7 @@ package com.luminiasoft.bitshares; import com.google.gson.JsonElement; import com.luminiasoft.bitshares.errors.MalformedAddressException; +import com.luminiasoft.bitshares.interfaces.ByteSerializable; import com.luminiasoft.bitshares.interfaces.JsonSerializable; import java.util.HashMap; diff --git a/src/main/java/com/luminiasoft/bitshares/Test.java b/src/main/java/com/luminiasoft/bitshares/Test.java index 01bf02f..8dba981 100644 --- a/src/main/java/com/luminiasoft/bitshares/Test.java +++ b/src/main/java/com/luminiasoft/bitshares/Test.java @@ -35,10 +35,10 @@ public class Test { public static final String OPENLEDGER_WITNESS_URL = "wss://bitshares.openledger.info/ws"; // public static final String WITNESS_URL = "wss://fr.blockpay.ch:8089"; - private TransferOperation transferOperation; + private Transaction transaction; - public TransferOperation getTransferOperation() { - return transferOperation; + public Transaction getTransaction() { + return transaction; } private WitnessResponseListener mListener = new WitnessResponseListener() { @@ -109,27 +109,27 @@ public class Test { }; public ECKey.ECDSASignature testSigning() { - byte[] serializedTransaction = this.transferOperation.toBytes(); + byte[] serializedTransaction = this.transaction.toBytes(); Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(serializedTransaction)); byte[] bytesDigest = hash.getBytes(); - ECKey sk = transferOperation.getPrivateKey(); + ECKey sk = transaction.getPrivateKey(); ECKey.ECDSASignature signature = sk.sign(hash); return signature; } public String testSigningMessage() { - byte[] serializedTransaction = this.transferOperation.toBytes(); + byte[] serializedTransaction = this.transaction.toBytes(); Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(serializedTransaction)); - ECKey sk = transferOperation.getPrivateKey(); + ECKey sk = transaction.getPrivateKey(); return sk.signMessage(hash.toString()); } public byte[] signMessage() { - byte[] serializedTransaction = this.transferOperation.toBytes(); + byte[] serializedTransaction = this.transaction.toBytes(); Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(serializedTransaction)); System.out.println(">> digest <<"); System.out.println(Util.bytesToHex(hash.getBytes())); - ECKey sk = transferOperation.getPrivateKey(); + ECKey sk = transaction.getPrivateKey(); System.out.println("Private key bytes"); System.out.println(Util.bytesToHex(sk.getPrivKeyBytes())); boolean isCanonical = false; @@ -183,10 +183,10 @@ public class Test { UserAccount to = new UserAccount("1.2.129848"); AssetAmount amount = new AssetAmount(UnsignedLong.valueOf(100), new Asset("1.3.120")); AssetAmount fee = new AssetAmount(UnsignedLong.valueOf(264174), new Asset("1.3.0")); - operations.add(new Transfer(from, to, amount, fee)); - this.transferOperation = new TransferOperation(Main.WIF, blockData, operations); - byte[] serializedTransaction = this.transferOperation.toBytes(); - System.out.println("Serialized transferOperation"); + operations.add(new TransferOperation(from, to, amount, fee)); + this.transaction = new Transaction(Main.WIF, blockData, operations); + byte[] serializedTransaction = this.transaction.toBytes(); + System.out.println("Serialized transaction"); System.out.println(Util.bytesToHex(serializedTransaction)); } @@ -335,7 +335,7 @@ public class Test { public void testTransactionSerialization() { try { - TransferOperation transferOperation = new TransferTransactionBuilder() + Transaction transaction = new TransferTransactionBuilder() .setSource(new UserAccount("1.2.138632")) .setDestination(new UserAccount("1.2.129848")) .setAmount(new AssetAmount(UnsignedLong.valueOf(100), new Asset("1.3.120"))) @@ -345,9 +345,9 @@ public class Test { .build(); ArrayList transactionList = new ArrayList<>(); - transactionList.add(transferOperation); + transactionList.add(transaction); - byte[] signature = transferOperation.getGrapheneSignature(); + byte[] signature = transaction.getGrapheneSignature(); System.out.println(Util.bytesToHex(signature)); ApiCall call = new ApiCall(4, "call", "broadcast_transaction", transactionList, "2.0", 1); String jsonCall = call.toJsonString(); @@ -420,7 +420,7 @@ public class Test { }; try { - TransferOperation transferOperation = new TransferTransactionBuilder() + Transaction transaction = new TransferTransactionBuilder() .setSource(new UserAccount("1.2.138632")) .setDestination(new UserAccount("1.2.129848")) .setAmount(new AssetAmount(UnsignedLong.valueOf(100), new Asset("1.3.120"))) @@ -430,9 +430,9 @@ public class Test { .build(); ArrayList transactionList = new ArrayList<>(); - transactionList.add(transferOperation); + transactionList.add(transaction); - transactionList.add(transferOperation); + transactionList.add(transaction); SSLContext context = null; context = NaiveSSLContext.getInstance("TLS"); @@ -443,7 +443,7 @@ public class Test { WebSocket mWebSocket = factory.createSocket(OPENLEDGER_WITNESS_URL); - mWebSocket.addListener(new TransactionBroadcastSequence(transferOperation, listener)); + mWebSocket.addListener(new TransactionBroadcastSequence(transaction, listener)); mWebSocket.connect(); } catch (MalformedTransactionException e) { @@ -514,7 +514,7 @@ public class Test { UserAccount to = new UserAccount("1.2.129848"); AssetAmount amount = new AssetAmount(UnsignedLong.valueOf(100), new Asset("1.3.120")); AssetAmount fee = new AssetAmount(UnsignedLong.valueOf(264174), new Asset("1.3.0")); - Transfer transfer = new Transfer(from, to, amount, fee); + TransferOperation transfer = new TransferOperation(from, to, amount, fee); ArrayList operations = new ArrayList<>(); operations.add(transfer); diff --git a/src/main/java/com/luminiasoft/bitshares/Transaction.java b/src/main/java/com/luminiasoft/bitshares/Transaction.java new file mode 100644 index 0000000..e0f4341 --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/Transaction.java @@ -0,0 +1,211 @@ +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 operations; + private List 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 operation_list){ + this.privateKey = DumpedPrivateKey.fromBase58(null, wif).getKey(); + this.blockData = block_data; + this.operations = operation_list; + this.extensions = new ArrayList(); + } + + /** + * 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.privateKey = privateKey; + this.blockData = blockData; + this.operations = operationList; + this.extensions = new ArrayList(); + } + + public ECKey getPrivateKey(){ + return this.privateKey; + } + + public List getOperations(){ return this.operations; } + + /** + * 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.setRelativeExpiration(this.blockData.getRelativeExpiration() + 1); + }else{ + isGrapheneCanonical = true; + } + } + 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 byteArray = new ArrayList(); + 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(); + + // Getting the signature before anything else, + // since this might change the transaction expiration data slightly + byte[] signature = getGrapheneSignature(); + + // 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(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; + + } + + class TransactionSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(Transaction transaction, Type type, JsonSerializationContext jsonSerializationContext) { + return transaction.toJsonObject(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/luminiasoft/bitshares/TransactionBuilder.java b/src/main/java/com/luminiasoft/bitshares/TransactionBuilder.java index 4d2e1ac..05249a8 100644 --- a/src/main/java/com/luminiasoft/bitshares/TransactionBuilder.java +++ b/src/main/java/com/luminiasoft/bitshares/TransactionBuilder.java @@ -11,5 +11,5 @@ public abstract class TransactionBuilder { protected ECKey privateKey; protected BlockData blockData; - public abstract TransferOperation build() throws MalformedTransactionException; + public abstract Transaction build() throws MalformedTransactionException; } diff --git a/src/main/java/com/luminiasoft/bitshares/Transfer.java b/src/main/java/com/luminiasoft/bitshares/Transfer.java deleted file mode 100644 index d2b2bc0..0000000 --- a/src/main/java/com/luminiasoft/bitshares/Transfer.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.luminiasoft.bitshares; - -import com.google.common.primitives.Bytes; -import com.google.common.primitives.UnsignedLong; -import com.google.gson.*; - -import java.lang.reflect.Type; - -/** - * Class used to encapsulate the Transfer operation related functionalities. - * TODO: Add extensions support - */ -public class Transfer extends BaseOperation { - public static final String KEY_FEE = "fee"; - public static final String KEY_AMOUNT = "amount"; - public static final String KEY_EXTENSIONS = "extensions"; - public static final String KEY_FROM = "from"; - public static final String KEY_TO = "to"; - - private AssetAmount fee; - private AssetAmount amount; - private UserAccount from; - private UserAccount to; - private Memo memo; - private String[] extensions; - - public Transfer(UserAccount from, UserAccount to, AssetAmount transferAmount, AssetAmount fee){ - super(OperationType.transfer_operation); - this.from = from; - this.to = to; - this.amount = transferAmount; - this.fee = fee; - this.memo = new Memo(); - } - - public Transfer(UserAccount from, UserAccount to, AssetAmount transferAmount){ - super(OperationType.transfer_operation); - this.from = from; - this.to = to; - this.amount = transferAmount; - this.memo = new Memo(); - } - - public void setFee(AssetAmount newFee){ - this.fee = newFee; - } - - @Override - public byte getId() { - return (byte) this.type.ordinal(); - } - - public UserAccount getFrom(){ - return this.from; - } - - public UserAccount getTo(){ - return this.to; - } - - public AssetAmount getAmount(){ - return this.amount; - } - - public AssetAmount getFee(){ - return this.fee; - } - - @Override - public byte[] toBytes() { - byte[] feeBytes = fee.toBytes(); - byte[] fromBytes = from.toBytes(); - byte[] toBytes = to.toBytes(); - byte[] amountBytes = amount.toBytes(); - byte[] memoBytes = memo.toBytes(); - return Bytes.concat(feeBytes, fromBytes, toBytes, amountBytes, memoBytes); - } - - @Override - public String toJsonString() { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(Transfer.class, new TransferSerializer()); - return gsonBuilder.create().toJson(this); - } - - @Override - public JsonElement toJsonObject() { - JsonArray array = new JsonArray(); - array.add(this.getId()); - JsonObject jsonObject = new JsonObject(); - jsonObject.add(KEY_FEE, fee.toJsonObject()); - jsonObject.add(KEY_AMOUNT, amount.toJsonObject()); - jsonObject.add(KEY_EXTENSIONS, new JsonArray()); - jsonObject.addProperty(KEY_FROM, from.toJsonString()); - jsonObject.addProperty(KEY_TO, to.toJsonString()); - array.add(jsonObject); - return array; - } - - public static class TransferSerializer implements JsonSerializer { - - @Override - public JsonElement serialize(Transfer transfer, Type type, JsonSerializationContext jsonSerializationContext) { - JsonArray arrayRep = new JsonArray(); - arrayRep.add(transfer.getId()); - arrayRep.add(transfer.toJsonObject()); - return arrayRep; - } - } - - /** - * This deserializer will work on any transfer operation serialized in the 'array form' used a lot in - * the Graphene Blockchain API. - * - * An example of this serialized form is the following: - * - * [ - * 0, - * { - * "fee": { - * "amount": 264174, - * "asset_id": "1.3.0" - * }, - * "from": "1.2.138632", - * "to": "1.2.129848", - * "amount": { - * "amount": 100, - * "asset_id": "1.3.0" - * }, - * "extensions": [] - * } - * ] - * - * It will convert this data into a nice Transfer object. - */ - public static class TransferDeserializer implements JsonDeserializer { - - @Override - public Transfer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if(json.isJsonArray()){ - // This block is used just to check if we are in the first step of the deserialization - // when we are dealing with an array. - JsonArray serializedTransfer = json.getAsJsonArray(); - if(serializedTransfer.get(0).getAsInt() != OperationType.transfer_operation.ordinal()){ - // If the operation type does not correspond to a transfer operation, we return null - return null; - }else{ - // Calling itself recursively, this is only done once, so there will be no problems. - return context.deserialize(serializedTransfer.get(1), Transfer.class); - } - }else{ - // This block is called in the second recursion and takes care of deserializing the - // transfer data itself. - JsonObject jsonObject = json.getAsJsonObject(); - - // Deserializing AssetAmount objects - AssetAmount amount = context.deserialize(jsonObject.get("amount"), AssetAmount.class); - AssetAmount fee = context.deserialize(jsonObject.get("fee"), AssetAmount.class); - - // Deserializing UserAccount objects - UserAccount from = new UserAccount(jsonObject.get("from").getAsString()); - UserAccount to = new UserAccount(jsonObject.get("to").getAsString()); - Transfer transfer = new Transfer(from, to, amount, fee); - return transfer; - } - } - } -} diff --git a/src/main/java/com/luminiasoft/bitshares/TransferOperation.java b/src/main/java/com/luminiasoft/bitshares/TransferOperation.java index ad6e8f4..3940e66 100644 --- a/src/main/java/com/luminiasoft/bitshares/TransferOperation.java +++ b/src/main/java/com/luminiasoft/bitshares/TransferOperation.java @@ -1,218 +1,167 @@ 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 com.google.gson.*; 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. + * Class used to encapsulate the TransferOperation operation related functionalities. + * TODO: Add extensions support */ -public class TransferOperation extends BaseOperation 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 class TransferOperation extends BaseOperation { + public static final String KEY_FEE = "fee"; + public static final String KEY_AMOUNT = "amount"; 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"; + public static final String KEY_FROM = "from"; + public static final String KEY_TO = "to"; - private ECKey privateKey; - private BlockData blockData; - private List operations; - private List extensions; + private AssetAmount fee; + private AssetAmount amount; + private UserAccount from; + private UserAccount to; + private Memo memo; + private String[] extensions; - /** - * TransferOperation 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 TransferOperation(String wif, BlockData block_data, List operation_list){ + public TransferOperation(UserAccount from, UserAccount to, AssetAmount transferAmount, AssetAmount fee){ super(OperationType.transfer_operation); - this.privateKey = DumpedPrivateKey.fromBase58(null, wif).getKey(); - this.blockData = block_data; - this.operations = operation_list; - this.extensions = new ArrayList(); + this.from = from; + this.to = to; + this.amount = transferAmount; + this.fee = fee; + this.memo = new Memo(); } - /** - * TransferOperation 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 TransferOperation(ECKey privateKey, BlockData blockData, List operationList){ + public TransferOperation(UserAccount from, UserAccount to, AssetAmount transferAmount){ super(OperationType.transfer_operation); - this.privateKey = privateKey; - this.blockData = blockData; - this.operations = operationList; - this.extensions = new ArrayList(); + this.from = from; + this.to = to; + this.amount = transferAmount; + this.memo = new Memo(); } - public ECKey getPrivateKey(){ - return this.privateKey; - } - - public List getOperations(){ return this.operations; } - - /** - * 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.setRelativeExpiration(this.blockData.getRelativeExpiration() + 1); - }else{ - isGrapheneCanonical = true; - } - } - return sigData; + public void setFee(AssetAmount newFee){ + this.fee = newFee; } @Override public byte getId() { - return 0; + return (byte) this.type.ordinal(); } - /** - * 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(Util.hexToBytes(Chains.BITSHARES.CHAIN_ID))); + public UserAccount getFrom(){ + return this.from; + } - // Adding the block data - byteArray.addAll(Bytes.asList(this.blockData.toBytes())); + public UserAccount getTo(){ + return this.to; + } - // Adding the number of operations - byteArray.add((byte) this.operations.size()); + public AssetAmount getAmount(){ + return this.amount; + } - // Adding all the operations - for(BaseOperation operation : operations){ - byteArray.add(operation.getId()); - byteArray.addAll(Bytes.asList(operation.toBytes())); - } + public AssetAmount getFee(){ + return this.fee; + } - //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 byte[] toBytes() { + byte[] feeBytes = fee.toBytes(); + byte[] fromBytes = from.toBytes(); + byte[] toBytes = to.toBytes(); + byte[] amountBytes = amount.toBytes(); + byte[] memoBytes = memo.toBytes(); + return Bytes.concat(feeBytes, fromBytes, toBytes, amountBytes, memoBytes); } @Override public String toJsonString() { GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(TransferOperation.class, new TransactionSerializer()); + gsonBuilder.registerTypeAdapter(TransferOperation.class, new TransferSerializer()); 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 = getGrapheneSignature(); - - // 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(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; - + public JsonElement toJsonObject() { + JsonArray array = new JsonArray(); + array.add(this.getId()); + JsonObject jsonObject = new JsonObject(); + jsonObject.add(KEY_FEE, fee.toJsonObject()); + jsonObject.add(KEY_AMOUNT, amount.toJsonObject()); + jsonObject.add(KEY_EXTENSIONS, new JsonArray()); + jsonObject.addProperty(KEY_FROM, from.toJsonString()); + jsonObject.addProperty(KEY_TO, to.toJsonString()); + array.add(jsonObject); + return array; } - class TransactionSerializer implements JsonSerializer { + public static class TransferSerializer implements JsonSerializer { @Override - public JsonElement serialize(TransferOperation transferOperation, Type type, JsonSerializationContext jsonSerializationContext) { - return transferOperation.toJsonObject(); + public JsonElement serialize(TransferOperation transfer, Type type, JsonSerializationContext jsonSerializationContext) { + JsonArray arrayRep = new JsonArray(); + arrayRep.add(transfer.getId()); + arrayRep.add(transfer.toJsonObject()); + return arrayRep; } } -} \ No newline at end of file + + /** + * This deserializer will work on any transfer operation serialized in the 'array form' used a lot in + * the Graphene Blockchain API. + * + * An example of this serialized form is the following: + * + * [ + * 0, + * { + * "fee": { + * "amount": 264174, + * "asset_id": "1.3.0" + * }, + * "from": "1.2.138632", + * "to": "1.2.129848", + * "amount": { + * "amount": 100, + * "asset_id": "1.3.0" + * }, + * "extensions": [] + * } + * ] + * + * It will convert this data into a nice TransferOperation object. + */ + public static class TransferDeserializer implements JsonDeserializer { + + @Override + public TransferOperation deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if(json.isJsonArray()){ + // This block is used just to check if we are in the first step of the deserialization + // when we are dealing with an array. + JsonArray serializedTransfer = json.getAsJsonArray(); + if(serializedTransfer.get(0).getAsInt() != OperationType.transfer_operation.ordinal()){ + // If the operation type does not correspond to a transfer operation, we return null + return null; + }else{ + // Calling itself recursively, this is only done once, so there will be no problems. + return context.deserialize(serializedTransfer.get(1), TransferOperation.class); + } + }else{ + // This block is called in the second recursion and takes care of deserializing the + // transfer data itself. + JsonObject jsonObject = json.getAsJsonObject(); + + // Deserializing AssetAmount objects + AssetAmount amount = context.deserialize(jsonObject.get("amount"), AssetAmount.class); + AssetAmount fee = context.deserialize(jsonObject.get("fee"), AssetAmount.class); + + // Deserializing UserAccount objects + UserAccount from = new UserAccount(jsonObject.get("from").getAsString()); + UserAccount to = new UserAccount(jsonObject.get("to").getAsString()); + TransferOperation transfer = new TransferOperation(from, to, amount, fee); + return transfer; + } + } + } +} diff --git a/src/main/java/com/luminiasoft/bitshares/TransferTransactionBuilder.java b/src/main/java/com/luminiasoft/bitshares/TransferTransactionBuilder.java index 8ea9ac7..97d07a2 100644 --- a/src/main/java/com/luminiasoft/bitshares/TransferTransactionBuilder.java +++ b/src/main/java/com/luminiasoft/bitshares/TransferTransactionBuilder.java @@ -46,7 +46,7 @@ public class TransferTransactionBuilder extends TransactionBuilder { return this; } - public TransferTransactionBuilder addOperation(Transfer transferOperation){ + public TransferTransactionBuilder addOperation(TransferOperation transferOperation){ if(operations == null){ operations = new ArrayList(); } @@ -54,7 +54,7 @@ public class TransferTransactionBuilder extends TransactionBuilder { } @Override - public TransferOperation build() throws MalformedTransactionException { + public Transaction build() throws MalformedTransactionException { if(privateKey == null){ throw new MalformedTransactionException("Missing private key information"); }else if(blockData == null){ @@ -73,14 +73,14 @@ public class TransferTransactionBuilder extends TransactionBuilder { if(transferAmount == null){ throw new MalformedTransactionException("Missing transfer amount information"); } - Transfer transferOperation; + TransferOperation transferOperation; if(feeAmount == null){ - transferOperation = new Transfer(sourceAccount, destinationAccount, transferAmount); + transferOperation = new TransferOperation(sourceAccount, destinationAccount, transferAmount); }else{ - transferOperation = new Transfer(sourceAccount, destinationAccount, transferAmount, feeAmount); + transferOperation = new TransferOperation(sourceAccount, destinationAccount, transferAmount, feeAmount); } operations.add(transferOperation); } - return new TransferOperation(privateKey, blockData, operations); + return new Transaction(privateKey, blockData, operations); } } diff --git a/src/main/java/com/luminiasoft/bitshares/models/HistoricalTransfer.java b/src/main/java/com/luminiasoft/bitshares/models/HistoricalTransfer.java index 763fbed..1e27519 100644 --- a/src/main/java/com/luminiasoft/bitshares/models/HistoricalTransfer.java +++ b/src/main/java/com/luminiasoft/bitshares/models/HistoricalTransfer.java @@ -1,8 +1,6 @@ package com.luminiasoft.bitshares.models; -import com.luminiasoft.bitshares.BaseOperation; -import com.luminiasoft.bitshares.GrapheneObject; -import com.luminiasoft.bitshares.Transfer; +import com.luminiasoft.bitshares.TransferOperation; /** * This class offers support to deserialization of transfer operations received by the API @@ -13,7 +11,7 @@ import com.luminiasoft.bitshares.Transfer; */ public class HistoricalTransfer { public String id; - public Transfer op; + public TransferOperation op; public Object[] result; public long block_num; public long trx_in_block; diff --git a/src/main/java/com/luminiasoft/bitshares/ws/GetRelativeAccountHistory.java b/src/main/java/com/luminiasoft/bitshares/ws/GetRelativeAccountHistory.java index 08af08e..d32806e 100644 --- a/src/main/java/com/luminiasoft/bitshares/ws/GetRelativeAccountHistory.java +++ b/src/main/java/com/luminiasoft/bitshares/ws/GetRelativeAccountHistory.java @@ -5,7 +5,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.luminiasoft.bitshares.AssetAmount; import com.luminiasoft.bitshares.RPC; -import com.luminiasoft.bitshares.Transfer; +import com.luminiasoft.bitshares.TransferOperation; import com.luminiasoft.bitshares.UserAccount; import com.luminiasoft.bitshares.interfaces.WitnessResponseListener; import com.luminiasoft.bitshares.models.ApiCall; @@ -119,7 +119,7 @@ public class GetRelativeAccountHistory extends WebSocketAdapter { System.out.println(frame.getPayloadText()); Type RelativeAccountHistoryResponse = new TypeToken>>(){}.getType(); GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(Transfer.class, new Transfer.TransferDeserializer()); + gsonBuilder.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer()); gsonBuilder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetDeserializer()); WitnessResponse> transfersResponse = gsonBuilder.create().fromJson(response, RelativeAccountHistoryResponse); mListener.onSuccess(transfersResponse); diff --git a/src/main/java/com/luminiasoft/bitshares/ws/TransactionBroadcastSequence.java b/src/main/java/com/luminiasoft/bitshares/ws/TransactionBroadcastSequence.java index bbd6b68..c4d129d 100644 --- a/src/main/java/com/luminiasoft/bitshares/ws/TransactionBroadcastSequence.java +++ b/src/main/java/com/luminiasoft/bitshares/ws/TransactionBroadcastSequence.java @@ -3,7 +3,7 @@ package com.luminiasoft.bitshares.ws; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.luminiasoft.bitshares.*; -import com.luminiasoft.bitshares.TransferOperation; +import com.luminiasoft.bitshares.Transaction; import com.luminiasoft.bitshares.interfaces.WitnessResponseListener; import com.luminiasoft.bitshares.models.ApiCall; import com.luminiasoft.bitshares.models.BaseResponse; @@ -24,7 +24,7 @@ import java.util.Map; import java.util.TimeZone; /** - * Class that will handle the transferOperation publication procedure. + * Class that will handle the transaction publication procedure. */ public class TransactionBroadcastSequence extends WebSocketAdapter { private final String TAG = this.getClass().getName(); @@ -35,7 +35,7 @@ public class TransactionBroadcastSequence extends WebSocketAdapter { private final static int BROADCAST_TRANSACTION = 4; public final static int EXPIRATION_TIME = 30; - private TransferOperation transferOperation; + private Transaction transaction; private long expirationTime; private String headBlockId; private long headBlockNumber; @@ -47,13 +47,13 @@ public class TransactionBroadcastSequence extends WebSocketAdapter { /** * Constructor of this class. The ids required - * @param transferOperation: The transferOperation to be broadcasted. + * @param transaction: The transaction to be broadcasted. * @param listener: A class implementing the WitnessResponseListener interface. This should * be implemented by the party interested in being notified about the success/failure - * of the transferOperation broadcast operation. + * of the transaction broadcast operation. */ - public TransactionBroadcastSequence(TransferOperation transferOperation, WitnessResponseListener listener){ - this.transferOperation = transferOperation; + public TransactionBroadcastSequence(Transaction transaction, WitnessResponseListener listener){ + this.transaction = transaction; this.mListener = listener; } @@ -102,14 +102,14 @@ public class TransactionBroadcastSequence extends WebSocketAdapter { headBlockNumber = dynamicProperties.head_block_number; ArrayList transactionList = new ArrayList<>(); - transactionList.add(transferOperation); + transactionList.add(transaction); ApiCall call = new ApiCall(broadcastApiId, RPC.CALL_BROADCAST_TRANSACTION, transactionList, "2.0", currentId); - // Finally sending transferOperation + // Finally sending transaction websocket.sendText(call.toJsonString()); }else if(baseResponse.id >= BROADCAST_TRANSACTION){ Type WitnessResponseType = new TypeToken>(){}.getType(); @@ -131,18 +131,18 @@ public class TransactionBroadcastSequence extends WebSocketAdapter { * with ONE transfer operation. */ retries++; - List operations = this.transferOperation.getOperations(); - Transfer transfer = (Transfer) operations.get(0); - transferOperation = new TransferTransactionBuilder() + List operations = this.transaction.getOperations(); + TransferOperation transfer = (TransferOperation) operations.get(0); + transaction = new TransferTransactionBuilder() .setSource(transfer.getFrom()) .setDestination(transfer.getTo()) .setAmount(transfer.getAmount()) .setFee(transfer.getFee()) .setBlockData(new BlockData(headBlockNumber, headBlockId, expirationTime + EXPIRATION_TIME)) - .setPrivateKey(transferOperation.getPrivateKey()) + .setPrivateKey(transaction.getPrivateKey()) .build(); ArrayList transactionList = new ArrayList<>(); - transactionList.add(transferOperation); + transactionList.add(transaction); ApiCall call = new ApiCall(broadcastApiId, RPC.CALL_BROADCAST_TRANSACTION, transactionList,