diff --git a/src/main/java/de/bitsharesmunich/graphenej/AccountUpdateOperation.java b/src/main/java/de/bitsharesmunich/graphenej/AccountUpdateOperation.java index 7c29c1f..b97f230 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/AccountUpdateOperation.java +++ b/src/main/java/de/bitsharesmunich/graphenej/AccountUpdateOperation.java @@ -8,7 +8,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; /** - * Class used to encapsulate operations related to the account_update_operation. + * Class used to encapsulate operations related to the ACCOUNT_UPDATE_OPERATION. */ public class AccountUpdateOperation extends BaseOperation { public static final String KEY_ACCOUNT = "account"; @@ -34,7 +34,7 @@ public class AccountUpdateOperation extends BaseOperation { * @param fee The fee to pay. Can be null. */ public AccountUpdateOperation(UserAccount account, Authority owner, Authority active, AccountOptions options, AssetAmount fee){ - super(OperationType.account_update_operation); + super(OperationType.ACCOUNT_UPDATE_OPERATION); this.fee = fee; this.account = account; this.owner = new Optional<>(owner); diff --git a/src/main/java/de/bitsharesmunich/graphenej/BlockData.java b/src/main/java/de/bitsharesmunich/graphenej/BlockData.java index ad789b6..4a65917 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/BlockData.java +++ b/src/main/java/de/bitsharesmunich/graphenej/BlockData.java @@ -12,7 +12,7 @@ public class BlockData implements ByteSerializable { private int refBlockNum; private long refBlockPrefix; - private long relativeExpiration; + private long expiration; /** * Block data constructor @@ -22,22 +22,20 @@ public class BlockData implements ByteSerializable { * Recall that block IDs have 32 bits of block number followed by the * actual block hash, so this field should be set using the second 32 bits * in the block_id_type - * @param relative_expiration: This field specifies the number of block intervals after the - * reference block until this transaction becomes invalid. If this field is - * set to zero, the "ref_block_prefix" is interpreted as an absolute timestamp - * of the time the transaction becomes invalid. + * @param relative_expiration: Expiration time specified as a POSIX or + * Unix time */ public BlockData(int ref_block_num, long ref_block_prefix, long relative_expiration){ this.refBlockNum = ref_block_num; this.refBlockPrefix = ref_block_prefix; - this.relativeExpiration = relative_expiration; + this.expiration = relative_expiration; } /** * Block data constructor that takes in raw blockchain information. * @param head_block_number: The last block number. * @param head_block_id: The last block apiId. - * @param relative_expiration: The relative expiration + * @param relative_expiration: The expiration time. */ public BlockData(long head_block_number, String head_block_id, long relative_expiration){ String hashData = head_block_id.substring(8, 16); @@ -47,7 +45,7 @@ public class BlockData implements ByteSerializable { } this.setRefBlockNum(head_block_number); this.setRefBlockPrefix(head_block_id); - this.relativeExpiration = relative_expiration; + this.expiration = relative_expiration; } /** @@ -97,12 +95,12 @@ public class BlockData implements ByteSerializable { return refBlockPrefix; } - public long getRelativeExpiration() { - return relativeExpiration; + public long getExpiration() { + return expiration; } - public void setRelativeExpiration(long relativeExpiration) { - this.relativeExpiration = relativeExpiration; + public void setExpiration(long expiration) { + this.expiration = expiration; } @@ -120,7 +118,7 @@ public class BlockData implements ByteSerializable { }else if(i >= REF_BLOCK_NUM_BYTES && i < REF_BLOCK_NUM_BYTES + REF_BLOCK_PREFIX_BYTES){ result[i] = (byte) (this.refBlockPrefix >> 8 * (i - REF_BLOCK_NUM_BYTES)); }else{ - result[i] = (byte) (this.relativeExpiration >> 8 * (i - REF_BLOCK_NUM_BYTES + REF_BLOCK_PREFIX_BYTES)); + result[i] = (byte) (this.expiration >> 8 * (i - REF_BLOCK_NUM_BYTES + REF_BLOCK_PREFIX_BYTES)); } } return result; diff --git a/src/main/java/de/bitsharesmunich/graphenej/GrapheneObject.java b/src/main/java/de/bitsharesmunich/graphenej/GrapheneObject.java index 52b7151..88f20a4 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/GrapheneObject.java +++ b/src/main/java/de/bitsharesmunich/graphenej/GrapheneObject.java @@ -8,6 +8,8 @@ package de.bitsharesmunich.graphenej; * Created by nelson on 11/8/16. */ public class GrapheneObject { + public static final String KEY_ID = "id"; + public static final int PROTOCOL_SPACE = 1; public static final int IMPLEMENTATION_SPACE = 2; diff --git a/src/main/java/de/bitsharesmunich/graphenej/Main.java b/src/main/java/de/bitsharesmunich/graphenej/Main.java index 8789f9d..a56442e 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/Main.java +++ b/src/main/java/de/bitsharesmunich/graphenej/Main.java @@ -73,6 +73,7 @@ public class Main { // test.testAssetSerialization(); // test.testGetMarketHistory(); // test.testGetAccountBalances(); - test.testGetAssetHoldersCount(); +// test.testGetAssetHoldersCount(); + test.testSubscription(null); } } diff --git a/src/main/java/de/bitsharesmunich/graphenej/OperationType.java b/src/main/java/de/bitsharesmunich/graphenej/OperationType.java index 2f54488..36d9f52 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/OperationType.java +++ b/src/main/java/de/bitsharesmunich/graphenej/OperationType.java @@ -1,52 +1,55 @@ package de.bitsharesmunich.graphenej; /** + * Enum type used to keep track of all the operation types and their corresponding ids. + * + * Source + * * Created by nelson on 11/6/16. */ public enum OperationType { - transfer_operation, - limit_order_create_operation, - limit_order_cancel_operation, - call_order_update_operation, - fill_order_operation, // VIRTUAL - account_create_operation, - account_update_operation, - account_whitelist_operation, - account_upgrade_operation, - account_transfer_operation, - asset_create_operation, - asset_update_operation, - asset_update_bitasset_operation, - asset_update_feed_producers_operation, - asset_issue_operation, - asset_reserve_operation, - asset_fund_fee_pool_operation, - asset_settle_operation, - asset_global_settle_operation, - asset_publish_feed_operation, - witness_create_operation, - witness_update_operation, - proposal_create_operation, - proposal_update_operation, - proposal_delete_operation, - withdraw_permission_create_operation, - withdraw_permission_update_operation, - withdraw_permission_claim_operation, - withdraw_permission_delete_operation, - committee_member_create_operation, - committee_member_update_operation, - committee_member_update_global_parameters_operation, - vesting_balance_create_operation, - vesting_balance_withdraw_operation, - worker_create_operation, - custom_operation, - assert_operation, - balance_claim_operation, - override_transfer_operation, - transfer_to_blind_operation, - blind_transfer_operation, - transfer_from_blind_operation, - asset_settle_cancel_operation, // VIRTUAL - asset_claim_fees_operation, - fba_distribute_operation // VIRTUAL + TRANSFER_OPERATION, + LIMIT_ORDER_CREATE_OPERATION, + LIMIT_ORDER_CANCEL_OPERATION, + CALL_ORDER_UPDATE_OPERATION, + FILL_ORDER_OPERATION, // VIRTUAL + ACCOUNT_CREATE_OPERATION, + ACCOUNT_UPDATE_OPERATION, + ACCOUNT_WHITELIST_OPERATION, + ACCOUNT_UPGRADE_OPERATION, + ACCOUNT_TRANSFER_OPERATION, + ASSET_CREATE_OPERATION, + ASSET_UPDATE_OPERATION, + ASSET_UPDATE_BITASSET_OPERATION, + ASSET_UPDATE_FEED_PRODUCERS_OPERATION, + ASSET_ISSUE_OPERATION, + ASSET_RESERVE_OPERATION, + ASSET_FUND_FEE_POOL_OPERATION, + ASSET_SETTLE_OPERATION, + ASSET_GLOBAL_SETTLE_OPERATION, + ASSET_PUBLISH_FEED_OPERATION, + WITNESS_CREATE_OPERATION, + WITNESS_UPDATE_OPERATION, + PROPOSAL_CREATE_OPERATION, + PROPOSAL_UPDATE_OPERATION, + PROPOSAL_DELETE_OPERATION, + WITHDRAW_PERMISSION_CREATE_OPERATION, + WITHDRAW_PERMISSION_UPDATE_OPERATION, + WITHDRAW_PERMISSION_CLAIM_OPERATION, + WITHDRAW_PERMISSION_DELETE_OPERATION, + COMMITTEE_MEMBER_CREATE_OPERATION, + COMMITTEE_MEMBER_UPDATE_OPERATION, + COMMITTEE_MEMBER_UPDATE_GLOBAL_PARAMETERS_OPERATION, + VESTING_BALANCE_CREATE_OPERATION, + VESTING_BALANCE_WITHDRAW_OPERATION, + WORKER_CREATE_OPERATION, + CUSTOM_OPERATION, + ASSERT_OPERATION, + BALANCE_CLAIM_OPERATION, + OVERRIDE_TRANSFER_OPERATION, + TRANSFER_TO_BLIND_OPERATION, + BLIND_TRANSFER_OPERATION, + TRANSFER_FROM_BLIND_OPERATION, + ASSET_SETTLE_CANCEL_OPERATION, // VIRTUAL + ASSET_CLAIM_FEES_OPERATION } diff --git a/src/main/java/de/bitsharesmunich/graphenej/RPC.java b/src/main/java/de/bitsharesmunich/graphenej/RPC.java index 7e15568..bf97c50 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/RPC.java +++ b/src/main/java/de/bitsharesmunich/graphenej/RPC.java @@ -10,6 +10,7 @@ public class RPC { public static final String CALL_HISTORY = "history"; public static final String CALL_DATABASE = "database"; public static final String CALL_ASSET = "asset"; + public static final String CALL_SET_SUBSCRIBE_CALLBACK = "set_subscribe_callback"; public static final String CALL_GET_ACCOUNT_BY_NAME = "get_account_by_name"; public static final String CALL_GET_ACCOUNTS = "get_accounts"; public static final String CALL_GET_DYNAMIC_GLOBAL_PROPERTIES = "get_dynamic_global_properties"; diff --git a/src/main/java/de/bitsharesmunich/graphenej/Test.java b/src/main/java/de/bitsharesmunich/graphenej/Test.java index f500011..237cc25 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/Test.java +++ b/src/main/java/de/bitsharesmunich/graphenej/Test.java @@ -1,5 +1,6 @@ package de.bitsharesmunich.graphenej; +import de.bitsharesmunich.graphenej.interfaces.SubscriptionListener; import de.bitsharesmunich.graphenej.models.*; import de.bitsharesmunich.graphenej.objects.Memo; import com.google.common.primitives.UnsignedLong; @@ -14,12 +15,10 @@ import de.bitsharesmunich.graphenej.test.NaiveSSLContext; import com.neovisionaries.ws.client.*; import de.bitsharesmunich.graphenej.api.*; import org.bitcoinj.core.*; -import org.spongycastle.asn1.x509.Holder; import org.spongycastle.crypto.digests.RIPEMD160Digest; import javax.net.ssl.SSLContext; import java.io.*; -import java.lang.reflect.Array; import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; @@ -592,7 +591,7 @@ public class Test { // Set the custom SSL context. factory.setSSLContext(context); - WebSocket mWebSocket = factory.createSocket(OPENLEDGER_WITNESS_URL); + WebSocket mWebSocket = factory.createSocket(AMAZON_WITNESS); mWebSocket.addListener(relativeAccountHistory); mWebSocket.connect(); } catch (IOException e) { @@ -1190,8 +1189,8 @@ public class Test { @Override public void onSuccess(WitnessResponse response) { System.out.println("onSuccess"); - List holdersCountList = (List) response.result; - for(HoldersCount holdersCount : holdersCountList){ + List holdersCountList = (List) response.result; + for(AssetHolderCount holdersCount : holdersCountList){ System.out.println(String.format("Asset %s has %d holders", holdersCount.asset.getObjectId(), holdersCount.count)); } } @@ -1221,4 +1220,60 @@ public class Test { System.out.println("IOException. Msg: " + e.getMessage()); } } + + public void testSubscription(WitnessResponseListener listener){ + SSLContext context = null; + + try { + context = NaiveSSLContext.getInstance("TLS"); + WebSocketFactory factory = new WebSocketFactory(); + + // Set the custom SSL context. + factory.setSSLContext(context); + + WebSocket mWebSocket = factory.createSocket(BLOCK_PAY_DE); + + SubscriptionMessagesHub subscriptionHub = new SubscriptionMessagesHub("", ""); + mWebSocket.addListener(subscriptionHub); + mWebSocket.connect(); + subscriptionHub.addSubscriptionListener(new SubscriptionListener() { + @Override + public ObjectType getInterestObjectType() { + return ObjectType.TRANSACTION_OBJECT; + } + + @Override + public void onSubscriptionUpdate(SubscriptionResponse response) { + try{ + List updatedObjects = (List) response.params.get(1); + if(updatedObjects.size() > 0){ + for(Serializable update : updatedObjects){ + if(update instanceof BroadcastedTransaction){ + Transaction t = ((BroadcastedTransaction) update).getTransaction(); + if(t.getOperations().size() > 0){ + for(BaseOperation op : t.getOperations()){ + if(op instanceof TransferOperation){ + System.out.println(String.format("Got transaction from: %s, to: %s", ((TransferOperation) op).getFrom().getObjectId(), ((TransferOperation) op).getTo().getObjectId())); + } + } + } + } + } + } + }catch(Exception e){ + System.out.println("Exception. Msg: "+e.getMessage()); + for(StackTraceElement el : e.getStackTrace()){ + System.out.println(el.getFileName()+"#"+el.getMethodName()+":"+el.getLineNumber()); + } + } + } + }); + } catch (NoSuchAlgorithmException e) { + System.out.println("NoSuchAlgorithmException. Msg: " + e.getMessage()); + } catch (WebSocketException e) { + System.out.println("WebSocketException. Msg: " + e.getMessage()); + } catch (IOException e) { + System.out.println("IOException. Msg: " + e.getMessage()); + } + } } diff --git a/src/main/java/de/bitsharesmunich/graphenej/Transaction.java b/src/main/java/de/bitsharesmunich/graphenej/Transaction.java index 244247c..e7d64cf 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/Transaction.java +++ b/src/main/java/de/bitsharesmunich/graphenej/Transaction.java @@ -1,12 +1,7 @@ package de.bitsharesmunich.graphenej; 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.google.gson.*; import de.bitsharesmunich.graphenej.interfaces.ByteSerializable; import de.bitsharesmunich.graphenej.interfaces.JsonSerializable; @@ -16,6 +11,7 @@ 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.Date; @@ -26,15 +22,18 @@ 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(); + /* 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"; + public static final String TIME_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; private ECKey privateKey; private BlockData blockData; @@ -64,6 +63,17 @@ public class Transaction implements ByteSerializable, JsonSerializable { 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; + } + /** * Updates the block data * @param blockData: New block data @@ -87,6 +97,14 @@ public class Transaction implements ByteSerializable, JsonSerializable { 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 @@ -125,7 +143,7 @@ public class Transaction implements ByteSerializable, JsonSerializable { 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); + this.blockData.setExpiration(this.blockData.getExpiration() + 1); }else{ isGrapheneCanonical = true; } @@ -186,8 +204,8 @@ public class Transaction implements ByteSerializable, JsonSerializable { byte[] signature = getGrapheneSignature(); // Formatting expiration time - Date expirationTime = new Date(blockData.getRelativeExpiration() * 1000); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + Date expirationTime = new Date(blockData.getExpiration() * 1000); + SimpleDateFormat dateFormat = new SimpleDateFormat(TIME_DATE_FORMAT); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); // Adding expiration @@ -216,11 +234,144 @@ public class Transaction implements ByteSerializable, JsonSerializable { } - class TransactionSerializer implements JsonSerializer { + /** + * 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(TIME_DATE_FORMAT); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + Date expirationDate = dateFormat.parse(expiration, new ParsePosition(0)); + BlockData blockData = new BlockData(refBlockNum, refBlockPrefix, expirationDate.getTime()); + + // 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()) { + System.out.println("Transfer operation detected!"); + operation = context.deserialize(jsonOperation, TransferOperation.class); + } else if (operationId == OperationType.LIMIT_ORDER_CREATE_OPERATION.ordinal()) { + //TODO: Add operation deserialization support + } 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()) { + //TODO: Add operation deserialization support + } 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 + } + 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); + } + } } \ No newline at end of file diff --git a/src/main/java/de/bitsharesmunich/graphenej/TransferOperation.java b/src/main/java/de/bitsharesmunich/graphenej/TransferOperation.java index 967afc1..bc0bae1 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/TransferOperation.java +++ b/src/main/java/de/bitsharesmunich/graphenej/TransferOperation.java @@ -26,7 +26,7 @@ public class TransferOperation extends BaseOperation { private String[] extensions; public TransferOperation(UserAccount from, UserAccount to, AssetAmount transferAmount, AssetAmount fee){ - super(OperationType.transfer_operation); + super(OperationType.TRANSFER_OPERATION); this.from = from; this.to = to; this.amount = transferAmount; @@ -35,7 +35,7 @@ public class TransferOperation extends BaseOperation { } public TransferOperation(UserAccount from, UserAccount to, AssetAmount transferAmount){ - super(OperationType.transfer_operation); + super(OperationType.TRANSFER_OPERATION); this.from = from; this.to = to; this.amount = transferAmount; @@ -144,7 +144,7 @@ public class TransferOperation extends BaseOperation { // 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.ordinal()){ // If the operation type does not correspond to a transfer operation, we return null return null; }else{ diff --git a/src/main/java/de/bitsharesmunich/graphenej/api/GetAllAssetHolders.java b/src/main/java/de/bitsharesmunich/graphenej/api/GetAllAssetHolders.java index bfb129d..9d92f8b 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/api/GetAllAssetHolders.java +++ b/src/main/java/de/bitsharesmunich/graphenej/api/GetAllAssetHolders.java @@ -63,10 +63,10 @@ public class GetAllAssetHolders extends BaseGrapheneHandler { ApiCall apiCall = new ApiCall(assetApiId, RPC.CALL_GET_ALL_ASSET_HOLDERS, emptyParams, RPC.VERSION, currentId); websocket.sendText(apiCall.toJsonString()); } else if (baseResponse.id == GET_ALL_ASSET_HOLDERS_COUNT) { - Type AssetTokenHolders = new TypeToken>>(){}.getType(); + Type AssetTokenHolders = new TypeToken>>(){}.getType(); GsonBuilder builder = new GsonBuilder(); - builder.registerTypeAdapter(HoldersCount.class, new HoldersCount.HoldersCountDeserializer()); - WitnessResponse> witnessResponse = builder.create().fromJson(response, AssetTokenHolders); + builder.registerTypeAdapter(AssetHolderCount.class, new AssetHolderCount.HoldersCountDeserializer()); + WitnessResponse> witnessResponse = builder.create().fromJson(response, AssetTokenHolders); mListener.onSuccess(witnessResponse); websocket.disconnect(); }else{ diff --git a/src/main/java/de/bitsharesmunich/graphenej/api/SubscriptionMessagesHub.java b/src/main/java/de/bitsharesmunich/graphenej/api/SubscriptionMessagesHub.java new file mode 100644 index 0000000..7180f28 --- /dev/null +++ b/src/main/java/de/bitsharesmunich/graphenej/api/SubscriptionMessagesHub.java @@ -0,0 +1,116 @@ +package de.bitsharesmunich.graphenej.api; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.neovisionaries.ws.client.WebSocket; +import com.neovisionaries.ws.client.WebSocketAdapter; +import com.neovisionaries.ws.client.WebSocketException; +import com.neovisionaries.ws.client.WebSocketFrame; +import de.bitsharesmunich.graphenej.AssetAmount; +import de.bitsharesmunich.graphenej.RPC; +import de.bitsharesmunich.graphenej.Transaction; +import de.bitsharesmunich.graphenej.TransferOperation; +import de.bitsharesmunich.graphenej.interfaces.SubscriptionListener; +import de.bitsharesmunich.graphenej.models.ApiCall; +import de.bitsharesmunich.graphenej.models.BaseResponse; +import de.bitsharesmunich.graphenej.models.SubscriptionResponse; +import de.bitsharesmunich.graphenej.models.WitnessResponse; + +import java.io.Serializable; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * A websocket adapter prepared to be used as a basic dispatch hub for subscription messages. + * + * Created by nelson on 1/26/17. + */ +public class SubscriptionMessagesHub extends WebSocketAdapter { + // Sequence of message ids + private final static int LOGIN_ID = 1; + private final static int GET_DATABASE_ID = 2; + private final static int SUBCRIPTION_REQUEST = 3; + + // ID of subscription notifications + private final static int SUBCRIPTION_NOTIFICATION = 4; + + private SubscriptionResponse.SubscriptionResponseDeserializer mSubscriptionDeserializer; + private Gson gson; + private String user; + private String password; + private int currentId = LOGIN_ID; + private int databaseApiId = -1; + + public SubscriptionMessagesHub(String user, String password){ + this.user = user; + this.password = password; + this.mSubscriptionDeserializer = new SubscriptionResponse.SubscriptionResponseDeserializer(); + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(SubscriptionResponse.class, mSubscriptionDeserializer); + builder.registerTypeAdapter(Transaction.class, new Transaction.TransactionDeserializer()); + builder.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer()); + builder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer()); + this.gson = builder.create(); + } + + public void addSubscriptionListener(SubscriptionListener listener){ + this.mSubscriptionDeserializer.addSubscriptionListener(listener); + } + + public void removeSubscriptionListener(SubscriptionListener listener){ + this.removeSubscriptionListener(listener); + } + + @Override + public void onConnected(WebSocket websocket, Map> headers) throws Exception { + ArrayList loginParams = new ArrayList<>(); + loginParams.add(user); + loginParams.add(password); + ApiCall loginCall = new ApiCall(1, RPC.CALL_LOGIN, loginParams, RPC.VERSION, currentId); + websocket.sendText(loginCall.toJsonString()); + } + + @Override + public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + String message = frame.getPayloadText(); + System.out.println("<< "+message); + if(currentId == LOGIN_ID){ + ArrayList emptyParams = new ArrayList<>(); + ApiCall getDatabaseId = new ApiCall(1, RPC.CALL_DATABASE, emptyParams, RPC.VERSION, currentId); + websocket.sendText(getDatabaseId.toJsonString()); + }else if(currentId == GET_DATABASE_ID){ + Type ApiIdResponse = new TypeToken>() {}.getType(); + WitnessResponse witnessResponse = gson.fromJson(message, ApiIdResponse); + databaseApiId = witnessResponse.result; + + ArrayList subscriptionParams = new ArrayList<>(); + subscriptionParams.add(String.format("%d", SUBCRIPTION_NOTIFICATION)); + subscriptionParams.add(false); + ApiCall getDatabaseId = new ApiCall(databaseApiId, RPC.CALL_SET_SUBSCRIBE_CALLBACK, subscriptionParams, RPC.VERSION, currentId); + websocket.sendText(getDatabaseId.toJsonString()); + }else if(currentId == SUBCRIPTION_REQUEST){ + // Listeners are called from within the SubscriptionResponseDeserializer, so there's nothing to handle here. + }else{ + SubscriptionResponse subscriptionResponse = gson.fromJson(message, SubscriptionResponse.class); + } + currentId++; + } + + @Override + public void onFrameSent(WebSocket websocket, WebSocketFrame frame) throws Exception { + System.out.println(">> "+frame.getPayloadText()); + } + + @Override + public void onError(WebSocket websocket, WebSocketException cause) throws Exception { + super.onError(websocket, cause); + } + + @Override + public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception { + super.handleCallbackError(websocket, cause); + } +} diff --git a/src/main/java/de/bitsharesmunich/graphenej/interfaces/SubscriptionListener.java b/src/main/java/de/bitsharesmunich/graphenej/interfaces/SubscriptionListener.java new file mode 100644 index 0000000..9081991 --- /dev/null +++ b/src/main/java/de/bitsharesmunich/graphenej/interfaces/SubscriptionListener.java @@ -0,0 +1,30 @@ +package de.bitsharesmunich.graphenej.interfaces; + +import de.bitsharesmunich.graphenej.ObjectType; +import de.bitsharesmunich.graphenej.models.SubscriptionResponse; + +/** + * Generic interface that must be implemented by any class that wants to be informed about a specific + * event notification. + * + * Created by nelson on 1/26/17. + */ +public interface SubscriptionListener { + + /** + * Every subscription listener must implement a method that returns the type of object it is + * interested in. + * @return: Instance of the ObjectType enum class. + */ + ObjectType getInterestObjectType(); + + + /** + * Method called whenever there is an update that might be of interest for this listener. + * Note however that the objects returned inside the SubscriptionResponse are not guaranteed to be + * only of the object type requested by this class in the getInterestObjectType. + * + * @param response: SubscriptionResponse instance, which may or may not contain an object of interest. + */ + void onSubscriptionUpdate(SubscriptionResponse response); +} diff --git a/src/main/java/de/bitsharesmunich/graphenej/models/ApiCall.java b/src/main/java/de/bitsharesmunich/graphenej/models/ApiCall.java index e07416e..2fbaa55 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/models/ApiCall.java +++ b/src/main/java/de/bitsharesmunich/graphenej/models/ApiCall.java @@ -75,19 +75,21 @@ public class ApiCall implements JsonSerializable { }else if(this.params.get(i) instanceof String || this.params.get(i) == null){ // Other times they are plain strings methodParams.add((String) this.params.get(i)); - }else if(this.params.get(i) instanceof ArrayList){ + }else if(this.params.get(i) instanceof ArrayList) { // Other times it might be an array JsonArray array = new JsonArray(); ArrayList listArgument = (ArrayList) this.params.get(i); - for(int l = 0; l < listArgument.size(); l++){ + for (int l = 0; l < listArgument.size(); l++) { Serializable element = listArgument.get(l); - if(element instanceof JsonSerializable) + if (element instanceof JsonSerializable) array.add(((JsonSerializable) element).toJsonObject()); - else if(element instanceof String){ + else if (element instanceof String) { array.add((String) element); } } methodParams.add(array); + }else if(this.params.get(i) instanceof Boolean){ + methodParams.add((boolean) this.params.get(i)); }else{ System.out.println("Skipping parameter of type: "+this.params.get(i).getClass()); } diff --git a/src/main/java/de/bitsharesmunich/graphenej/models/HoldersCount.java b/src/main/java/de/bitsharesmunich/graphenej/models/AssetHolderCount.java similarity index 72% rename from src/main/java/de/bitsharesmunich/graphenej/models/HoldersCount.java rename to src/main/java/de/bitsharesmunich/graphenej/models/AssetHolderCount.java index 4804a8c..9de3ddc 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/models/HoldersCount.java +++ b/src/main/java/de/bitsharesmunich/graphenej/models/AssetHolderCount.java @@ -8,19 +8,19 @@ import java.lang.reflect.Type; /** * Created by nelson on 1/25/17. */ -public class HoldersCount { +public class AssetHolderCount { public static final String KEY_ASSET_ID = "asset_id"; public static final String KEY_COUNT = "count"; public Asset asset; public long count; - public static class HoldersCountDeserializer implements JsonDeserializer { + public static class HoldersCountDeserializer implements JsonDeserializer { @Override - public HoldersCount deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + public AssetHolderCount deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { JsonObject jsonObject = json.getAsJsonObject(); - HoldersCount holdersCount = new HoldersCount(); + AssetHolderCount holdersCount = new AssetHolderCount(); holdersCount.asset = new Asset(jsonObject.get(KEY_ASSET_ID).getAsString()); holdersCount.count = jsonObject.get(KEY_COUNT).getAsLong(); return holdersCount; diff --git a/src/main/java/de/bitsharesmunich/graphenej/models/BroadcastedTransaction.java b/src/main/java/de/bitsharesmunich/graphenej/models/BroadcastedTransaction.java new file mode 100644 index 0000000..ee53101 --- /dev/null +++ b/src/main/java/de/bitsharesmunich/graphenej/models/BroadcastedTransaction.java @@ -0,0 +1,42 @@ +package de.bitsharesmunich.graphenej.models; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import de.bitsharesmunich.graphenej.GrapheneObject; +import de.bitsharesmunich.graphenej.Transaction; + +import java.io.Serializable; +import java.lang.reflect.Type; + +/** + * Created by nelson on 1/28/17. + */ +public class BroadcastedTransaction extends GrapheneObject implements Serializable { + public static final String KEY_TRX = "trx"; + public static final String KEY_TRX_ID = "trx_id"; + + private Transaction trx; + private String trx_id; + + public BroadcastedTransaction(String id){ + super(id); + } + + public void setTransaction(Transaction t){ + this.trx = t; + } + + public Transaction getTransaction() { + return trx; + } + + public void setTransactionId(String id){ + this.trx_id = id; + } + + public String getTransactionId() { + return trx_id; + } +} diff --git a/src/main/java/de/bitsharesmunich/graphenej/models/SubscriptionResponse.java b/src/main/java/de/bitsharesmunich/graphenej/models/SubscriptionResponse.java index 4118963..d8502be 100644 --- a/src/main/java/de/bitsharesmunich/graphenej/models/SubscriptionResponse.java +++ b/src/main/java/de/bitsharesmunich/graphenej/models/SubscriptionResponse.java @@ -9,13 +9,35 @@ import com.google.gson.JsonParseException; import java.io.Serializable; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; +import java.util.*; -import de.bitsharesmunich.graphenej.GrapheneObject; -import de.bitsharesmunich.graphenej.ObjectType; +import de.bitsharesmunich.graphenej.*; +import de.bitsharesmunich.graphenej.interfaces.SubscriptionListener; /** + * Class that represents a generic subscription response. + * The template for every subscription response is the following: + * + * { + * "method": "notice" + * "params": [ + * SUBSCRIPTION_ID, + * [[ + * { "id": "2.1.0", ... }, + * { "id": ... }, + * { "id": ... }, + * { "id": ... } + * ]] + * ], + * } + * + * As of 1/2017, the witness API returns all sort of events, not just the ones we're interested in once we + * make a call to the 'set_subscribe_callback', regardless of whether the 'clear_filter' parameter is set to + * true or false. + * + * To minimize CPU usage, we introduce a scheme of selective parsing, implemented by the static inner class + * SubscriptionResponseDeserializer. + * * Created by nelson on 1/12/17. */ public class SubscriptionResponse { @@ -27,7 +49,49 @@ public class SubscriptionResponse { public String method; public List params; + /** + * Deserializer class that is used to parse and deserialize subscription responses in a partial way, + * depending on the amount of SubscriptionListeners we might have registered. + * + * The rationale behind these architecture is to avoid wasting computational resources parsing unneeded + * objects that might come once the are subscribed to the witness notifications. + */ public static class SubscriptionResponseDeserializer implements JsonDeserializer { + private HashMap listenerTypeCount; + private LinkedList mListeners; + + /** + * Constructor that will just create a list of SubscriptionListeners and + * a map of ObjectType to integer in order to keep track of how many listeners + * to each type of object we have. + */ + public SubscriptionResponseDeserializer(){ + mListeners = new LinkedList<>(); + listenerTypeCount = new HashMap<>(); + } + + public void addSubscriptionListener(SubscriptionListener subscriptionListener){ + int currentCount = 0; + if(listenerTypeCount.containsKey(subscriptionListener.getInterestObjectType())){ + currentCount = listenerTypeCount.get(subscriptionListener.getInterestObjectType()); + } + this.listenerTypeCount.put(subscriptionListener.getInterestObjectType(), currentCount + 1); + this.mListeners.add(subscriptionListener); + } + + public List getSubscriptionListeners(){ + return this.mListeners; + } + + public void removeSubscriptionListener(SubscriptionListener subscriptionListener){ + int currentCount = listenerTypeCount.get(subscriptionListener.getInterestObjectType()); + if(currentCount != 0){ + this.listenerTypeCount.put(subscriptionListener.getInterestObjectType(), currentCount); + }else{ + System.out.println("Trying to remove subscription listener, but none is registered!"); + } + this.mListeners.remove(subscriptionListener); + } @Override public SubscriptionResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { @@ -44,26 +108,48 @@ public class SubscriptionResponse { JsonArray subArray = paramsArray.get(1).getAsJsonArray().get(0).getAsJsonArray(); for(JsonElement object : subArray){ if(object.isJsonObject()){ + GrapheneObject grapheneObject = new GrapheneObject(object.getAsJsonObject().get(KEY_ID).getAsString()); - JsonObject jsonObject = object.getAsJsonObject(); - if(grapheneObject.getObjectType() == ObjectType.ACCOUNT_BALANCE_OBJECT){ - AccountBalanceUpdate balanceObject = new AccountBalanceUpdate(grapheneObject.getObjectId()); - balanceObject.owner = jsonObject.get(AccountBalanceUpdate.KEY_OWNER).getAsString(); - balanceObject.asset_type = jsonObject.get(AccountBalanceUpdate.KEY_ASSET_TYPE).getAsString(); - balanceObject.balance = jsonObject.get(AccountBalanceUpdate.KEY_BALANCE).getAsLong(); - secondArgument.add(balanceObject); - }else if(grapheneObject.getObjectType() == ObjectType.DYNAMIC_GLOBAL_PROPERTY_OBJECT){ - DynamicGlobalProperties dynamicGlobal = new DynamicGlobalProperties(grapheneObject.getObjectId()); - dynamicGlobal.head_block_number = jsonObject.get(DynamicGlobalProperties.KEY_HEAD_BLOCK_NUMBER).getAsLong(); - dynamicGlobal.head_block_id = jsonObject.get(DynamicGlobalProperties.KEY_HEAD_BLOCK_ID).getAsString(); - dynamicGlobal.time = jsonObject.get(DynamicGlobalProperties.KEY_TIME).getAsString(); - //TODO: Deserialize all other attributes - secondArgument.add(dynamicGlobal); + int listenerTypeCount = 0; + if(this.listenerTypeCount.containsKey(grapheneObject.getObjectType())){ + listenerTypeCount = this.listenerTypeCount.get(grapheneObject.getObjectType()); + } + /* + * Here's where we apply the selective deserialization logic, meaning we only completely deserialize + * an object contained in a notification if there is at least one registered listener interested in + * objects of that type. + */ + if(listenerTypeCount > 0){ + JsonObject jsonObject = object.getAsJsonObject(); + if(grapheneObject.getObjectType() == ObjectType.ACCOUNT_BALANCE_OBJECT){ + AccountBalanceUpdate balanceObject = new AccountBalanceUpdate(grapheneObject.getObjectId()); + balanceObject.owner = jsonObject.get(AccountBalanceUpdate.KEY_OWNER).getAsString(); + balanceObject.asset_type = jsonObject.get(AccountBalanceUpdate.KEY_ASSET_TYPE).getAsString(); + balanceObject.balance = jsonObject.get(AccountBalanceUpdate.KEY_BALANCE).getAsLong(); + secondArgument.add(balanceObject); + }else if(grapheneObject.getObjectType() == ObjectType.DYNAMIC_GLOBAL_PROPERTY_OBJECT){ + DynamicGlobalProperties dynamicGlobal = new DynamicGlobalProperties(grapheneObject.getObjectId()); + dynamicGlobal.head_block_number = jsonObject.get(DynamicGlobalProperties.KEY_HEAD_BLOCK_NUMBER).getAsLong(); + dynamicGlobal.head_block_id = jsonObject.get(DynamicGlobalProperties.KEY_HEAD_BLOCK_ID).getAsString(); + dynamicGlobal.time = jsonObject.get(DynamicGlobalProperties.KEY_TIME).getAsString(); + //TODO: Deserialize all other attributes + secondArgument.add(dynamicGlobal); + }else if(grapheneObject.getObjectType() == ObjectType.TRANSACTION_OBJECT){ + BroadcastedTransaction broadcastedTransaction = new BroadcastedTransaction(grapheneObject.getObjectId()); + broadcastedTransaction.setTransaction(context.deserialize(jsonObject.get(BroadcastedTransaction.KEY_TRX), Transaction.class)); + broadcastedTransaction.setTransactionId(jsonObject.get(BroadcastedTransaction.KEY_TRX_ID).getAsString()); + secondArgument.add(broadcastedTransaction); + }else{ + //TODO: Add support for other types of objects + } } }else{ secondArgument.add(object.getAsString()); } } + for(SubscriptionListener listener : mListeners){ + listener.onSubscriptionUpdate(response); + } return response; } }