- Added support for the 'get_account_history_by_operations' API call

- Introduced a test case for the de-serialization of the HistoryOperationDetail object instance
- Making the sample app use the newly introduced 'get_account_history_by_operations' API call
develop
Nelson R. Perez 2018-09-05 21:04:46 -05:00
parent f8326093a2
commit 229590457b
13 changed files with 290 additions and 34 deletions

View File

@ -1,12 +1,19 @@
package cy.agorise.graphenej;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import cy.agorise.graphenej.interfaces.ByteSerializable;
import cy.agorise.graphenej.interfaces.JsonSerializable;
import cy.agorise.graphenej.operations.TransferOperation;
/**
* Created by nelson on 11/5/16.
* Base class that represents a generic operation
*/
public abstract class BaseOperation implements ByteSerializable, JsonSerializable {
@ -32,4 +39,54 @@ public abstract class BaseOperation implements ByteSerializable, JsonSerializabl
array.add(this.getId());
return array;
}
/**
* <p>
* De-serializer used to unpack data from a generic operation. The general format used in the
* JSON-RPC blockchain API is the following:
* </p>
*
* <code>[OPERATION_ID, OPERATION_OBJECT]</code><br>
*
* <p>
* Where <code>OPERATION_ID</code> is one of the operations defined in {@link cy.agorise.graphenej.OperationType}
* and <code>OPERATION_OBJECT</code> is the actual operation serialized in the JSON format.
* </p>
* Here's an example of this serialized form for a transfer operation:<br><br>
*<pre>
*[
* 0,
* {
* "fee": {
* "amount": 264174,
* "asset_id": "1.3.0"
* },
* "from": "1.2.138632",
* "to": "1.2.129848",
* "amount": {
* "amount": 100,
* "asset_id": "1.3.0"
* },
* "extensions": []
* }
*]
*</pre><br>
* If this class is used, this serialized data will be translated to a TransferOperation object instance.<br>
*
* TODO: Add support for operations other than the 'transfer'
*/
public static class OperationDeserializer implements JsonDeserializer<BaseOperation> {
@Override
public BaseOperation deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
BaseOperation operation = null;
if(json.isJsonArray()){
JsonArray array = json.getAsJsonArray();
if(array.get(0).getAsLong() == OperationType.TRANSFER_OPERATION.ordinal()){
operation = context.deserialize(array.get(1), TransferOperation.class);
}
}
return operation;
}
}
}

View File

@ -1,12 +1,17 @@
package cy.agorise.graphenej;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import cy.agorise.graphenej.interfaces.ByteSerializable;
import cy.agorise.graphenej.interfaces.JsonSerializable;
import java.util.ArrayList;
/**
* Created by nelson on 11/9/16.
*/
@ -40,4 +45,15 @@ public class Extensions implements JsonSerializable, ByteSerializable {
public int size(){
return extensions.size();
}
/**
* Custom de-serializer used to avoid problems when de-serializing an object that contains
* an extension array.
*/
public static class ExtensionsDeserializer implements JsonDeserializer<Extensions> {
@Override
public Extensions deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return null;
}
}
}

View File

@ -20,6 +20,7 @@ public class RPC {
public static final String CALL_GET_KEY_REFERENCES = "get_key_references";
public static final String CALL_GET_RELATIVE_ACCOUNT_HISTORY = "get_relative_account_history";
public static final String CALL_GET_ACCOUNT_HISTORY = "get_account_history";
public static final String CALL_GET_ACCOUNT_HISTORY_BY_OPERATIONS = "get_account_history_by_operations";
public static final String CALL_LOOKUP_ACCOUNTS = "lookup_accounts";
public static final String CALL_LIST_ASSETS = "list_assets";
public static final String CALL_GET_OBJECTS = "get_objects";

View File

@ -13,10 +13,13 @@ import cy.agorise.graphenej.Asset;
import cy.agorise.graphenej.AssetAmount;
import cy.agorise.graphenej.AssetOptions;
import cy.agorise.graphenej.Authority;
import cy.agorise.graphenej.BaseOperation;
import cy.agorise.graphenej.Extensions;
import cy.agorise.graphenej.LimitOrder;
import cy.agorise.graphenej.Transaction;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.api.calls.GetAccountByName;
import cy.agorise.graphenej.api.calls.GetAccountHistoryByOperations;
import cy.agorise.graphenej.api.calls.GetAccounts;
import cy.agorise.graphenej.api.calls.GetBlock;
import cy.agorise.graphenej.api.calls.GetBlockHeader;
@ -31,6 +34,7 @@ import cy.agorise.graphenej.models.AccountProperties;
import cy.agorise.graphenej.models.Block;
import cy.agorise.graphenej.models.BlockHeader;
import cy.agorise.graphenej.models.BucketObject;
import cy.agorise.graphenej.models.HistoryOperationDetail;
import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.objects.Memo;
import cy.agorise.graphenej.operations.CustomOperation;
@ -139,6 +143,19 @@ public class DeserializationMap {
.registerTypeAdapter(LimitOrder.class, new LimitOrder.LimitOrderDeserializer())
.create();
mGsonMap.put(GetLimitOrders.class, getLimitOrdersGson);
// GetAccountHistoryByOperations
mClassMap.put(GetAccountHistoryByOperations.class, HistoryOperationDetail.class);
Gson getAccountHistoryByOperationsGson = new GsonBuilder()
.setExclusionStrategies(new DeserializationMap.SkipAccountOptionsStrategy(), new DeserializationMap.SkipAssetOptionsStrategy())
.registerTypeAdapter(BaseOperation.class, new BaseOperation.OperationDeserializer())
.registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer())
.registerTypeAdapter(Extensions.class, new Extensions.ExtensionsDeserializer())
.registerTypeAdapter(Memo.class, new Memo.MemoDeserializer())
.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer())
.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
.create();
mGsonMap.put(GetAccountHistoryByOperations.class, getAccountHistoryByOperationsGson);
}
public Class getReceivedClass(Class _class){

View File

@ -42,6 +42,7 @@ import cy.agorise.graphenej.models.Block;
import cy.agorise.graphenej.models.BlockHeader;
import cy.agorise.graphenej.models.BucketObject;
import cy.agorise.graphenej.models.DynamicGlobalProperties;
import cy.agorise.graphenej.models.HistoryOperationDetail;
import cy.agorise.graphenej.models.JsonRpcNotification;
import cy.agorise.graphenej.models.JsonRpcResponse;
import cy.agorise.graphenej.models.OperationHistory;
@ -359,7 +360,11 @@ public class NetworkService extends Service {
} else if(responsePayloadClass == AccountProperties.class){
Type GetAccountByNameResponse = new TypeToken<JsonRpcResponse<AccountProperties>>(){}.getType();
parsedResponse = gson.fromJson(text, GetAccountByNameResponse);
} else if(responsePayloadClass == List.class){
} else if(responsePayloadClass == HistoryOperationDetail.class){
Type GetAccountHistoryByOperationsResponse = new TypeToken<JsonRpcResponse<HistoryOperationDetail>>(){}.getType();
Log.d(TAG,"*> "+text);
parsedResponse = gson.fromJson(text, GetAccountHistoryByOperationsResponse);
}else if(responsePayloadClass == List.class){
// If the response payload is a List, further inquiry is required in order to
// determine a list of what is expected here
if(requestClass == GetAccounts.class){

View File

@ -0,0 +1,69 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import cy.agorise.graphenej.OperationType;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
public class GetAccountHistoryByOperations implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_HISTORY;
private UserAccount mUserAccount;
private List<OperationType> mOperationTypes;
private long mStart;
private long mLimit;
/**
* @param userAccount The user account that should be queried
* @param operationsTypes The IDs of the operation we want to get operations in the account( 0 = transfer , 1 = limit order create, ...)
* @param start The sequence number where to start listing operations
* @param limit The max number of entries to return (from start number)
*/
public GetAccountHistoryByOperations(UserAccount userAccount, List<OperationType> operationsTypes, long start, long limit){
this.mUserAccount = userAccount;
this.mOperationTypes = operationsTypes;
this.mStart = start;
this.mLimit = limit;
}
/**
* @param userAccount The user account that should be queried
* @param operationsTypes The IDs of the operation we want to get operations in the account( 0 = transfer , 1 = limit order create, ...)
* @param start The sequence number where to start listing operations
* @param limit The max number of entries to return (from start number)
*/
public GetAccountHistoryByOperations(String userAccount, List<OperationType> operationsTypes, long start, long limit){
if(userAccount.matches("^1\\.2\\.\\d*$")){
this.mUserAccount = new UserAccount(userAccount);
}else{
this.mUserAccount = new UserAccount("", userAccount);
}
this.mOperationTypes = operationsTypes;
this.mStart = start;
this.mLimit = limit;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
if(mUserAccount.getName() != null){
params.add(mUserAccount.getName());
}else{
params.add(mUserAccount.getObjectId());
}
ArrayList<Integer> operationTypes = new ArrayList<>();
for(OperationType operationType : mOperationTypes){
operationTypes.add(operationType.ordinal());
}
params.add(operationTypes);
params.add(mStart);
params.add(mLimit);
return new ApiCall(apiId, RPC.CALL_GET_ACCOUNT_HISTORY_BY_OPERATIONS, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,28 @@
package cy.agorise.graphenej.models;
import java.util.List;
/**
* Model class used to represent the struct defined in graphene::app::history_operation_detail and
* returned as response to the 'get_account_history_by_operations' API call.
*/
public class HistoryOperationDetail {
private long total_count;
List<OperationHistory> operation_history_objs;
public long getTotalCount() {
return total_count;
}
public void setTotalCount(long total_count) {
this.total_count = total_count;
}
public List<OperationHistory> getOperationHistoryObjs() {
return operation_history_objs;
}
public void setOperationHistoryObjs(List<OperationHistory> operation_history_objs) {
this.operation_history_objs = operation_history_objs;
}
}

View File

@ -9,8 +9,8 @@ import com.google.gson.JsonParseException;
import java.io.Serializable;
import java.lang.reflect.Type;
import cy.agorise.graphenej.BaseOperation;
import cy.agorise.graphenej.GrapheneObject;
import cy.agorise.graphenej.operations.TransferOperation;
/**
@ -27,7 +27,7 @@ public class OperationHistory extends GrapheneObject implements Serializable {
public static final String KEY_OP_IN_TRX = "op_in_trx";
public static final String KEY_VIRTUAL_OP = "virtual_op";
private TransferOperation op;
private BaseOperation op;
public Object[] result;
private long block_num;
private long trx_in_block;
@ -38,11 +38,11 @@ public class OperationHistory extends GrapheneObject implements Serializable {
super(id);
}
public TransferOperation getOperation() {
public BaseOperation getOperation() {
return op;
}
public void setOperation(TransferOperation op) {
public void setOperation(BaseOperation op) {
this.op = op;
}
@ -122,14 +122,13 @@ public class OperationHistory extends GrapheneObject implements Serializable {
long blockNum = jsonObject.get(KEY_BLOCK_NUM).getAsLong();
long trxInBlock = jsonObject.get(KEY_TRX_IN_BLOCK).getAsLong();
long opInTrx = jsonObject.get(KEY_OP_IN_TRX).getAsLong();
TransferOperation transferOperation = context.deserialize(jsonObject.get(KEY_OP), TransferOperation.class);
BaseOperation operation = context.deserialize(jsonObject.get(KEY_OP), BaseOperation.class);
long virtualOp = jsonObject.get(KEY_VIRTUAL_OP).getAsLong();
OperationHistory operationHistory = new OperationHistory(id);
operationHistory.setBlockNum(blockNum);
operationHistory.setTransactionsInBlock(trxInBlock);
operationHistory.setOperationsInTrx(opInTrx);
operationHistory.setOperation(transferOperation);
operationHistory.setOperation(operation);
operationHistory.setVirtualOp(virtualOp);
return operationHistory;
}

View File

@ -55,7 +55,7 @@ public class GetRelativeAccountHistoryTest extends BaseApiTest {
for(OperationHistory historicalTransfer : resp.result){
if(historicalTransfer.getOperation() != null){
System.out.println("Got transfer operation!");
TransferOperation transferOperation = historicalTransfer.getOperation();
TransferOperation transferOperation = (TransferOperation) historicalTransfer.getOperation();
System.out.println(String.format("%s - > %s, memo: %s",
transferOperation.getFrom().getObjectId(),
transferOperation.getTo().getObjectId(),

View File

@ -0,0 +1,40 @@
package cy.agorise.graphenej.models;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import junit.framework.Assert;
import org.junit.Test;
import java.lang.reflect.Type;
import cy.agorise.graphenej.AssetAmount;
import cy.agorise.graphenej.BaseOperation;
import cy.agorise.graphenej.Extensions;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.api.android.DeserializationMap;
import cy.agorise.graphenej.objects.Memo;
public class HistoryOperationDetailsTest {
@Test
public void testDeserialization(){
String text = "{\"id\":5,\"jsonrpc\":\"2.0\",\"result\":{\"total_count\":2,\"operation_history_objs\":[{\"id\":\"1.11.5701809\",\"op\":[0,{\"fee\":{\"amount\":264174,\"asset_id\":\"1.3.0\"},\"from\":\"1.2.99700\",\"to\":\"1.2.138632\",\"amount\":{\"amount\":20000,\"asset_id\":\"1.3.120\"},\"extensions\":[]}],\"result\":[0,{}],\"block_num\":11094607,\"trx_in_block\":0,\"op_in_trx\":0,\"virtual_op\":31767},{\"id\":\"1.11.5701759\",\"op\":[0,{\"fee\":{\"amount\":264174,\"asset_id\":\"1.3.0\"},\"from\":\"1.2.99700\",\"to\":\"1.2.138632\",\"amount\":{\"amount\":10000000,\"asset_id\":\"1.3.0\"},\"extensions\":[]}],\"result\":[0,{}],\"block_num\":11094501,\"trx_in_block\":0,\"op_in_trx\":0,\"virtual_op\":31717}]}}\n";
Gson gson = new GsonBuilder()
.setExclusionStrategies(new DeserializationMap.SkipAccountOptionsStrategy(), new DeserializationMap.SkipAssetOptionsStrategy())
.registerTypeAdapter(BaseOperation.class, new BaseOperation.OperationDeserializer())
.registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer())
.registerTypeAdapter(Memo.class, new Memo.MemoSerializer())
.registerTypeAdapter(Extensions.class, new Extensions.ExtensionsDeserializer())
.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer())
.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
.create();
Type GetAccountHistoryByOperationsResponse = new TypeToken<JsonRpcResponse<HistoryOperationDetail>>(){}.getType();
JsonRpcResponse<HistoryOperationDetail> response = gson.fromJson(text, GetAccountHistoryByOperationsResponse);
Assert.assertNotNull(response.result);
Assert.assertNotNull(response.result.operation_history_objs);
}
}

View File

@ -47,6 +47,7 @@ public class CallsActivity extends AppCompatActivity {
RPC.CALL_LIST_ASSETS,
RPC.CALL_GET_ACCOUNT_BY_NAME,
RPC.CALL_GET_LIMIT_ORDERS,
RPC.CALL_GET_ACCOUNT_HISTORY_BY_OPERATIONS,
RPC.CALL_SET_SUBSCRIBE_CALLBACK
};

View File

@ -22,12 +22,14 @@ import java.util.HashMap;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import cy.agorise.graphenej.OperationType;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.api.ConnectionStatusUpdate;
import cy.agorise.graphenej.api.android.DeserializationMap;
import cy.agorise.graphenej.api.android.RxBus;
import cy.agorise.graphenej.api.calls.GetAccountByName;
import cy.agorise.graphenej.api.calls.GetAccountHistoryByOperations;
import cy.agorise.graphenej.api.calls.GetAccounts;
import cy.agorise.graphenej.api.calls.GetBlock;
import cy.agorise.graphenej.api.calls.GetLimitOrders;
@ -35,7 +37,6 @@ import cy.agorise.graphenej.api.calls.GetObjects;
import cy.agorise.graphenej.api.calls.ListAssets;
import cy.agorise.graphenej.models.JsonRpcResponse;
import cy.agorise.graphenej.objects.Memo;
import cy.agorise.graphenej.operations.TransferOperation;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
@ -83,7 +84,6 @@ public class PerformCallActivity extends ConnectedActivity {
private Gson gson = new GsonBuilder()
.setExclusionStrategies(new DeserializationMap.SkipAccountOptionsStrategy(), new DeserializationMap.SkipAssetOptionsStrategy())
.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer())
.registerTypeAdapter(Memo.class, new Memo.MemoSerializer())
.create();
@ -125,6 +125,9 @@ public class PerformCallActivity extends ConnectedActivity {
case RPC.CALL_GET_ACCOUNT_BY_NAME:
setupAccountByName();
break;
case RPC.CALL_GET_ACCOUNT_HISTORY_BY_OPERATIONS:
setupGetAccountHistoryByOperations();
break;
case RPC.CALL_GET_LIMIT_ORDERS:
setupGetLimitOrders();
default:
@ -210,6 +213,21 @@ public class PerformCallActivity extends ConnectedActivity {
param1.setInputType(InputType.TYPE_CLASS_TEXT);
}
private void setupGetAccountHistoryByOperations(){
requiredInput(4);
Resources resources = getResources();
mParam1View.setHint(resources.getString(R.string.get_account_history_by_operations_arg1));
mParam2View.setHint(resources.getString(R.string.get_account_history_by_operations_arg2));
mParam3View.setHint(resources.getString(R.string.get_account_history_by_operations_arg3));
mParam4View.setHint(resources.getString(R.string.get_account_history_by_operations_arg4));
param2.setText("0"); // Only transfer de-serialization is currently supported by the library!
param2.setEnabled(false);
param2.setInputType(InputType.TYPE_CLASS_NUMBER);
param3.setInputType(InputType.TYPE_CLASS_NUMBER);
param4.setInputType(InputType.TYPE_CLASS_NUMBER);
}
private void setupGetLimitOrders(){
requiredInput(3);
Resources resources = getResources();
@ -274,6 +292,10 @@ public class PerformCallActivity extends ConnectedActivity {
break;
case RPC.CALL_GET_LIMIT_ORDERS:
getLimitOrders();
break;
case RPC.CALL_GET_ACCOUNT_HISTORY_BY_OPERATIONS:
getAccountHistoryByOperations();
break;
default:
Log.d(TAG,"Default called");
}
@ -334,6 +356,20 @@ public class PerformCallActivity extends ConnectedActivity {
}
}
private void getAccountHistoryByOperations(){
try{
String account = param1.getText().toString();
ArrayList<OperationType> operationTypes = new ArrayList<>();
operationTypes.add(OperationType.TRANSFER_OPERATION); // Currently restricted to transfer operations
long start = Long.parseLong(param3.getText().toString());
long limit = Long.parseLong(param4.getText().toString());
long id = mNetworkService.sendMessage(new GetAccountHistoryByOperations(account, operationTypes, start, limit), GetAccountHistoryByOperations.REQUIRED_API);
}catch(NumberFormatException e){
Toast.makeText(this, getString(R.string.error_number_format), Toast.LENGTH_SHORT).show();
Log.e(TAG,"NumberFormatException while trying to read arguments for 'get_account_history_by_operations'. Msg: "+e.getMessage());
}
}
/**
* Internal method that will decide what to do with each JSON-RPC response
*
@ -345,36 +381,17 @@ public class PerformCallActivity extends ConnectedActivity {
String request = responseMap.get(id);
switch(request){
case RPC.CALL_GET_ACCOUNTS:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
case RPC.CALL_GET_BLOCK:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
case RPC.CALL_GET_BLOCK_HEADER:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
case RPC.CALL_GET_MARKET_HISTORY:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
case RPC.CALL_GET_ACCOUNT_HISTORY:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
case RPC.CALL_GET_RELATIVE_ACCOUNT_HISTORY:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
case RPC.CALL_GET_REQUIRED_FEES:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
case RPC.CALL_LOOKUP_ASSET_SYMBOLS:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
case RPC.CALL_LIST_ASSETS:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
case RPC.CALL_GET_ACCOUNT_BY_NAME:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
case RPC.CALL_GET_LIMIT_ORDERS:
case RPC.CALL_GET_ACCOUNT_HISTORY_BY_OPERATIONS:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
default:

View File

@ -41,6 +41,12 @@
<!-- Get account by name fields -->
<string name="get_accounts_by_name_arg1">Account name</string>
<!-- GetAccountHistoryByOperations input fields -->
<string name="get_account_history_by_operations_arg1">Account id or name</string>
<string name="get_account_history_by_operations_arg2">Operaton type (only transfer type is supported)</string>
<string name="get_account_history_by_operations_arg3">Start sequence number</string>
<string name="get_account_history_by_operations_arg4">Limit</string>
<!-- GetLimitOrders input fields -->
<string name="get_limit_orders_arg1">Asset A</string>
<string name="get_limit_orders_arg2">Asset B</string>