Adding basic support for the subscription feature of the blockchain API and listening for transfer operations in transactions

master
Nelson R. Perez 2017-01-29 22:00:39 -05:00
parent fc9915ab78
commit aec4e55953
16 changed files with 596 additions and 109 deletions

View File

@ -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);

View File

@ -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
* <a href="https://en.wikipedia.org/wiki/Unix_time">Unix time</a>
*/
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;

View File

@ -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;

View File

@ -73,6 +73,7 @@ public class Main {
// test.testAssetSerialization();
// test.testGetMarketHistory();
// test.testGetAccountBalances();
test.testGetAssetHoldersCount();
// test.testGetAssetHoldersCount();
test.testSubscription(null);
}
}

View File

@ -1,52 +1,55 @@
package de.bitsharesmunich.graphenej;
/**
* Enum type used to keep track of all the operation types and their corresponding ids.
*
* <a href="https://bitshares.org/doxygen/operations_8hpp_source.html">Source</a>
*
* 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
}

View File

@ -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";

View File

@ -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<HoldersCount> holdersCountList = (List<HoldersCount>) response.result;
for(HoldersCount holdersCount : holdersCountList){
List<AssetHolderCount> holdersCountList = (List<AssetHolderCount>) 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<Serializable> updatedObjects = (List<Serializable>) 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());
}
}
}

View File

@ -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<BaseOperation> 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<BaseOperation> 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<Transaction> {
/**
* 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<Transaction> {
@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<Transaction> {
@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<BaseOperation> 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);
}
}
}

View File

@ -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{

View File

@ -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<WitnessResponse<List<HoldersCount>>>(){}.getType();
Type AssetTokenHolders = new TypeToken<WitnessResponse<List<AssetHolderCount>>>(){}.getType();
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(HoldersCount.class, new HoldersCount.HoldersCountDeserializer());
WitnessResponse<List<HoldersCount>> witnessResponse = builder.create().fromJson(response, AssetTokenHolders);
builder.registerTypeAdapter(AssetHolderCount.class, new AssetHolderCount.HoldersCountDeserializer());
WitnessResponse<List<AssetHolderCount>> witnessResponse = builder.create().fromJson(response, AssetTokenHolders);
mListener.onSuccess(witnessResponse);
websocket.disconnect();
}else{

View File

@ -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<String, List<String>> headers) throws Exception {
ArrayList<Serializable> 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<Serializable> 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<WitnessResponse<Integer>>() {}.getType();
WitnessResponse<Integer> witnessResponse = gson.fromJson(message, ApiIdResponse);
databaseApiId = witnessResponse.result;
ArrayList<Serializable> 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);
}
}

View File

@ -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);
}

View File

@ -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<Serializable> listArgument = (ArrayList<Serializable>) 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());
}

View File

@ -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<HoldersCount> {
public static class HoldersCountDeserializer implements JsonDeserializer<AssetHolderCount> {
@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;

View File

@ -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;
}
}

View File

@ -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<Serializable> 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<SubscriptionResponse> {
private HashMap<ObjectType, Integer> listenerTypeCount;
private LinkedList<SubscriptionListener> 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<SubscriptionListener> 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;
}
}