From d8c796203afc9951b345d32142b8d412a7f96afe Mon Sep 17 00:00:00 2001 From: superbigfu <512034786@qq.com> Date: Tue, 2 Apr 2019 15:30:10 +0800 Subject: [PATCH] fix transaction deserializer --- pom.xml | 16 +++- .../client/graphenej/objects/Transaction.java | 86 +++++++++++++++---- .../operations/AccountCreateOperation.java | 78 +++++++++++++++-- .../operations/LimitOrderCreateOperation.java | 2 +- .../operations/TransferOperation.java | 2 +- .../com/gxchain/client/util/GXGsonUtil.java | 4 +- .../gxchain/client/util/GXGsonUtilTest.java | 30 +++++++ 7 files changed, 188 insertions(+), 30 deletions(-) create mode 100644 src/test/java/com/gxchain/client/util/GXGsonUtilTest.java diff --git a/pom.xml b/pom.xml index 42b75a8..52d599b 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,14 @@ UTF-8 2.5.0 + + + + gxchain + http://repo.gxchain.cn/repository/maven-public/ + + + junit @@ -140,13 +148,13 @@ nexus-releases - http://192.168.1.122:8081/repository/maven-releases/ - + + http://repo.gxchain.cn/repository/maven-releases/ nexus-snapshots - http://192.168.1.122:8081/repository/maven-snapshots/ - + + http://repo.gxchain.cn/repository/maven-snapshots/ \ No newline at end of file diff --git a/src/main/java/com/gxchain/client/graphenej/objects/Transaction.java b/src/main/java/com/gxchain/client/graphenej/objects/Transaction.java index defbfd1..d489c02 100644 --- a/src/main/java/com/gxchain/client/graphenej/objects/Transaction.java +++ b/src/main/java/com/gxchain/client/graphenej/objects/Transaction.java @@ -6,25 +6,29 @@ import com.gxchain.client.graphenej.enums.OperationType; import com.gxchain.client.graphenej.interfaces.ByteSerializable; import com.gxchain.client.graphenej.interfaces.JsonSerializable; +import com.gxchain.client.graphenej.operations.AccountCreateOperation; import com.gxchain.client.graphenej.operations.BaseOperation; import com.gxchain.client.graphenej.operations.LimitOrderCreateOperation; import com.gxchain.client.graphenej.operations.TransferOperation; import com.gxchain.client.util.TxSerializerUtil; import com.gxchain.common.signature.SignatureUtil; +import com.gxchain.common.signature.utils.StringUtils; import com.gxchain.common.signature.utils.Wif; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.DumpedPrivateKey; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Sha256Hash; -import org.bitcoinj.core.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Type; import java.text.ParsePosition; import java.text.SimpleDateFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; /** * Class used to represent a generic Graphene transaction. @@ -39,7 +43,9 @@ public class Transaction implements ByteSerializable, JsonSerializable { /* Default expiration time */ public static final int DEFAULT_EXPIRATION_TIME = 120; + /* Constant field names used for serialization/deserialization purposes */ + public static final String KEY_CHAIN_ID = "chain_id"; public static final String KEY_EXPIRATION = "expiration"; public static final String KEY_SIGNATURES = "signatures"; public static final String KEY_OPERATIONS = "operations"; @@ -72,6 +78,22 @@ public Transaction(ECKey privateKey, BlockData blockData, List op this.extensions = new Extensions(); } + /** + * Constructor used to build a BlockTransaction 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 chainId + * @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. + * @param signature + */ + public Transaction(String chainId, BlockData blockData, List operationList, String signature) { + this.chainId = chainId; + this.blockData = blockData; + this.operations = operationList; + this.signature = signature; + } + /** * BlockTransaction constructor. * @@ -120,8 +142,9 @@ public void setBlockData(BlockData blockData) { * @param fees: New fees to apply */ public void setFees(List fees) { - for (int i = 0; i < operations.size(); i++) + for (int i = 0; i < operations.size(); i++) { operations.get(i).setFee(fees.get(i)); + } } public ECKey getPrivateKey() { @@ -152,7 +175,7 @@ public boolean hasPrivateKey() { * @return: A valid signature of the current transaction. */ public byte[] getGrapheneSignature() { - return SignatureUtil.signature(this.toBytes(),new Wif(privateKey).toString()); + return SignatureUtil.signature(this.toBytes(), new Wif(privateKey).toString()); } /** @@ -165,7 +188,22 @@ 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(getChainId()))); - byteArray.addAll(Bytes.asList(TxSerializerUtil.serializeTransaction(this.toJsonObjectNoSign()))); + + // 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); } @@ -183,16 +221,17 @@ public JsonObject toJsonObject() { // Getting the signature before anything else, // since this might change the transaction expiration data slightly - long l1 = System.currentTimeMillis(); - byte[] signature = getGrapheneSignature(); - logger.info("signature consuming " + (System.currentTimeMillis() - l1) + " ms"); - String sign = Util.bytesToHex(signature); - this.signature = sign; + if (StringUtils.isEmpty(this.signature)) { + long l1 = System.currentTimeMillis(); + byte[] signature = getGrapheneSignature(); + logger.info("signature consuming " + (System.currentTimeMillis() - l1) + " ms"); + this.signature = Util.bytesToHex(signature); + } JsonObject obj = toJsonObjectNoSign(); // Adding signatures JsonArray signatureArray = new JsonArray(); - signatureArray.add(sign); + signatureArray.add(this.signature); obj.add(KEY_SIGNATURES, signatureArray); return obj; @@ -205,7 +244,7 @@ public JsonObject toJsonObjectNoSign() { obj.addProperty(KEY_REF_BLOCK_PREFIX, blockData.getRefBlockPrefix()); // Formatting expiration time - Date expirationTime = new Date(blockData.getExpiration() * 1000); + Date expirationTime = new Date(blockData.getExpiration()); SimpleDateFormat dateFormat = new SimpleDateFormat(Util.TIME_DATE_FORMAT); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); @@ -267,29 +306,42 @@ public Transaction deserialize(JsonElement json, Type typeOfT, JsonDeserializati Date expirationDate = dateFormat.parse(expiration, new ParsePosition(0)); BlockData blockData = new BlockData(refBlockNum, refBlockPrefix, expirationDate.getTime()); + // Parsing chainId + String chainId = null == jsonObject.get(KEY_CHAIN_ID) ? null : jsonObject.get(KEY_CHAIN_ID).getAsString(); + + // Parsing signatures + String signature = null; + try { + signature = null == jsonObject.get(KEY_SIGNATURES) ? null : jsonObject.get(KEY_SIGNATURES).getAsString(); + } catch (Exception e) { + } + // 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()) { + if (operationId == OperationType.TRANSFER_OPERATION.getCode()) { operation = context.deserialize(jsonOperation, TransferOperation.class); - } else if (operationId == OperationType.LIMIT_ORDER_CREATE_OPERATION.ordinal()) { + } else if (operationId == OperationType.LIMIT_ORDER_CREATE_OPERATION.getCode()) { operation = context.deserialize(jsonOperation, LimitOrderCreateOperation.class); + } else if (operationId == OperationType.ACCOUNT_CREATE_OPERATION.getCode()) { + operation = context.deserialize(jsonOperation, AccountCreateOperation.class); } - if (operation != null) + if (operation != null) { operationList.add(operation); + } operation = null; } - return new Transaction(blockData, operationList); + return new Transaction(chainId, blockData, operationList, signature); } catch (Exception e) { LOGGER.info("Exception. Msg: " + e.getMessage()); for (StackTraceElement el : e.getStackTrace()) { LOGGER.info(el.getFileName() + "#" + el.getMethodName() + ":" + el.getLineNumber()); } } - return new Transaction(blockData, operationList); + return new Transaction(chainId, blockData, operationList, signature); } } } diff --git a/src/main/java/com/gxchain/client/graphenej/operations/AccountCreateOperation.java b/src/main/java/com/gxchain/client/graphenej/operations/AccountCreateOperation.java index a650102..bbae1f8 100644 --- a/src/main/java/com/gxchain/client/graphenej/operations/AccountCreateOperation.java +++ b/src/main/java/com/gxchain/client/graphenej/operations/AccountCreateOperation.java @@ -2,15 +2,14 @@ import com.google.common.primitives.Bytes; import com.google.common.primitives.UnsignedLong; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.google.gson.*; import com.gxchain.client.graphenej.Varint; import com.gxchain.client.graphenej.enums.OperationType; import com.gxchain.client.graphenej.objects.*; import lombok.Data; +import java.lang.reflect.Type; + /** * Class used to encapsulate operations related to the ACCOUNT_CREATE_OPERATION. */ @@ -56,7 +55,35 @@ public AccountCreateOperation(UserAccount registrar, UserAccount referrer, Autho this.owner = owner; this.active = active; this.options = options; - extensions = new Extensions(); + this.extensions = new Extensions(); + } + + /** + * Account create operation constructor. + * + * @param registrar register account + * @param referrer + * @param owner Owner authority to set. Can be null. + * @param active Active authority to set. Can be null. + * @param options Can be null + * @param fee The fee to pay. Can be null. + * @param referrerPercent + * @param name register name + */ + public AccountCreateOperation(UserAccount registrar, UserAccount referrer, + Authority owner, Authority active, + AccountOptions options, AssetAmount fee, + int referrerPercent, String name) { + super(OperationType.ACCOUNT_CREATE_OPERATION); + this.fee = fee; + this.registrar = registrar; + this.referrer = referrer; + this.owner = owner; + this.active = active; + this.options = options; + this.extensions = new Extensions(); + this.referrerPercent = referrerPercent; + this.name = name; } public AccountCreateOperation(UserAccount registrar, UserAccount referrer, Authority owner, Authority active, AccountOptions options) { @@ -118,4 +145,45 @@ public byte[] toBytes() { byte[] extensionBytes = extensions.toBytes(); return Bytes.concat(feeBytes, registrarBytes, referrerBytes, referrerPercentBytes, new byte[]{(byte) this.name.length()}, nameBytes, ownerBytes, activeBytes, optionsBytes, extensionBytes); } + + public static class AccountCreateSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(AccountCreateOperation accountCreate, Type type, JsonSerializationContext jsonSerializationContext) { + return accountCreate.toJsonObject(); + } + } + + public static class AccountCreateDeserializer implements JsonDeserializer { + + @Override + public AccountCreateOperation deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json.isJsonArray()) { + JsonArray serializedTransfer = json.getAsJsonArray(); + if (serializedTransfer.get(0).getAsInt() != OperationType.ACCOUNT_CREATE_OPERATION.getCode()) { + return null; + } else { + return context.deserialize(serializedTransfer.get(1), AccountCreateOperation.class); + } + } else { + JsonObject jsonObject = json.getAsJsonObject(); + + AssetAmount fee = context.deserialize(jsonObject.get(KEY_FEE), AssetAmount.class); + Authority owner = context.deserialize(jsonObject.get(KEY_OWNER), Authority.class); + Authority active = context.deserialize(jsonObject.get(KEY_ACTIVE), Authority.class); + AccountOptions options = context.deserialize(jsonObject.get(KEY_OPTIONS), AccountOptions.class); + + UserAccount registrar = new UserAccount(jsonObject.get(KEY_REGISTRAR).getAsString()); + UserAccount referrer = new UserAccount(jsonObject.get(KEY_REFERRER).getAsString()); + + int referrerPercent = jsonObject.get(KEY_REFERRER_PERCENT).getAsInt(); + String name = jsonObject.get(KEY_NAME).getAsString(); + + AccountCreateOperation accountCreate = new AccountCreateOperation( + registrar, referrer, owner, active, options, fee, referrerPercent, name); + + return accountCreate; + } + } + } } diff --git a/src/main/java/com/gxchain/client/graphenej/operations/LimitOrderCreateOperation.java b/src/main/java/com/gxchain/client/graphenej/operations/LimitOrderCreateOperation.java index c5972a1..a2ce381 100644 --- a/src/main/java/com/gxchain/client/graphenej/operations/LimitOrderCreateOperation.java +++ b/src/main/java/com/gxchain/client/graphenej/operations/LimitOrderCreateOperation.java @@ -188,7 +188,7 @@ public LimitOrderCreateOperation deserialize(JsonElement json, Type typeOfT, Jso // 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.LIMIT_ORDER_CREATE_OPERATION.ordinal()){ + if(serializedTransfer.get(0).getAsInt() != OperationType.LIMIT_ORDER_CREATE_OPERATION.getCode()){ // If the operation type does not correspond to a transfer operation, we return null return null; }else{ diff --git a/src/main/java/com/gxchain/client/graphenej/operations/TransferOperation.java b/src/main/java/com/gxchain/client/graphenej/operations/TransferOperation.java index 279e613..ae3723c 100644 --- a/src/main/java/com/gxchain/client/graphenej/operations/TransferOperation.java +++ b/src/main/java/com/gxchain/client/graphenej/operations/TransferOperation.java @@ -173,7 +173,7 @@ public TransferOperation deserialize(JsonElement json, Type typeOfT, JsonDeseria // 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 (serializedTransfer.get(0).getAsInt() != OperationType.TRANSFER_OPERATION.getCode()) { // If the operation type does not correspond to a transfer operation, we return null return null; } else { diff --git a/src/main/java/com/gxchain/client/util/GXGsonUtil.java b/src/main/java/com/gxchain/client/util/GXGsonUtil.java index f9e65b2..65dc4fd 100644 --- a/src/main/java/com/gxchain/client/util/GXGsonUtil.java +++ b/src/main/java/com/gxchain/client/util/GXGsonUtil.java @@ -2,14 +2,13 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; import com.gxchain.client.graphenej.models.ApiCall; import com.gxchain.client.graphenej.models.DynamicGlobalProperties; import com.gxchain.client.graphenej.objects.*; +import com.gxchain.client.graphenej.operations.AccountCreateOperation; import com.gxchain.client.graphenej.operations.TransferOperation; import java.lang.reflect.Type; -import java.util.List; /** * @author liruobin @@ -21,6 +20,7 @@ public class GXGsonUtil { static { builder.registerTypeAdapter(Transaction.class, new Transaction.TransactionDeserializer()); builder.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer()); + builder.registerTypeAdapter(AccountCreateOperation.class, new AccountCreateOperation.AccountCreateDeserializer()); builder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer()); builder.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer()); builder.registerTypeAdapter(DynamicGlobalProperties.class, new DynamicGlobalProperties.DynamicGlobalPropertiesDeserializer()); diff --git a/src/test/java/com/gxchain/client/util/GXGsonUtilTest.java b/src/test/java/com/gxchain/client/util/GXGsonUtilTest.java new file mode 100644 index 0000000..ce10918 --- /dev/null +++ b/src/test/java/com/gxchain/client/util/GXGsonUtilTest.java @@ -0,0 +1,30 @@ +package com.gxchain.client.util; + +import com.gxchain.client.graphenej.enums.OperationType; +import com.gxchain.client.graphenej.objects.Transaction; +import org.junit.Assert; +import org.junit.Test; + +public class GXGsonUtilTest { + + + @Test + public void operationTypeTest() { + Assert.assertEquals(5, OperationType.ACCOUNT_CREATE_OPERATION.getCode()); + } + + + @Test + public void transactionDeserializerTest() { + String hex = "{\"ref_block_num\":7971,\"ref_block_prefix\":2672178873,\"expiration\":\"2019-04-01T13:16:24\"," + + "\"operations\":[" + + "[5,{\"fee\":{\"amount\":1000,\"asset_id\":\"1.3.1\"},\"extensions\":[],\"registrar\":\"1.2.1131733\",\"referrer\":\"1.2.1131733\",\"referrer_percent\":10000," + + "\"name\":\"cherryreg22\",\"owner\":{\"weight_threshold\":1,\"account_auths\":[],\"key_auths\":[[\"GXC5u2vbL1ME9PBnJTRtBHRr8JCM7hDEiY4fJS5mY3HjyhvRj8tAP\",1]],\"address_auths\":[]},\"active\":{\"weight_threshold\":1,\"account_auths\":[],\"key_auths\":[[\"GXC5u2vbL1ME9PBnJTRtBHRr8JCM7hDEiY4fJS5mY3HjyhvRj8tAP\",1]],\"address_auths\":[]},\"options\":{\"memo_key\":\"GXC5u2vbL1ME9PBnJTRtBHRr8JCM7hDEiY4fJS5mY3HjyhvRj8tAP\",\"num_committee\":0,\"num_witness\":0,\"voting_account\":\"1.2.5\",\"votes\":[],\"extensions\":[]}}]],\"signatures\":[\"206b20fccb39d914c4af2682c2552f948a7e12039d08d70fad788e9d62a2724b511608ae8c754067e08eb83ba6131910c655f36de26c568c97cca18e54d3c271fe\"],\"extensions\":[]}"; + Transaction transaction = GXGsonUtil.fromJson(hex, Transaction.class); + transaction.setChainId("4f7d07969c446f8342033acb3ab2ae5044cbe0fde93db02de75bd17fa8fd84b8"); + + System.out.println(transaction.toJsonString()); + } + + +}