Merge branch 'feat_central_broker' into develop

develop
Nelson R. Perez 2018-09-17 13:08:28 -05:00
commit 56fb257eb2
100 changed files with 3975 additions and 159 deletions

1
.gitignore vendored
View File

@ -104,4 +104,3 @@ graphenej/build
local.properties
sample

View File

@ -3,11 +3,25 @@ subprojects {
mavenCentral()
}
}
allprojects {
repositories {
mavenCentral()
jcenter()
maven {
url "https://maven.google.com"
}
}
}
buildscript {
repositories {
mavenCentral()
maven {
url 'https://maven.google.com/'
name 'Google'
}
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.android.tools.build:gradle:3.1.4'
}
}

0
gradlew vendored Normal file → Executable file
View File

View File

@ -2,33 +2,39 @@ group 'cy.agorise'
version '0.4.7-alpha2'
apply plugin: 'com.android.library'
apply from: 'maven-push.gradle'
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
compile 'com.neovisionaries:nv-websocket-client:1.30'
compile 'org.bitcoinj:bitcoinj-core:0.14.3'
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.0'
compile group: "org.tukaani", name: "xz", version: "1.6"
}
//apply from: 'maven-push.gradle'
android {
compileSdkVersion 24
buildToolsVersion "25.0.0"
buildToolsVersion '27.0.3'
defaultConfig {
minSdkVersion 9
minSdkVersion 14
targetSdkVersion 24
versionCode 12
versionName "0.4.7-alpha3"
vectorDrawables.useSupportLibrary = true
}
buildTypes {
debug{}
preRelease{}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
}
dependencies {
testImplementation group: 'junit', name: 'junit', version: '4.12'
implementation 'com.neovisionaries:nv-websocket-client:1.30'
implementation 'org.bitcoinj:bitcoinj-core:0.14.3'
implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.0'
implementation group: "org.tukaani", name: "xz", version: "1.6"
// Rx dependencies
api 'io.reactivex.rxjava2:rxandroid:2.0.2'
api 'io.reactivex.rxjava2:rxjava:2.1.16'
api 'com.jakewharton.rxrelay2:rxrelay:2.0.0'
api 'com.squareup.okhttp3:okhttp:3.5.0'
}

View File

@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cy.agorise.graphenej">
package="cy.agorise.graphenej">
<uses-sdk android:minSdkVersion="1" />
<application/>
<application>
<service
android:name=".api.android.NetworkService"
android:enabled="true"
android:exported="true"></service>
</application>
</manifest>

View File

@ -9,5 +9,10 @@ package cy.agorise.graphenej;
public enum AuthorityType {
OWNER,
ACTIVE,
MEMO
MEMO;
@Override
public String toString() {
return String.format("%d", this.ordinal());
}
}

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,5 +1,7 @@
package cy.agorise.graphenej;
import android.annotation.SuppressLint;
import org.bitcoinj.core.DumpedPrivateKey;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
@ -69,7 +71,15 @@ public class BrainKey {
public BrainKey(String words, int sequence) {
this.mBrainKey = words;
this.sequenceNumber = sequence;
String encoded = String.format("%s %d", words, sequence);
derivePrivateKey();
}
/**
* Generates the actual private key from the brainkey + sequence number
*/
private void derivePrivateKey(){
@SuppressLint("DefaultLocale")
String encoded = String.format("%s %d", this.mBrainKey, this.sequenceNumber);
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte[] bytes = md.digest(encoded.getBytes("UTF-8"));
@ -120,19 +130,28 @@ public class BrainKey {
}
/**
* Brain key words getter
* @return: The word sequence that comprises this brain key
* Brain key words getter.
* @return The word sequence that comprises this brain key
*/
public String getBrainKey(){
return mBrainKey;
}
/**
* Sequence number getter
* @return: The sequence number used alongside with the brain key words in order
* Sequence number getter.
* @return The sequence number used alongside with the brain key words in order
* to derive the private key
*/
public int getSequenceNumber(){
return sequenceNumber;
}
/**
* Sequence number setter.
* @param sequenceNumber The sequence number used to generate a specific key from this brainkey
*/
public void setSequenceNumber(int sequenceNumber) {
this.sequenceNumber = sequenceNumber;
derivePrivateKey();
}
}

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

@ -1,4 +1,4 @@
package cy.agorise.graphenej.objects;
package cy.agorise.graphenej;
import com.google.common.primitives.Bytes;
import com.google.gson.Gson;
@ -19,9 +19,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import cy.agorise.graphenej.Address;
import cy.agorise.graphenej.PublicKey;
import cy.agorise.graphenej.Util;
import cy.agorise.graphenej.errors.ChecksumException;
import cy.agorise.graphenej.errors.MalformedAddressException;
import cy.agorise.graphenej.interfaces.ByteSerializable;
@ -32,7 +29,6 @@ import cy.agorise.graphenej.interfaces.JsonSerializable;
* {@url https://bitshares.org/doxygen/structgraphene_1_1chain_1_1memo__data.html}
*/
public class Memo implements ByteSerializable, JsonSerializable {
public final static String TAG = "Memo";
public static final String KEY_FROM = "from";
public static final String KEY_TO = "to";
public static final String KEY_NONCE = "nonce";
@ -291,13 +287,15 @@ public class Memo implements ByteSerializable, JsonSerializable {
memoObject.addProperty(KEY_FROM, "");
memoObject.addProperty(KEY_TO, "");
memoObject.addProperty(KEY_NONCE, "");
memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message));
if(this.message != null)
memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message));
return null;
}else{
memoObject.addProperty(KEY_FROM, this.from.toString());
memoObject.addProperty(KEY_TO, this.to.toString());
memoObject.addProperty(KEY_NONCE, String.format("%x", this.nonce));
memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message));
if(this.message != null)
memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message));
}
return memoObject;
}
@ -310,8 +308,9 @@ public class Memo implements ByteSerializable, JsonSerializable {
*/
public JsonElement toJson(boolean decimal){
JsonElement jsonElement = toJsonObject();
if(decimal){
if(decimal && jsonElement != null){
JsonObject jsonObject = (JsonObject) jsonElement;
// The nonce is interpreted in base 16, but it is going to be written in base 10
BigInteger nonce = new BigInteger(jsonObject.get(KEY_NONCE).getAsString(), 16);
jsonObject.addProperty(KEY_NONCE, nonce.toString());
}

View File

@ -132,4 +132,8 @@ public class OrderBook {
}
return obtainedBase;
}
public List<LimitOrder> getLimitOrders(){
return limitOrders;
}
}

View File

@ -53,4 +53,9 @@ public class PublicKey implements ByteSerializable, Serializable {
PublicKey other = (PublicKey) obj;
return this.publicKey.equals(other.getKey());
}
@Override
public String toString() {
return getAddress();
}
}

View File

@ -14,14 +14,17 @@ public class RPC {
public static final String CALL_CANCEL_ALL_SUBSCRIPTIONS = "cancel_all_subscriptions";
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_FULL_ACCOUNTS = "get_full_accounts";
public static final String CALL_GET_DYNAMIC_GLOBAL_PROPERTIES = "get_dynamic_global_properties";
public static final String CALL_BROADCAST_TRANSACTION = "broadcast_transaction";
public static final String CALL_GET_REQUIRED_FEES = "get_required_fees";
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 GET_OBJECTS = "get_objects";
public static final String CALL_GET_OBJECTS = "get_objects";
public static final String GET_ACCOUNT_BALANCES = "get_account_balances";
public static final String CALL_LOOKUP_ASSET_SYMBOLS = "lookup_asset_symbols";
public static final String CALL_GET_BLOCK_HEADER = "get_block_header";

View File

@ -47,6 +47,7 @@ public class UserAccount extends GrapheneObject implements ByteSerializable, Jso
public static final String KEY_OWNER_SPECIAL_AUTHORITY = "owner_special_authority";
public static final String KEY_ACTIVE_SPECIAL_AUTHORITY = "active_special_authority";
public static final String KEY_N_CONTROL_FLAGS = "top_n_control_flags";
public static final String LIFETIME_EXPIRATION_DATE = "1969-12-31T23:59:59";
@Expose
private String name;
@ -84,6 +85,7 @@ public class UserAccount extends GrapheneObject implements ByteSerializable, Jso
@Expose
private long referrerRewardsPercentage;
private boolean isLifeTime;
/**
@ -248,6 +250,14 @@ public class UserAccount extends GrapheneObject implements ByteSerializable, Jso
this.statistics = statistics;
}
public boolean isLifeTime() {
return isLifeTime;
}
public void setLifeTime(boolean lifeTime) {
isLifeTime = lifeTime;
}
/**
* Deserializer used to build a UserAccount instance from the full JSON-formatted response obtained
* by the 'get_objects' API call.
@ -274,8 +284,10 @@ public class UserAccount extends GrapheneObject implements ByteSerializable, Jso
// Handling the deserialization and assignation of the membership date, which internally
// is stored as a long POSIX time value
try{
Date date = dateFormat.parse(jsonAccount.get(KEY_MEMBERSHIP_EXPIRATION_DATE).getAsString());
String expirationDate = jsonAccount.get(KEY_MEMBERSHIP_EXPIRATION_DATE).getAsString();
Date date = dateFormat.parse(expirationDate);
userAccount.setMembershipExpirationDate(date.getTime());
userAccount.setLifeTime(expirationDate.equals(LIFETIME_EXPIRATION_DATE));
} catch (ParseException e) {
System.out.println("ParseException. Msg: "+e.getMessage());
}

View File

@ -0,0 +1,12 @@
package cy.agorise.graphenej.api;
/**
* Class used to list all currently supported API accesses
*/
public class ApiAccess {
public static final int API_NONE = 0x00;
public static final int API_DATABASE = 0x01;
public static final int API_HISTORY = 0x02;
public static final int API_NETWORK_BROADCAST = 0x04;
}

View File

@ -0,0 +1,65 @@
package cy.agorise.graphenej.api;
/**
* Class used to send connection status updates.
*
* Connection status updates can be any of the following:
* - {@link ConnectionStatusUpdate#CONNECTED}
* - {@link ConnectionStatusUpdate#AUTHENTICATED}
* - {@link ConnectionStatusUpdate#API_UPDATE}
* - {@link ConnectionStatusUpdate#DISCONNECTED}
*
* This is specified by the field called {@link #updateCode}.
*
* If the updateCode is ConnectionStatusUpdate#API_UPDATE another extra field called
* {@link #api} is used to specify which api we're getting access to.
*/
public class ConnectionStatusUpdate {
// Constant used to announce that a connection has been established
public final static int CONNECTED = 0;
// Constant used to announce a successful authentication
public final static int AUTHENTICATED = 1;
// Constant used to announce an api update
public final static int API_UPDATE = 2;
// Constant used to announce a disconnection event
public final static int DISCONNECTED = 3;
/**
* The update code is the general purpose of the update message. Can be any of the following:
* - {@link ConnectionStatusUpdate#CONNECTED}
* - {@link ConnectionStatusUpdate#AUTHENTICATED}
* - {@link ConnectionStatusUpdate#API_UPDATE}
* - {@link ConnectionStatusUpdate#DISCONNECTED}
*/
private int updateCode;
/**
* This field is used in case the updateCode is {@link ConnectionStatusUpdate#API_UPDATE} and
* it serves to specify which API we're getting access to.
*
* It can be any of the fields defined in {@link ApiAccess}
*/
private int api;
public ConnectionStatusUpdate(int updateCode, int api){
this.updateCode = updateCode;
this.api = api;
}
public int getUpdateCode() {
return updateCode;
}
public void setUpdateCode(int updateCode) {
this.updateCode = updateCode;
}
public int getApi() {
return api;
}
public void setApi(int api) {
this.api = api;
}
}

View File

@ -75,11 +75,9 @@ public class GetObjects extends BaseGrapheneHandler {
public void onConnected(WebSocket websocket, Map<String, List<String>> headers) throws Exception {
ArrayList<Serializable> params = new ArrayList<>();
ArrayList<Serializable> subParams = new ArrayList<>();
for(String id : this.ids){
subParams.add(id);
}
subParams.addAll(this.ids);
params.add(subParams);
ApiCall apiCall = new ApiCall(0, RPC.GET_OBJECTS, params, RPC.VERSION, 0);
ApiCall apiCall = new ApiCall(0, RPC.CALL_GET_OBJECTS, params, RPC.VERSION, 0);
websocket.sendText(apiCall.toJsonString());
}

View File

@ -18,9 +18,9 @@ import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.interfaces.WitnessResponseListener;
import cy.agorise.graphenej.models.ApiCall;
import cy.agorise.graphenej.models.BaseResponse;
import cy.agorise.graphenej.models.HistoricalTransfer;
import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.models.WitnessResponse;
import cy.agorise.graphenej.objects.Memo;
import cy.agorise.graphenej.Memo;
import cy.agorise.graphenej.operations.TransferOperation;
/**
@ -158,12 +158,13 @@ public class GetRelativeAccountHistory extends BaseGrapheneHandler {
sendRelativeAccountHistoryRequest();
}else if(baseResponse.id >= GET_HISTORY_DATA){
Type RelativeAccountHistoryResponse = new TypeToken<WitnessResponse<List<HistoricalTransfer>>>(){}.getType();
Type RelativeAccountHistoryResponse = new TypeToken<WitnessResponse<List<OperationHistory>>>(){}.getType();
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer());
gsonBuilder.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer());
gsonBuilder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer());
gsonBuilder.registerTypeAdapter(Memo.class, new Memo.MemoDeserializer());
WitnessResponse<List<HistoricalTransfer>> transfersResponse = gsonBuilder.create().fromJson(response, RelativeAccountHistoryResponse);
WitnessResponse<List<OperationHistory>> transfersResponse = gsonBuilder.create().fromJson(response, RelativeAccountHistoryResponse);
mListener.onSuccess(transfersResponse);
}
}

View File

@ -23,9 +23,10 @@ import cy.agorise.graphenej.interfaces.SubscriptionHub;
import cy.agorise.graphenej.interfaces.SubscriptionListener;
import cy.agorise.graphenej.models.ApiCall;
import cy.agorise.graphenej.models.DynamicGlobalProperties;
import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.models.SubscriptionResponse;
import cy.agorise.graphenej.models.WitnessResponse;
import cy.agorise.graphenej.objects.Memo;
import cy.agorise.graphenej.Memo;
import cy.agorise.graphenej.operations.CustomOperation;
import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
import cy.agorise.graphenej.operations.TransferOperation;
@ -61,7 +62,7 @@ public class SubscriptionMessagesHub extends BaseGrapheneHandler implements Subs
private int subscriptionCounter = 0;
private HashMap<Long, BaseGrapheneHandler> mHandlerMap = new HashMap<>();
private List<BaseGrapheneHandler> pendingHandlerList = new ArrayList<>();
private boolean printLogs;
private boolean printLogs = true;
// State variables
private boolean isUnsubscribing;
@ -96,6 +97,7 @@ public class SubscriptionMessagesHub extends BaseGrapheneHandler implements Subs
builder.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer());
builder.registerTypeAdapter(DynamicGlobalProperties.class, new DynamicGlobalProperties.DynamicGlobalPropertiesDeserializer());
builder.registerTypeAdapter(Memo.class, new Memo.MemoDeserializer());
builder.registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer());
this.gson = builder.create();
}
@ -186,7 +188,7 @@ public class SubscriptionMessagesHub extends BaseGrapheneHandler implements Subs
}
payload.add(objects);
ApiCall subscribe = new ApiCall(databaseApiId, RPC.GET_OBJECTS, payload, RPC.VERSION, MANUAL_SUBSCRIPTION_ID);
ApiCall subscribe = new ApiCall(databaseApiId, RPC.CALL_GET_OBJECTS, payload, RPC.VERSION, MANUAL_SUBSCRIPTION_ID);
websocket.sendText(subscribe.toJsonString());
subscriptionCounter++;
}else{

View File

@ -0,0 +1,215 @@
package cy.agorise.graphenej.api.android;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.HashMap;
import java.util.List;
import cy.agorise.graphenej.AccountOptions;
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;
import cy.agorise.graphenej.api.calls.GetFullAccounts;
import cy.agorise.graphenej.api.calls.GetLimitOrders;
import cy.agorise.graphenej.api.calls.GetMarketHistory;
import cy.agorise.graphenej.api.calls.GetObjects;
import cy.agorise.graphenej.api.calls.GetRelativeAccountHistory;
import cy.agorise.graphenej.api.calls.GetRequiredFees;
import cy.agorise.graphenej.api.calls.ListAssets;
import cy.agorise.graphenej.api.calls.LookupAssetSymbols;
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.FullAccountDetails;
import cy.agorise.graphenej.models.HistoryOperationDetail;
import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.Memo;
import cy.agorise.graphenej.operations.CustomOperation;
import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
import cy.agorise.graphenej.operations.TransferOperation;
/**
* Class used to store a mapping of request class to two important things:
*
* 1- The class to which the corresponding response should be de-serialized to
* 2- An instance of the Gson class, with all required type adapters
*/
public class DeserializationMap {
private final String TAG = this.getClass().getName();
private HashMap<Class, Class> mClassMap = new HashMap<>();
private HashMap<Class, Gson> mGsonMap = new HashMap<>();
public DeserializationMap(){
Gson genericGson = new Gson();
// GetBlock
mClassMap.put(GetBlock.class, Block.class);
Gson getBlockGson = new GsonBuilder()
.registerTypeAdapter(Transaction.class, new Transaction.TransactionDeserializer())
.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer())
.registerTypeAdapter(LimitOrderCreateOperation.class, new LimitOrderCreateOperation.LimitOrderCreateDeserializer())
.registerTypeAdapter(CustomOperation.class, new CustomOperation.CustomOperationDeserializer())
.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
.create();
mGsonMap.put(GetBlock.class, getBlockGson);
// GetAccounts
mClassMap.put(GetAccounts.class, List.class);
Gson getAccountsGson = new GsonBuilder()
.setExclusionStrategies(new SkipAccountOptionsStrategy())
.registerTypeAdapter(Authority.class, new Authority.AuthorityDeserializer())
.registerTypeAdapter(AccountOptions.class, new AccountOptions.AccountOptionsDeserializer())
.create();
mGsonMap.put(GetAccounts.class, getAccountsGson);
// GetRequiredFees
mClassMap.put(GetRequiredFees.class, List.class);
Gson getRequiredFeesGson = new GsonBuilder()
.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
.create();
mGsonMap.put(GetRequiredFees.class, getRequiredFeesGson);
// GetRelativeAccountHistory
mClassMap.put(GetRelativeAccountHistory.class, List.class);
Gson getRelativeAcountHistoryGson = new GsonBuilder()
.setExclusionStrategies(new SkipAccountOptionsStrategy(), new SkipAssetOptionsStrategy())
.registerTypeAdapter(BaseOperation.class, new BaseOperation.OperationDeserializer())
.registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer())
.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer())
.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
.registerTypeAdapter(Memo.class, new Memo.MemoDeserializer())
.create();
mGsonMap.put(GetRelativeAccountHistory.class, getRelativeAcountHistoryGson);
// GetBlockHeader
mClassMap.put(GetBlockHeader.class, BlockHeader.class);
mGsonMap.put(GetBlockHeader.class, genericGson);
// GetMarketHistory
mClassMap.put(GetMarketHistory.class, List.class);
Gson getMarketHistoryGson = new GsonBuilder()
.registerTypeAdapter(BucketObject.class, new BucketObject.BucketDeserializer())
.create();
mGsonMap.put(GetMarketHistory.class, getMarketHistoryGson);
// LookupAssetSymbols
mClassMap.put(LookupAssetSymbols.class, List.class);
Gson lookupAssetSymbolGson = new GsonBuilder()
.registerTypeAdapter(Asset.class, new Asset.AssetDeserializer())
.create();
mGsonMap.put(LookupAssetSymbols.class, lookupAssetSymbolGson);
// GetObjects
mClassMap.put(GetObjects.class, List.class);
Gson getObjectsGson = new GsonBuilder()
.registerTypeAdapter(Asset.class, new Asset.AssetDeserializer())
.create();
mGsonMap.put(GetObjects.class, getObjectsGson);
// ListAssets
mClassMap.put(ListAssets.class, List.class);
Gson listAssetsGson = new GsonBuilder()
.registerTypeAdapter(Asset.class, new Asset.AssetDeserializer())
.create();
mGsonMap.put(ListAssets.class, listAssetsGson);
// GetAccountByName
mClassMap.put(GetAccountByName.class, AccountProperties.class);
Gson getAccountByNameGson = new GsonBuilder()
.registerTypeAdapter(Authority.class, new Authority.AuthorityDeserializer())
.registerTypeAdapter(AccountOptions.class, new AccountOptions.AccountOptionsDeserializer())
.create();
mGsonMap.put(GetAccountByName.class, getAccountByNameGson);
// GetLimitOrders
mClassMap.put(GetLimitOrders.class, List.class);
Gson getLimitOrdersGson = new GsonBuilder()
.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer())
.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);
// GetFullAccounts
mClassMap.put(GetFullAccounts.class, List.class);
Gson getFullAccountsGson = new GsonBuilder()
.registerTypeAdapter(FullAccountDetails.class, new FullAccountDetails.FullAccountDeserializer())
.registerTypeAdapter(Authority.class, new Authority.AuthorityDeserializer())
.registerTypeAdapter(Memo.class, new Memo.MemoDeserializer())
.registerTypeAdapter(AccountOptions.class, new AccountOptions.AccountOptionsDeserializer())
.create();
mGsonMap.put(GetFullAccounts.class, getFullAccountsGson);
}
public Class getReceivedClass(Class _class){
return mClassMap.get(_class);
}
public Gson getGson(Class aClass) {
return mGsonMap.get(aClass);
}
/**
* This class is required in order to break a recursion loop when de-serializing the
* AccountProperties class instance.
*/
public static class SkipAccountOptionsStrategy implements ExclusionStrategy {
@Override
public boolean shouldSkipField(FieldAttributes f) {
return false;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return clazz == AccountOptions.class;
}
}
/**
* This class is required in order to break a recursion loop when de-serializing the
* AssetAmount instance.
*/
public static class SkipAssetOptionsStrategy implements ExclusionStrategy {
@Override
public boolean shouldSkipField(FieldAttributes f) {
return false;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return clazz == AssetOptions.class;
}
}
}

View File

@ -0,0 +1,526 @@
package cy.agorise.graphenej.api.android;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.io.Serializable;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import cy.agorise.graphenej.Asset;
import cy.agorise.graphenej.AssetAmount;
import cy.agorise.graphenej.BaseOperation;
import cy.agorise.graphenej.LimitOrder;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.Transaction;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.api.ConnectionStatusUpdate;
import cy.agorise.graphenej.api.bitshares.Nodes;
import cy.agorise.graphenej.api.calls.ApiCallable;
import cy.agorise.graphenej.api.calls.GetAccounts;
import cy.agorise.graphenej.api.calls.GetFullAccounts;
import cy.agorise.graphenej.api.calls.GetLimitOrders;
import cy.agorise.graphenej.api.calls.GetMarketHistory;
import cy.agorise.graphenej.api.calls.GetObjects;
import cy.agorise.graphenej.api.calls.GetRelativeAccountHistory;
import cy.agorise.graphenej.api.calls.GetRequiredFees;
import cy.agorise.graphenej.api.calls.ListAssets;
import cy.agorise.graphenej.models.AccountProperties;
import cy.agorise.graphenej.models.ApiCall;
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.FullAccountDetails;
import cy.agorise.graphenej.models.HistoryOperationDetail;
import cy.agorise.graphenej.models.JsonRpcNotification;
import cy.agorise.graphenej.models.JsonRpcResponse;
import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.Memo;
import cy.agorise.graphenej.operations.CustomOperation;
import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
import cy.agorise.graphenej.operations.TransferOperation;
import io.reactivex.annotations.Nullable;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
/**
* Service in charge of maintaining a connection to the full node.
*/
public class NetworkService extends Service {
private final String TAG = this.getClass().getName();
private static final int NORMAL_CLOSURE_STATUS = 1000;
public static final String KEY_USERNAME = "key_username";
public static final String KEY_PASSWORD = "key_password";
public static final String KEY_REQUESTED_APIS = "key_requested_apis";
/**
* Constant used to pass a custom list of node URLs. This should be a simple
* comma separated list of URLs.
*
* For example:
*
* wss://domain1.com/ws,wss://domain2.com/ws,wss://domain3.com/ws
*/
public static final String KEY_CUSTOM_NODE_URLS = "key_custom_node_urls";
private final IBinder mBinder = new LocalBinder();
private WebSocket mWebSocket;
private int mSocketIndex;
// Username and password used to connect to a specific node
private String mUsername;
private String mPassword;
private boolean isLoggedIn = false;
private String mLastCall;
private long mCurrentId = 0;
// Requested APIs passed to this service
private int mRequestedApis;
// Variable used to keep track of the currently obtained API accesses
private HashMap<Integer, Integer> mApiIds = new HashMap<Integer, Integer>();
private ArrayList<String> mNodeUrls = new ArrayList<>();
private Gson gson = new GsonBuilder()
.registerTypeAdapter(Transaction.class, new Transaction.TransactionDeserializer())
.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer())
.registerTypeAdapter(LimitOrderCreateOperation.class, new LimitOrderCreateOperation.LimitOrderCreateDeserializer())
.registerTypeAdapter(CustomOperation.class, new CustomOperation.CustomOperationDeserializer())
.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer())
.registerTypeAdapter(DynamicGlobalProperties.class, new DynamicGlobalProperties.DynamicGlobalPropertiesDeserializer())
.registerTypeAdapter(Memo.class, new Memo.MemoDeserializer())
.registerTypeAdapter(BaseOperation.class, new BaseOperation.OperationDeserializer())
.registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer())
.registerTypeAdapter(JsonRpcNotification.class, new JsonRpcNotification.JsonRpcNotificationDeserializer())
.create();
// Map used to keep track of outgoing request ids and its request types. This is just
// one of two required mappings. The second one is implemented by the DeserializationMap
// class.
private HashMap<Long, Class> mRequestClassMap = new HashMap<>();
// This class is used to keep track of the mapping between request classes and response
// payload classes. It also provides a handy method that returns a Gson deserializer instance
// suited for every response type.
private DeserializationMap mDeserializationMap = new DeserializationMap();
@Override
public void onCreate() {
super.onCreate();
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
// Retrieving credentials and requested API data from the shared preferences
mUsername = pref.getString(NetworkService.KEY_USERNAME, "");
mPassword = pref.getString(NetworkService.KEY_PASSWORD, "");
mRequestedApis = pref.getInt(NetworkService.KEY_REQUESTED_APIS, -1);
// If the user of the library desires, a custom list of node URLs can
// be passed using the KEY_CUSTOM_NODE_URLS constant
String serializedNodeUrls = pref.getString(NetworkService.KEY_CUSTOM_NODE_URLS, "");
// Deciding whether to use an externally provided list of node URLs, or use our internal one
if(serializedNodeUrls.equals("")){
mNodeUrls.addAll(Arrays.asList(Nodes.NODE_URLS));
}else{
String[] urls = serializedNodeUrls.split(",");
mNodeUrls.addAll(Arrays.asList(urls));
}
connect();
}
private void connect(){
OkHttpClient client = new OkHttpClient();
String url = mNodeUrls.get(mSocketIndex % mNodeUrls.size());
Log.d(TAG,"Trying to connect with: "+url);
Request request = new Request.Builder().url(url).build();
client.newWebSocket(request, mWebSocketListener);
}
public long sendMessage(String message){
if(mWebSocket != null){
if(mWebSocket.send(message)){
Log.v(TAG,"-> " + message);
return mCurrentId;
}
}else{
throw new RuntimeException("Websocket connection has not yet been established");
}
return -1;
}
/**
* Method that will send a message to the full node, and takes as an argument one of the
* API call wrapper classes. This is the preferred method of sending blockchain API calls.
*
* @param apiCallable The object that will get serialized into a request
* @param requiredApi The required APIs for this specific request. Should be one of the
* constants specified in the ApiAccess class.
* @return The id of the message that was just sent, or -1 if no message was sent.
*/
public long sendMessage(ApiCallable apiCallable, int requiredApi){
if(requiredApi != -1 && mApiIds.containsKey(requiredApi) || requiredApi == ApiAccess.API_NONE){
int apiId = 0;
if(requiredApi != ApiAccess.API_NONE)
apiId = mApiIds.get(requiredApi);
ApiCall call = apiCallable.toApiCall(apiId, ++mCurrentId);
mRequestClassMap.put(mCurrentId, apiCallable.getClass());
if(mWebSocket != null && mWebSocket.send(call.toJsonString())){
Log.v(TAG,"-> "+call.toJsonString());
return mCurrentId;
}
}
return -1;
}
/**
* Method used to inform any external party a clue about the current connectivity status
* @return True if the service is currently connected and logged in, false otherwise.
*/
public boolean isConnected(){
return mWebSocket != null && isLoggedIn;
}
@Override
public void onDestroy() {
if(mWebSocket != null)
mWebSocket.close(NORMAL_CLOSURE_STATUS, null);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
/**
* Class used for the client Binder. Because we know this service always
* runs in the same process as its clients, we don't need to deal with IPC.
*/
public class LocalBinder extends Binder {
public NetworkService getService() {
// Return this instance of LocalService so clients can call public methods
return NetworkService.this;
}
}
private WebSocketListener mWebSocketListener = new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
mWebSocket = webSocket;
// Notifying all listeners about the new connection status
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.CONNECTED, ApiAccess.API_NONE));
// If we're not yet logged in, we should do it now
if(!isLoggedIn){
ArrayList<Serializable> loginParams = new ArrayList<>();
loginParams.add(mUsername);
loginParams.add(mPassword);
ApiCall loginCall = new ApiCall(1, RPC.CALL_LOGIN, loginParams, RPC.VERSION, ++mCurrentId);
mLastCall = RPC.CALL_LOGIN;
sendMessage(loginCall.toJsonString());
}
}
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
Log.v(TAG,"<- "+text);
JsonRpcNotification notification = gson.fromJson(text, JsonRpcNotification.class);
if(notification.method != null){
// If we are dealing with a notification
handleJsonRpcNotification(notification);
}else{
// If we are dealing with a response
JsonRpcResponse<?> response = gson.fromJson(text, JsonRpcResponse.class);
if(response.result != null){
// Handling initial handshake with the full node (authentication and API access checks)
if(response.result instanceof Double || response.result instanceof Boolean){
switch (mLastCall) {
case RPC.CALL_LOGIN:
isLoggedIn = true;
// Broadcasting result
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.AUTHENTICATED, ApiAccess.API_NONE));
checkNextRequestedApiAccess();
break;
case RPC.CALL_DATABASE: {
// Deserializing integer response
Type IntegerJsonResponse = new TypeToken<JsonRpcResponse<Integer>>() {}.getType();
JsonRpcResponse<Integer> apiIdResponse = gson.fromJson(text, IntegerJsonResponse);
// Storing the "database" api id
mApiIds.put(ApiAccess.API_DATABASE, apiIdResponse.result);
// Broadcasting result
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.API_UPDATE, ApiAccess.API_DATABASE));
checkNextRequestedApiAccess();
break;
}
case RPC.CALL_HISTORY: {
// Deserializing integer response
Type IntegerJsonResponse = new TypeToken<JsonRpcResponse<Integer>>() {}.getType();
JsonRpcResponse<Integer> apiIdResponse = gson.fromJson(text, IntegerJsonResponse);
// Broadcasting result
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.API_UPDATE, ApiAccess.API_HISTORY));
// Storing the "history" api id
mApiIds.put(ApiAccess.API_HISTORY, apiIdResponse.result);
checkNextRequestedApiAccess();
break;
}
case RPC.CALL_NETWORK_BROADCAST:
// Deserializing integer response
Type IntegerJsonResponse = new TypeToken<JsonRpcResponse<Integer>>() {}.getType();
JsonRpcResponse<Integer> apiIdResponse = gson.fromJson(text, IntegerJsonResponse);
// Broadcasting result
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.API_UPDATE, ApiAccess.API_NETWORK_BROADCAST));
// Storing the "network_broadcast" api access
mApiIds.put(ApiAccess.API_NETWORK_BROADCAST, apiIdResponse.result);
// All calls have been handled at this point
mLastCall = "";
break;
}
}
}
if(response.error != null && response.error.message != null){
// We could not make sense of this incoming message, just log a warning
Log.w(TAG,"Error.Msg: "+response.error.message);
}
// Properly de-serialize all other fields and broadcasts to the event bus
handleJsonRpcResponse(response, text);
}
}
/**
* Private method that will de-serialize all fields of every kind of JSON-RPC response
* and broadcast it to the event bus.
*
* @param response De-serialized response
* @param text Raw text, as received
*/
private void handleJsonRpcResponse(JsonRpcResponse response, String text){
JsonRpcResponse parsedResponse = null;
Class requestClass = mRequestClassMap.get(response.id);
if(requestClass != null){
// Removing the class entry in the map
mRequestClassMap.remove(response.id);
// Obtaining the response payload class
Class responsePayloadClass = mDeserializationMap.getReceivedClass(requestClass);
Gson gson = mDeserializationMap.getGson(requestClass);
if(responsePayloadClass == Block.class){
// If the response payload is a Block instance, we proceed to de-serialize it
Type GetBlockResponse = new TypeToken<JsonRpcResponse<Block>>() {}.getType();
parsedResponse = gson.fromJson(text, GetBlockResponse);
}else if(responsePayloadClass == BlockHeader.class){
// If the response payload is a BlockHeader instance, we proceed to de-serialize it
Type GetBlockHeaderResponse = new TypeToken<JsonRpcResponse<BlockHeader>>(){}.getType();
parsedResponse = gson.fromJson(text, GetBlockHeaderResponse);
} else if(responsePayloadClass == AccountProperties.class){
Type GetAccountByNameResponse = new TypeToken<JsonRpcResponse<AccountProperties>>(){}.getType();
parsedResponse = gson.fromJson(text, GetAccountByNameResponse);
} else if(responsePayloadClass == HistoryOperationDetail.class){
Type GetAccountHistoryByOperationsResponse = new TypeToken<JsonRpcResponse<HistoryOperationDetail>>(){}.getType();
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){
// If the request call was the wrapper to the get_accounts API call, we know
// the response should be in the form of a JsonRpcResponse<List<AccountProperties>>
// so we proceed with that
Type GetAccountsResponse = new TypeToken<JsonRpcResponse<List<AccountProperties>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetAccountsResponse);
}else if(requestClass == GetRequiredFees.class){
Type GetRequiredFeesResponse = new TypeToken<JsonRpcResponse<List<AssetAmount>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetRequiredFeesResponse);
}else if(requestClass == GetRelativeAccountHistory.class){
Type RelativeAccountHistoryResponse = new TypeToken<JsonRpcResponse<List<OperationHistory>>>(){}.getType();
parsedResponse = gson.fromJson(text, RelativeAccountHistoryResponse);
}else if(requestClass == GetMarketHistory.class){
Type GetMarketHistoryResponse = new TypeToken<JsonRpcResponse<List<BucketObject>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetMarketHistoryResponse);
}else if(requestClass == GetObjects.class){
parsedResponse = handleGetObject(text);
}else if(requestClass == ListAssets.class){
Type LisAssetsResponse = new TypeToken<JsonRpcResponse<List<Asset>>>(){}.getType();
parsedResponse = gson.fromJson(text, LisAssetsResponse);
}else if(requestClass == GetLimitOrders.class){
Type GetLimitOrdersResponse = new TypeToken<JsonRpcResponse<List<LimitOrder>>>() {}.getType();
parsedResponse = gson.fromJson(text, GetLimitOrdersResponse);
} else if (requestClass == GetFullAccounts.class) {
Type GetFullAccountsResponse = new TypeToken<JsonRpcResponse<List<FullAccountDetails>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetFullAccountsResponse);
} else {
Log.w(TAG,"Unknown request class");
}
}else{
Log.w(TAG,"Unhandled situation");
}
}
// In case the parsedResponse instance is null, we fall back to the raw response
if(parsedResponse == null){
parsedResponse = response;
}
// Broadcasting the parsed response to all interested listeners
RxBus.getBusInstance().send(parsedResponse);
}
/**
* Private method that will just broadcast a de-serialized notification to all interested parties
* @param notification De-serialized notification
*/
private void handleJsonRpcNotification(JsonRpcNotification notification){
// Broadcasting the parsed notification to all interested listeners
RxBus.getBusInstance().send(notification);
}
/**
* Method used to try to deserialize a 'get_objects' API call. Since this request can be used
* for several types of objects, the de-serialization procedure can be a bit more complex.
*
* @param response Response to a 'get_objects' API call
*/
private JsonRpcResponse handleGetObject(String response){
//TODO: Implement a proper de-serialization logic
return null;
}
/**
* Method used to check all possible API accesses.
*
* The service will try to obtain sequentially API access ids for the following APIs:
*
* - Database
* - History
* - Network broadcast
*/
private void checkNextRequestedApiAccess(){
if( (mRequestedApis & ApiAccess.API_DATABASE) == ApiAccess.API_DATABASE &&
mApiIds.get(ApiAccess.API_DATABASE) == null){
// If we need the "database" api access and we don't yet have it
ApiCall apiCall = new ApiCall(1, RPC.CALL_DATABASE, null, RPC.VERSION, ++mCurrentId);
mLastCall = RPC.CALL_DATABASE;
sendMessage(apiCall.toJsonString());
} else if( (mRequestedApis & ApiAccess.API_HISTORY) == ApiAccess.API_HISTORY &&
mApiIds.get(ApiAccess.API_HISTORY) == null){
// If we need the "history" api access and we don't yet have it
ApiCall apiCall = new ApiCall(1, RPC.CALL_HISTORY, null, RPC.VERSION, ++mCurrentId);
mLastCall = RPC.CALL_HISTORY;
sendMessage(apiCall.toJsonString());
}else if( (mRequestedApis & ApiAccess.API_NETWORK_BROADCAST) == ApiAccess.API_NETWORK_BROADCAST &&
mApiIds.get(ApiAccess.API_NETWORK_BROADCAST) == null){
// If we need the "network_broadcast" api access and we don't yet have it
ApiCall apiCall = new ApiCall(1, RPC.CALL_NETWORK_BROADCAST, null, RPC.VERSION, ++mCurrentId);
mLastCall = RPC.CALL_NETWORK_BROADCAST;
sendMessage(apiCall.toJsonString());
}
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
Log.d(TAG,"onClosed");
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE));
isLoggedIn = false;
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
Log.e(TAG,"onFailure. Exception: "+t.getClass().getName()+", Msg: "+t.getMessage());
// Logging error stack trace
for(StackTraceElement element : t.getStackTrace()){
Log.e(TAG,String.format("%s#%s:%s", element.getClassName(), element.getMethodName(), element.getLineNumber()));
}
// Registering current status
isLoggedIn = false;
mCurrentId = 0;
mApiIds.clear();
// If there is a response, we print it
if(response != null){
Log.e(TAG,"Response: "+response.message());
}
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE));
mSocketIndex++;
if(mSocketIndex > mNodeUrls.size() * 3){
Log.e(TAG,"Giving up on connections");
stopSelf();
}else{
connect();
}
}
};
/**
* Method used to check whether or not the network service is connected to a node that
* offers a specific API.
*
* @param whichApi The API we want to use.
* @return True if the node has got that API enabled, false otherwise
*/
public boolean hasApiId(int whichApi){
return mApiIds.get(whichApi) != null;
}
public ArrayList<String> getNodeUrls() {
return mNodeUrls;
}
public void setNodeUrls(ArrayList<String> mNodeUrls) {
this.mNodeUrls = mNodeUrls;
}
}

View File

@ -0,0 +1,117 @@
package cy.agorise.graphenej.api.android;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import java.lang.ref.WeakReference;
/**
* This class should be instantiated at the application level of the android app.
*
* It will monitor the interaction between the different activities of an app and help us decide
* when the connection to the full node should be interrupted.
*/
public class NetworkServiceManager implements Application.ActivityLifecycleCallbacks {
private final String TAG = this.getClass().getName();
/**
* Constant used to specify how long will the app wait for another activity to go through its starting life
* cycle events before running the teardownConnectionTask task.
*
* This is used as a means to detect whether or not the user has left the app.
*/
private final int DISCONNECT_DELAY = 1500;
/**
* Handler instance used to schedule tasks back to the main thread
*/
private Handler mHandler = new Handler();
/**
* Weak reference to the application context
*/
private WeakReference<Context> mContextReference;
// In case we want to interact directly with the service
private NetworkService mService;
/**
* Runnable used to schedule a service disconnection once the app is not visible to the user for
* more than DISCONNECT_DELAY milliseconds.
*/
private final Runnable mDisconnectRunnable = new Runnable() {
@Override
public void run() {
Context context = mContextReference.get();
if(mService != null){
context.unbindService(mServiceConnection);
mService = null;
}
context.stopService(new Intent(context, NetworkService.class));
}
};
public NetworkServiceManager(Context context){
mContextReference = new WeakReference<Context>(context);
}
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
if(mService == null){
// Starting a NetworkService instance
Context context = mContextReference.get();
Intent intent = new Intent(context, NetworkService.class);
context.startService(intent);
}
}
@Override
public void onActivityStarted(Activity activity) {
mHandler.removeCallbacks(mDisconnectRunnable);
if(mService == null){
Context context = mContextReference.get();
Intent intent = new Intent(context, NetworkService.class);
context.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
}
@Override
public void onActivityResumed(Activity activity) {}
@Override
public void onActivityPaused(Activity activity) {
mHandler.postDelayed(mDisconnectRunnable, DISCONNECT_DELAY);
}
@Override
public void onActivityStopped(Activity activity) {}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {}
@Override
public void onActivityDestroyed(Activity activity) {}
/** Defines callbacks for backend binding, passed to bindService() */
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className,
IBinder service) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
NetworkService.LocalBinder binder = (NetworkService.LocalBinder) service;
mService = binder.getService();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {}
};
}

View File

@ -0,0 +1,36 @@
package cy.agorise.graphenej.api.android;
import com.jakewharton.rxrelay2.PublishRelay;
import com.jakewharton.rxrelay2.Relay;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
/**
* Explained here: https://blog.kaush.co/2014/12/24/implementing-an-event-bus-with-rxjava-rxbus/
*/
public class RxBus {
private static RxBus rxBus;
public static final RxBus getBusInstance(){
if(rxBus == null){
rxBus = new RxBus();
}
return rxBus;
}
private final Relay<Object> _bus = PublishRelay.create().toSerialized();
public void send(Object o) {
_bus.accept(o);
}
public Flowable<Object> asFlowable() {
return _bus.toFlowable(BackpressureStrategy.LATEST);
}
public boolean hasObservers() {
return _bus.hasObservers();
}
}

View File

@ -0,0 +1,13 @@
package cy.agorise.graphenej.api.bitshares;
/**
* Known public nodes
*/
public class Nodes {
public static final String[] NODE_URLS = {
"wss://dexnode.net/ws", // Dallas, USA
"wss://bitshares.crypto.fans/ws", // Munich, Germany
"wss://bitshares.openledger.info/ws", // Openledger node
};
}

View File

@ -0,0 +1,17 @@
package cy.agorise.graphenej.api.calls;
import cy.agorise.graphenej.models.ApiCall;
/**
* Interface to be implemented by all classes that will produce an ApiCall object instance
* as a result.
*/
public interface ApiCallable {
/**
*
* @return An instance of the {@link ApiCall} class
*/
ApiCall toApiCall(int apiId, long sequenceId);
}

View File

@ -0,0 +1,17 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
public class CancelAllSubscriptions implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_DATABASE;
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
return new ApiCall(apiId, RPC.CALL_CANCEL_ALL_SUBSCRIPTIONS, new ArrayList<Serializable>(), RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,25 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
public class GetAccountByName implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_NONE;
private String accountName;
public GetAccountByName(String name){
this.accountName = name;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> accountParams = new ArrayList<>();
accountParams.add(this.accountName);
return new ApiCall(apiId, RPC.CALL_GET_ACCOUNT_BY_NAME, accountParams, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,43 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
public class GetAccountHistory implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_HISTORY;
private UserAccount mUserAccount;
private String startOperation;
private String endOperation;
private int limit;
public GetAccountHistory(UserAccount userAccount, String start, String end, int limit){
this.mUserAccount = userAccount;
this.startOperation = start;
this.endOperation = end;
this.limit = limit;
}
public GetAccountHistory(String userId, String start, String end, int limit){
this.mUserAccount = new UserAccount(userId);
this.startOperation = start;
this.endOperation = end;
this.limit = limit;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
params.add(mUserAccount.getObjectId());
params.add(endOperation);
params.add(limit);
params.add(startOperation);
return new ApiCall(apiId, RPC.CALL_GET_ACCOUNT_HISTORY, params, RPC.VERSION, sequenceId);
}
}

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,39 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
/**
* Wrapper around the "get_accounts" API call.
*/
public class GetAccounts implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_NONE;
private List<UserAccount> mUserAccounts;
public GetAccounts(List<UserAccount> accountList){
mUserAccounts = accountList;
}
public GetAccounts(UserAccount userAccount){
mUserAccounts = new ArrayList<>();
mUserAccounts.add(userAccount);
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
ArrayList<Serializable> accountIds = new ArrayList<>();
for(UserAccount userAccount : mUserAccounts){
accountIds.add(userAccount.getObjectId());
}
params.add(accountIds);
return new ApiCall(apiId, RPC.CALL_GET_ACCOUNTS, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,30 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
/**
* Wrapper around the "get_block" API call.
*/
public class GetBlock implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_DATABASE;
private long blockNumber;
public GetBlock(long blockNum){
this.blockNumber = blockNum;
}
public ApiCall toApiCall(int apiId, long sequenceId){
ArrayList<Serializable> params = new ArrayList<>();
String blockNum = String.format("%d", this.blockNumber);
params.add(blockNum);
return new ApiCall(apiId, RPC.CALL_GET_BLOCK, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,30 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
/**
* Wrapper around the "get_block_header" API call. To be used in the single-connection mode.
*/
public class GetBlockHeader implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_DATABASE;
private long blockNumber;
public GetBlockHeader(long number){
this.blockNumber = number;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
String blockNum = String.format("%d", this.blockNumber);
params.add(blockNum);
return new ApiCall(apiId, RPC.CALL_GET_BLOCK_HEADER, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,34 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
/**
* Wrapper around the 'get_full_accounts' API call.
*/
public class GetFullAccounts implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_NONE;
private List<String> mUserAccounts;
private boolean mSubscribe;
public GetFullAccounts(List<String> accounts, boolean subscribe){
this.mUserAccounts = accounts;
this.mSubscribe = subscribe;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
ArrayList<Serializable> accounts = new ArrayList<Serializable>(mUserAccounts);
params.add(accounts);
params.add(mSubscribe);
return new ApiCall(apiId, RPC.CALL_GET_FULL_ACCOUNTS, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,41 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
/** Class that implements get_limit_orders request handler.
*
* Get limit orders in a given market.
*
* The request returns the limit orders, ordered from least price to greatest
*
* @see <a href="https://goo.gl/5sRTRq">get_limit_orders API doc</a>
*
*/
public class GetLimitOrders implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_DATABASE;
private String a;
private String b;
private int limit;
public GetLimitOrders(String a, String b, int limit){
this.a = a;
this.b = b;
this.limit = limit;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> parameters = new ArrayList<>();
parameters.add(a);
parameters.add(b);
parameters.add(limit);
return new ApiCall(apiId, RPC.CALL_GET_LIMIT_ORDERS, parameters, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,86 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import cy.agorise.graphenej.Asset;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
public class GetMarketHistory implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_HISTORY;
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
// API call parameters
private Asset base;
private Asset quote;
private long bucket;
private Date start;
private Date end;
/**
* Constructor that receives the start and end time as UNIX timestamp in milliseconds.
*
* @param base Desired asset history
* @param quote Asset to which the base price will be compared to
* @param bucket The time interval (in seconds) for each point should be (analog to
* candles on a candle stick graph).
* @param start Timestamp (POSIX) of of the most recent operation to retrieve
* (Note: The name can be counter intuitive, but it follow the original
* API parameter name)
* @param end Timestamp (POSIX) of the the earliest operation to retrieve
*/
public GetMarketHistory(Asset base, Asset quote, long bucket, long start, long end){
this(base, quote, bucket, fromTimestamp(start), fromTimestamp(end));
}
/**
* Constructor that receives the start and end time as Date instance objects.
*
* @param base Desired asset history
* @param quote Asset to which the base price will be compared to
* @param bucket The time interval (in seconds) for each point should be (analog to
* candles on a candle stick graph).
* @param start Date and time of of the most recent operation to retrieve
* (Note: The name can be counter intuitive, but it follow the original
* API parameter name)
* @param end Date and time of the the earliest operation to retrieve
*/
public GetMarketHistory(Asset base, Asset quote, long bucket, Date start, Date end){
this.base = base;
this.quote = quote;
this.bucket = bucket;
this.start = start;
this.end = end;
}
/**
* Internal method used to convert a timestamp to a Date.
*
* @param timestamp POSIX timestamp expressed in milliseconds since 1/1/1970
* @return Date instance
*/
private static Date fromTimestamp(long timestamp){
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
return calendar.getTime();
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
params.add(this.base.getObjectId());
params.add(this.quote.getObjectId());
params.add(this.bucket);
params.add(DATE_FORMAT.format(this.start));
params.add(DATE_FORMAT.format(this.end));
return new ApiCall(apiId, RPC.CALL_GET_MARKET_HISTORY, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,29 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
/**
* Wrapper around the "get_objects" API call.
*/
public class GetObjects implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_DATABASE;
private List<String> ids;
public GetObjects(List<String> ids){
this.ids = ids;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
ArrayList<String> subParams = new ArrayList<>(ids);
params.add(subParams);
return new ApiCall(apiId, RPC.CALL_GET_OBJECTS, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,47 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
/**
* Wrapper around the "get_relative_account_history" API call
*/
public class GetRelativeAccountHistory implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_HISTORY;
// API call parameters
private UserAccount mUserAccount;
private int stop;
private int limit;
private int start;
/**
* Constructor
* @param userAccount
* @param stop
* @param limit
* @param start
*/
public GetRelativeAccountHistory(UserAccount userAccount, int stop, int limit, int start){
this.mUserAccount = userAccount;
this.stop = stop;
this.limit = limit;
this.start = start;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
params.add(mUserAccount.getObjectId());
params.add(this.stop);
params.add(this.limit);
params.add(this.start);
return new ApiCall(apiId, RPC.CALL_GET_RELATIVE_ACCOUNT_HISTORY, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,44 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import cy.agorise.graphenej.Asset;
import cy.agorise.graphenej.BaseOperation;
import cy.agorise.graphenej.BlockData;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.Transaction;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
/**
* Wrapper around the "get_required_fees" API call
*/
public class GetRequiredFees implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_DATABASE;
private Transaction mTransaction;
private Asset mFeeAsset;
public GetRequiredFees(Transaction transaction, Asset feeAsset){
this.mTransaction = transaction;
this.mFeeAsset = feeAsset;
}
public GetRequiredFees(List<BaseOperation> operations, Asset feeAsset){
this.mTransaction = new Transaction(new BlockData(0, 0, 0), operations);
this.mFeeAsset = feeAsset;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
// Building a new API call to request fees information
ArrayList<Serializable> accountParams = new ArrayList<>();
accountParams.add((Serializable) mTransaction.getOperations());
accountParams.add(this.mFeeAsset.getObjectId());
return new ApiCall(apiId, RPC.CALL_GET_REQUIRED_FEES, accountParams, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,44 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
public class ListAssets implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_DATABASE;
/**
* Constant that must be used as argument to the constructor of this class to indicate
* that the user wants to get all existing assets.
*/
public static final int LIST_ALL = -1;
/**
* Internal constant used to represent the maximum limit of assets retrieved in one call.
*/
public static final int MAX_BATCH_SIZE = 100;
private String lowerBound;
private int limit;
public ListAssets(String lowerBoundSymbol, int limit){
this.lowerBound = lowerBoundSymbol;
this.limit = limit;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
params.add(this.lowerBound);
if(limit > MAX_BATCH_SIZE || limit == LIST_ALL){
params.add(MAX_BATCH_SIZE);
}else{
params.add(this.limit);
}
return new ApiCall(apiId, RPC.CALL_LIST_ASSETS, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,37 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import cy.agorise.graphenej.Asset;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
public class LookupAssetSymbols implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_NONE;
private List<Asset> mAssetList;
public LookupAssetSymbols(List<Asset> assetList){
this.mAssetList = assetList;
}
public LookupAssetSymbols(Asset asset){
mAssetList = new ArrayList<Asset>();
mAssetList.add(asset);
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
ArrayList<String> subArray = new ArrayList<>();
for(int i = 0; i < mAssetList.size(); i++){
Asset asset = mAssetList.get(i);
subArray.add(asset.getObjectId());
params.add(subArray);
}
return new ApiCall(apiId, RPC.CALL_LOOKUP_ASSET_SYMBOLS, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,26 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.models.ApiCall;
public class SetSubscribeCallback implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_DATABASE;
private boolean clearFilter;
public SetSubscribeCallback(boolean clearFilter){
this.clearFilter = clearFilter;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> subscriptionParams = new ArrayList<>();
subscriptionParams.add(new Long(sequenceId));
subscriptionParams.add(clearFilter);
return new ApiCall(apiId, RPC.CALL_SET_SUBSCRIBE_CALLBACK, subscriptionParams, RPC.VERSION, sequenceId);
}
}

View File

@ -19,6 +19,7 @@ import cy.agorise.graphenej.interfaces.JsonSerializable;
* @see <a href="http://docs.bitshares.org/api/websocket.html">Websocket Calls & Notifications</a>
*/
public class ApiCall implements JsonSerializable {
public static final String KEY_SEQUENCE_ID = "id";
public static final String KEY_METHOD = "method";
public static final String KEY_PARAMS = "params";
@ -65,34 +66,39 @@ public class ApiCall implements JsonSerializable {
paramsArray.add(this.apiId);
paramsArray.add(this.methodToCall);
JsonArray methodParams = new JsonArray();
for(int i = 0; i < this.params.size(); i++){
if(this.params.get(i) instanceof JsonSerializable) {
// Sometimes the parameters are objects
methodParams.add(((JsonSerializable) this.params.get(i)).toJsonObject());
}else if (Number.class.isInstance(this.params.get(i))){
// Other times they are numbers
methodParams.add( (Number) this.params.get(i));
}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) {
// 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++) {
Serializable element = listArgument.get(l);
if (element instanceof JsonSerializable)
array.add(((JsonSerializable) element).toJsonObject());
else if (element instanceof String) {
array.add((String) element);
if(this.params != null){
for(int i = 0; i < this.params.size(); i++){
if(this.params.get(i) instanceof JsonSerializable) {
// Sometimes the parameters are objects
methodParams.add(((JsonSerializable) this.params.get(i)).toJsonObject());
}else if (Number.class.isInstance(this.params.get(i))){
// Other times they are numbers
methodParams.add( (Number) this.params.get(i));
}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) {
// 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++) {
Serializable element = listArgument.get(l);
if (element instanceof JsonSerializable)
array.add(((JsonSerializable) element).toJsonObject());
else if (element instanceof String) {
array.add((String) element);
}else if (element instanceof Long){
array.add((Long) element);
}else if(element instanceof Integer){
array.add((Integer) 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());
}
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());
}
}
paramsArray.add(methodParams);

View File

@ -1,7 +1,8 @@
package cy.agorise.graphenej.models;
/**
* Created by nelson on 11/12/16.
* Base response class
* @deprecated Use {@link JsonRpcResponse} instead
*/
public class BaseResponse {
public long id;

View File

@ -1,12 +1,11 @@
package cy.agorise.graphenej.models;
/**
* Created by nelson on 12/13/16.
* Class used to represent the response to the 'get_block_header' API call.
*/
public class BlockHeader {
public String previous;
public String timestamp;
public String witness;
public String transaction_merkle_root;
public Object[] extension;
}

View File

@ -0,0 +1,68 @@
package cy.agorise.graphenej.models;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
/**
* Model class used in the de-serialization of the response to the 'get_full_accounts' API call.
* @see cy.agorise.graphenej.api.calls.GetFullAccounts
*/
public class FullAccountDetails {
private AccountProperties account;
private Statistics statistics;
public FullAccountDetails(AccountProperties properties, Statistics statistics){
this.account = properties;
this.statistics = statistics;
}
public AccountProperties getAccount() {
return account;
}
public void setAccount(AccountProperties account) {
this.account = account;
}
public Statistics getStatistics() {
return statistics;
}
public void setStatistics(Statistics statistics) {
this.statistics = statistics;
}
public static class Statistics {
public String id;
public String owner;
public String name;
public String most_recent_op;
public long total_ops;
public long removed_ops;
public long total_core_in_orders;
public String core_in_balance;
public boolean has_cashback_vb;
public boolean is_voting;
public long lifetime_fees_paid;
public long pending_fees;
public long pending_vested_fees;
}
public static class FullAccountDeserializer implements JsonDeserializer<FullAccountDetails> {
@Override
public FullAccountDetails deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonArray array = (JsonArray) json;
JsonObject jsonObject = (JsonObject) array.get(1);
AccountProperties properties = context.deserialize(jsonObject.get("account"), AccountProperties.class);
Statistics statistics = context.deserialize(jsonObject.get("statistics"), Statistics.class);
return new FullAccountDetails(properties, statistics);
}
}
}

View File

@ -1,69 +0,0 @@
package cy.agorise.graphenej.models;
import cy.agorise.graphenej.operations.TransferOperation;
/**
* This class offers support to deserialization of transfer operations received by the API
* method get_relative_account_history.
*
* More operations types might be listed in the response of that method, but by using this class
* those will be filtered out of the parsed result.
*/
public class HistoricalTransfer {
private String id;
private TransferOperation op;
public Object[] result;
private long block_num;
private long trx_in_block;
private long op_in_trx;
private long virtual_op;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public TransferOperation getOperation() {
return op;
}
public void setOperation(TransferOperation op) {
this.op = op;
}
public long getBlockNum() {
return block_num;
}
public void setBlockNum(long block_num) {
this.block_num = block_num;
}
public long getTransactionsInBlock() {
return trx_in_block;
}
public void setTransactionsInBlock(long trx_in_block) {
this.trx_in_block = trx_in_block;
}
public long getOperationsInTrx() {
return op_in_trx;
}
public void setOperationsInTrx(long op_in_trx) {
this.op_in_trx = op_in_trx;
}
public long getVirtualOp() {
return virtual_op;
}
public void setVirtualOp(long virtual_op) {
this.virtual_op = virtual_op;
}
}

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

@ -0,0 +1,101 @@
package cy.agorise.graphenej.models;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.io.Serializable;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import cy.agorise.graphenej.GrapheneObject;
import cy.agorise.graphenej.ObjectType;
import cy.agorise.graphenej.OperationType;
import cy.agorise.graphenej.Transaction;
/**
* Class that represents a generic subscription notification.
* The template for every subscription response is the following:
*
* {
* "method": "notice"
* "params": [
* SUBSCRIPTION_ID,
* [[
* { "id": "2.1.0", ... },
* { "id": ... },
* { "id": ... },
* { "id": ... }
* ]]
* ],
* }
*/
public class JsonRpcNotification {
public static final String KEY_METHOD = "method";
public static final String KEY_PARAMS = "params";
public String method;
public List<Serializable> params;
/**
* Inner static class used to parse and deserialize subscription notifications.
*/
public static class JsonRpcNotificationDeserializer implements JsonDeserializer<JsonRpcNotification> {
@Override
public JsonRpcNotification deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonRpcNotification notification = new JsonRpcNotification();
JsonObject responseObject = json.getAsJsonObject();
if(!responseObject.has(KEY_METHOD)){
return notification;
}
notification.method = responseObject.get(KEY_METHOD).getAsString();
JsonArray paramsArray = responseObject.get(KEY_PARAMS).getAsJsonArray();
notification.params = new ArrayList<>();
notification.params.add(paramsArray.get(0).getAsInt());
ArrayList<Serializable> secondArgument = new ArrayList<>();
notification.params.add(secondArgument);
JsonArray subArray = paramsArray.get(1).getAsJsonArray().get(0).getAsJsonArray();
for(JsonElement object : subArray){
if(object.isJsonObject()){
GrapheneObject grapheneObject = new GrapheneObject(object.getAsJsonObject().get(GrapheneObject.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 dynamicGlobalProperties = context.deserialize(object, DynamicGlobalProperties.class);
secondArgument.add(dynamicGlobalProperties);
}else if(grapheneObject.getObjectType() == ObjectType.TRANSACTION_OBJECT){
BroadcastedTransaction broadcastedTransaction = new BroadcastedTransaction(grapheneObject.getObjectId());
broadcastedTransaction.setTransaction((Transaction) context.deserialize(jsonObject.get(BroadcastedTransaction.KEY_TRX), Transaction.class));
broadcastedTransaction.setTransactionId(jsonObject.get(BroadcastedTransaction.KEY_TRX_ID).getAsString());
secondArgument.add(broadcastedTransaction);
}else if(grapheneObject.getObjectType() == ObjectType.OPERATION_HISTORY_OBJECT){
if(jsonObject.get(OperationHistory.KEY_OP).getAsJsonArray().get(0).getAsLong() == OperationType.TRANSFER_OPERATION.ordinal()){
OperationHistory operationHistory = context.deserialize(jsonObject, OperationHistory.class);
secondArgument.add(operationHistory);
}else{
//TODO: Add support for other operations
}
}else{
//TODO: Add support for other types of objects
}
}else{
secondArgument.add(object.getAsString());
}
}
return notification;
}
}
}

View File

@ -0,0 +1,31 @@
package cy.agorise.graphenej.models;
/**
* Used to represent a JSON-RPC response object
*/
public class JsonRpcResponse<T> {
public long id;
public Error error;
public T result;
public static class Error {
public ErrorData data;
public int code;
public String message;
public Error(String message){
this.message = message;
}
}
public static class ErrorData {
public int code;
public String name;
public String message;
//TODO: Include stack data
public ErrorData(String message){
this.message = message;
}
}
}

View File

@ -0,0 +1,136 @@
package cy.agorise.graphenej.models;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.io.Serializable;
import java.lang.reflect.Type;
import cy.agorise.graphenej.BaseOperation;
import cy.agorise.graphenej.GrapheneObject;
/**
* This class offers support to deserialization of transfer operations received by the API
* method get_relative_account_history.
*
* More operations types might be listed in the response of that method, but by using this class
* those will be filtered out of the parsed result.
*/
public class OperationHistory extends GrapheneObject implements Serializable {
public static final String KEY_OP = "op";
public static final String KEY_BLOCK_NUM = "block_num";
public static final String KEY_TRX_IN_BLOCK = "trx_in_block";
public static final String KEY_OP_IN_TRX = "op_in_trx";
public static final String KEY_VIRTUAL_OP = "virtual_op";
private BaseOperation op;
public Object[] result;
private long block_num;
private long trx_in_block;
private long op_in_trx;
private long virtual_op;
public OperationHistory(String id) {
super(id);
}
public BaseOperation getOperation() {
return op;
}
public void setOperation(BaseOperation op) {
this.op = op;
}
public long getBlockNum() {
return block_num;
}
public void setBlockNum(long block_num) {
this.block_num = block_num;
}
public long getTransactionsInBlock() {
return trx_in_block;
}
public void setTransactionsInBlock(long trx_in_block) {
this.trx_in_block = trx_in_block;
}
public long getOperationsInTrx() {
return op_in_trx;
}
public void setOperationsInTrx(long op_in_trx) {
this.op_in_trx = op_in_trx;
}
public long getVirtualOp() {
return virtual_op;
}
public void setVirtualOp(long virtual_op) {
this.virtual_op = virtual_op;
}
/**
* Deserializer used to transform a an operation history object from its serialized form to an
* OperationHistory instance.
*
* The serialized form of this object is the following:
*
* {
"id": "1.11.178205535",
"op": [
14,
{
"fee": {
"amount": 10425,
"asset_id": "1.3.0"
},
"issuer": "1.2.374566",
"asset_to_issue": {
"amount": 8387660,
"asset_id": "1.3.3271"
},
"issue_to_account": "1.2.797835",
"extensions": []
}
],
"result": [
0,
{}
],
"block_num": 26473240,
"trx_in_block": 11,
"op_in_trx": 0,
"virtual_op": 660
}
* //TODO: Expand this deserializer for operation history objects that have an operation other than the transfer operation
*/
public static class OperationHistoryDeserializer implements JsonDeserializer<OperationHistory> {
@Override
public OperationHistory deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String id = jsonObject.get(KEY_ID).getAsString();
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();
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(operation);
operationHistory.setVirtualOp(virtualOp);
return operationHistory;
}
}
}

View File

@ -16,6 +16,7 @@ import java.util.List;
import cy.agorise.graphenej.GrapheneObject;
import cy.agorise.graphenej.ObjectType;
import cy.agorise.graphenej.OperationType;
import cy.agorise.graphenej.Transaction;
import cy.agorise.graphenej.interfaces.SubscriptionListener;
@ -43,15 +44,12 @@ import cy.agorise.graphenej.interfaces.SubscriptionListener;
* 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 {
private static final String TAG = "SubscriptionResponse";
public static final String KEY_ID = "id";
public static final String KEY_METHOD = "method";
public static final String KEY_PARAMS = "params";
public int id;
public String method;
public List<Serializable> params;
@ -182,6 +180,14 @@ public class SubscriptionResponse {
broadcastedTransaction.setTransactionId(jsonObject.get(BroadcastedTransaction.KEY_TRX_ID).getAsString());
objectMap.put(ObjectType.TRANSACTION_OBJECT, true);
secondArgument.add(broadcastedTransaction);
}else if(grapheneObject.getObjectType() == ObjectType.OPERATION_HISTORY_OBJECT){
if(jsonObject.get(OperationHistory.KEY_OP).getAsJsonArray().get(0).getAsLong() == OperationType.TRANSFER_OPERATION.ordinal()){
OperationHistory operationHistory = context.deserialize(jsonObject, OperationHistory.class);
objectMap.put(ObjectType.OPERATION_HISTORY_OBJECT, true);
secondArgument.add(operationHistory);
}else{
//TODO: Add support for other operations
}
}else{
//TODO: Add support for other types of objects
}

View File

@ -2,6 +2,7 @@ package cy.agorise.graphenej.models;
/**
* Generic witness response
* @deprecated Use {@link JsonRpcResponse} instead
*/
public class WitnessResponse<T> extends BaseResponse{
public static final String KEY_ID = "id";

View File

@ -17,7 +17,7 @@ import cy.agorise.graphenej.AssetAmount;
import cy.agorise.graphenej.BaseOperation;
import cy.agorise.graphenej.OperationType;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.objects.Memo;
import cy.agorise.graphenej.Memo;
/**
* Class used to encapsulate the TransferOperation operation related functionalities.

View File

@ -3,7 +3,7 @@ package cy.agorise.graphenej.operations;
import cy.agorise.graphenej.AssetAmount;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.errors.MalformedOperationException;
import cy.agorise.graphenej.objects.Memo;
import cy.agorise.graphenej.Memo;
/**
* Factory class used to build a transfer operation

View File

@ -10,6 +10,9 @@ import org.junit.Test;
*/
public class BrainKeyTest {
public final String TEST_BRAINKEY = "BARIC BICKERN LITZ TIPFUL JINGLED POOL TUMBAK PURIST APOPYLE DURAIN SATLIJK FAUCAL";
public final String TEST_BRAINKEY_OPENLEDGER = "ona refan abscise neebor battik terbia bandit sundra gasser debar phytol frat hauler accede primy garland";
private BrainKey mBrainKey;
@Before
@ -17,6 +20,9 @@ public class BrainKeyTest {
mBrainKey = new BrainKey(TEST_BRAINKEY, BrainKey.DEFAULT_SEQUENCE_NUMBER);
}
/**
* Test making sure that a simple brainkey can successfully generate the expected public address
*/
@Test
public void testAddress(){
Address address = mBrainKey.getPublicAddress(Address.BITSHARES_PREFIX);
@ -24,4 +30,25 @@ public class BrainKeyTest {
"BTS61UqqgE3ARuTGcckzARsdQm4EMFdBEwYyi1pbwyHrZZWrCDhT2",
address.toString());
}
/**
* Test making sure that a OpenLedger's brainkey can successfully generate the given
* 'owner' and 'active' keys.
*/
@Test
public void testOpenledgerAddress(){
BrainKey brainKey1 = new BrainKey(TEST_BRAINKEY_OPENLEDGER, 0);
BrainKey brainKey2 = new BrainKey(TEST_BRAINKEY_OPENLEDGER, 1);
Address ownerAddress = brainKey1.getPublicAddress(Address.BITSHARES_PREFIX);
Address activeAddress = brainKey2.getPublicAddress(Address.BITSHARES_PREFIX);
Assert.assertEquals("Owner address matches",
"BTS6dqT3J7tUcZP6xHo2mHkL8tq8zw5TQgGd6ntRMXH1EoNsCWTzm",
ownerAddress.toString());
Assert.assertEquals("Active address matches",
"BTS6DKvgY3yPyN7wKrhBGYhrnghhLSVCYz3ugUdi9pDPkicS6B7N2",
activeAddress.toString());
}
}

View File

@ -24,7 +24,6 @@ import cy.agorise.graphenej.api.TransactionBroadcastSequence;
import cy.agorise.graphenej.interfaces.WitnessResponseListener;
import cy.agorise.graphenej.models.BaseResponse;
import cy.agorise.graphenej.models.WitnessResponse;
import cy.agorise.graphenej.objects.Memo;
import cy.agorise.graphenej.operations.CustomOperation;
import cy.agorise.graphenej.operations.LimitOrderCancelOperation;
import cy.agorise.graphenej.operations.LimitOrderCreateOperation;

View File

@ -0,0 +1,67 @@
package cy.agorise.graphenej.api;
import com.neovisionaries.ws.client.WebSocketException;
import junit.framework.Assert;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.interfaces.WitnessResponseListener;
import cy.agorise.graphenej.models.AccountProperties;
import cy.agorise.graphenej.models.BaseResponse;
import cy.agorise.graphenej.models.WitnessResponse;
public class GetAccountsTest extends BaseApiTest {
private UserAccount ltmAccount = new UserAccount("1.2.99700");
private UserAccount nonLtmAccount = new UserAccount("1.2.140994");
@Test
public void testGetAccount(){
ArrayList<UserAccount> userAccounts = new ArrayList<>();
userAccounts.add(ltmAccount);
userAccounts.add(nonLtmAccount);
mWebSocket.addListener(new GetAccounts(userAccounts, true, new WitnessResponseListener(){
@Override
public void onSuccess(WitnessResponse response) {
System.out.println("onSuccess.");
List<AccountProperties> accounts = (List<AccountProperties>) response.result;
System.out.println(String.format("Got %d accounts", accounts.size()));
for(AccountProperties accountProperties : accounts){
System.out.println("account name....: "+accountProperties.name);
System.out.println("expiration date.: "+accountProperties.membership_expiration_date);
}
AccountProperties ltmAccountProperties = accounts.get(0);
AccountProperties nonLtmAccountProperties = accounts.get(1);
Assert.assertEquals(ltmAccountProperties.membership_expiration_date, UserAccount.LIFETIME_EXPIRATION_DATE);
Assert.assertFalse(nonLtmAccountProperties.membership_expiration_date.equals(UserAccount.LIFETIME_EXPIRATION_DATE));
synchronized (GetAccountsTest.this){
GetAccountsTest.this.notifyAll();
}
}
@Override
public void onError(BaseResponse.Error error) {
System.out.println("onError. Msg: "+error.message);
synchronized (GetAccountsTest.this){
GetAccountsTest.this.notifyAll();
}
}
}));
try{
mWebSocket.connect();
synchronized (this){
wait();
}
}catch (WebSocketException e) {
System.out.println("WebSocketException. Msg: " + e.getMessage());
} catch (InterruptedException e) {
System.out.println("InterruptedException. Msg: "+e.getMessage());
}
}
}

View File

@ -27,6 +27,7 @@ public class GetObjectsTest extends BaseApiTest{
private final Asset asset = new Asset("1.3.0", "BTS", 5);
private final UserAccount account = new UserAccount("1.2.116354");
private final UserAccount bilthon_25 = new UserAccount("1.2.151069");
private UserAccount ltmAccount = new UserAccount("1.2.99700");
private final String[] bitAssetIds = new String[]{"2.4.21", "2.4.83"};
@Test
@ -109,6 +110,50 @@ public class GetObjectsTest extends BaseApiTest{
}
}
@Test
public void testGetLtmAccount(){
ArrayList<String> ids = new ArrayList<>();
ids.add(ltmAccount.getObjectId());
mWebSocket.addListener(new GetObjects(ids, new WitnessResponseListener() {
@Override
public void onSuccess(WitnessResponse response) {
System.out.println("onSuccess");
List<GrapheneObject> result = (List<GrapheneObject>) response.result;
UserAccount userAccount = (UserAccount) result.get(0);
System.out.println("Account name.....: "+userAccount.getName());
System.out.println("Is LTM...........: "+userAccount.isLifeTime());
System.out.println("json string......: "+userAccount.toJsonString());
System.out.println("owner............: "+userAccount.getOwner().getKeyAuthList().get(0).getAddress());
System.out.println("active key.......: "+userAccount.getActive().getKeyAuthList().get(0).getAddress());
System.out.println("memo: "+userAccount.getOptions().getMemoKey().getAddress());
Assert.assertEquals("We expect this account to be LTM",true, userAccount.isLifeTime());
synchronized (GetObjectsTest.this){
GetObjectsTest.this.notifyAll();
}
}
@Override
public void onError(BaseResponse.Error error) {
System.out.println("onError");
synchronized (GetObjectsTest.this){
GetObjectsTest.this.notifyAll();
}
}
}));
try {
mWebSocket.connect();
synchronized (this){
wait();
}
}catch (WebSocketException e) {
System.out.println("WebSocketException. Msg: " + e.getMessage());
} catch (InterruptedException e) {
System.out.println("InterruptedException. Msg: "+e.getMessage());
}
}
@Test
public void testBitAssetData(){
try{

View File

@ -8,7 +8,7 @@ import java.util.List;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.interfaces.WitnessResponseListener;
import cy.agorise.graphenej.models.BaseResponse;
import cy.agorise.graphenej.models.HistoricalTransfer;
import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.models.WitnessResponse;
import cy.agorise.graphenej.operations.TransferOperation;
@ -51,11 +51,11 @@ public class GetRelativeAccountHistoryTest extends BaseApiTest {
public void onSuccess(WitnessResponse response) {
System.out.println("mTransferHistoryListener.onSuccess");
historicalTransferCount++;
WitnessResponse<List<HistoricalTransfer>> resp = response;
for(HistoricalTransfer historicalTransfer : resp.result){
WitnessResponse<List<OperationHistory>> resp = response;
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

@ -10,13 +10,14 @@ import java.util.Timer;
import java.util.TimerTask;
import cy.agorise.graphenej.ObjectType;
import cy.agorise.graphenej.Transaction;
import cy.agorise.graphenej.interfaces.NodeErrorListener;
import cy.agorise.graphenej.interfaces.SubscriptionListener;
import cy.agorise.graphenej.models.BaseResponse;
import cy.agorise.graphenej.models.BroadcastedTransaction;
import cy.agorise.graphenej.models.DynamicGlobalProperties;
import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.models.SubscriptionResponse;
import cy.agorise.graphenej.Transaction;
/**
* Class used to encapsulate all tests that relate to the {@see SubscriptionMessagesHub} class.
@ -178,7 +179,7 @@ public class SubscriptionMessagesHubTest extends BaseApiTest {
@Test
public void testBroadcastedTransactionDeserializer(){
try{
mMessagesHub = new SubscriptionMessagesHub("", "", mErrorListener);
mMessagesHub = new SubscriptionMessagesHub("", "", true, mErrorListener);
mMessagesHub.addSubscriptionListener(new SubscriptionListener() {
private int MAX_MESSAGES = 15;
private int messageCounter = 0;
@ -197,7 +198,7 @@ public class SubscriptionMessagesHubTest extends BaseApiTest {
if(item instanceof BroadcastedTransaction){
BroadcastedTransaction broadcastedTransaction = (BroadcastedTransaction) item;
Transaction tx = broadcastedTransaction.getTransaction();
System.out.println(String.format("Got %d operations", tx.getOperations().size()));
// System.out.println(String.format("Got %d operations", tx.getOperations().size()));
}
}
}
@ -213,6 +214,30 @@ public class SubscriptionMessagesHubTest extends BaseApiTest {
}
});
mMessagesHub.addSubscriptionListener(new SubscriptionListener() {
@Override
public ObjectType getInterestObjectType() {
return ObjectType.OPERATION_HISTORY_OBJECT;
}
@Override
public void onSubscriptionUpdate(SubscriptionResponse response) {
System.out.println("onSubscriptionUpdate. response.params.size: "+response.params.size());
if(response.params.size() == 2){
List<Serializable> payload = (List) response.params.get(1);
if(payload.size() > 0){
for(Serializable item : payload){
if(item instanceof OperationHistory){
OperationHistory operationHistory = (OperationHistory) item;
System.out.println("Operation history: <id:"+operationHistory.getObjectId()+", op: "+operationHistory.getOperation().toJsonString()+">");
}
}
}
}
}
});
mWebSocket.addListener(mMessagesHub);
mWebSocket.connect();

View File

@ -0,0 +1,25 @@
package cy.agorise.graphenej.api.calls;
import junit.framework.Assert;
import org.junit.Test;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.models.ApiCall;
public class GetAccountHistoryTest {
@Test
public void testSerialization(){
UserAccount userAccount = new UserAccount("1.2.139293");
String end = "1.11.225030218";
String start = "1.11.225487973";
int limit = 20;
GetAccountHistory getAccountHistory = new GetAccountHistory(userAccount, start, end, limit);
ApiCall apiCall = getAccountHistory.toApiCall(2, 3);
String serialized = apiCall.toJsonString();
System.out.println("> "+serialized);
String expected = "{\"id\":3,\"method\":\"call\",\"params\":[2,\"get_account_history\",[\"1.2.139293\",\"1.11.225030218\",20,\"1.11.225487973\"]],\"jsonrpc\":\"2.0\"}";
Assert.assertEquals("Serialized is as expected", expected, serialized);
}
}

View File

@ -0,0 +1,39 @@
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 java.util.List;
import cy.agorise.graphenej.AccountOptions;
import cy.agorise.graphenej.Authority;
import cy.agorise.graphenej.Memo;
public class FullAccountDetailsTest {
@Test
public void testDeserialization(){
String serialized = "{\"id\":0,\"jsonrpc\":\"2.0\",\"result\":[[\"bilthon-1\",{\"account\":{\"id\":\"1.2.139205\",\"membership_expiration_date\":\"1970-01-01T00:00:00\",\"registrar\":\"1.2.117600\",\"referrer\":\"1.2.90200\",\"lifetime_referrer\":\"1.2.90200\",\"network_fee_percentage\":2000,\"lifetime_referrer_fee_percentage\":3000,\"referrer_rewards_percentage\":9000,\"name\":\"bilthon-1\",\"owner\":{\"weight_threshold\":1,\"account_auths\":[],\"key_auths\":[[\"BTS8RiFgs8HkcVPVobHLKEv6yL3iXcC9SWjbPVS15dDAXLG9GYhnY\",1]],\"address_auths\":[]},\"active\":{\"weight_threshold\":1,\"account_auths\":[],\"key_auths\":[[\"BTS8RiFgs8HkcVPVobHLKEv6yL3iXcC9SWjbPVS15dDAXLG9GYhnY\",1]],\"address_auths\":[]},\"options\":{\"memo_key\":\"BTS8RiFgs8HkcVPVobHLKEv6yL3iXcC9SWjbPVS15dDAXLG9GYhnY\",\"voting_account\":\"1.2.5\",\"num_witness\":0,\"num_committee\":0,\"votes\":[],\"extensions\":[]},\"statistics\":\"2.6.139205\",\"whitelisting_accounts\":[],\"blacklisting_accounts\":[],\"whitelisted_accounts\":[],\"blacklisted_accounts\":[],\"owner_special_authority\":[0,{}],\"active_special_authority\":[0,{}],\"top_n_control_flags\":0},\"statistics\":{\"id\":\"2.6.139205\",\"owner\":\"1.2.139205\",\"name\":\"bilthon-1\",\"most_recent_op\":\"2.9.6668024\",\"total_ops\":3,\"removed_ops\":0,\"total_core_in_orders\":0,\"core_in_balance\":71279,\"has_cashback_vb\":false,\"is_voting\":false,\"lifetime_fees_paid\":28721,\"pending_fees\":0,\"pending_vested_fees\":0},\"registrar_name\":\"bitshares-munich-faucet\",\"referrer_name\":\"bitshares-munich\",\"lifetime_referrer_name\":\"bitshares-munich\",\"votes\":[],\"balances\":[{\"id\":\"2.5.44951\",\"owner\":\"1.2.139205\",\"asset_type\":\"1.3.0\",\"balance\":71279,\"maintenance_flag\":false}],\"vesting_balances\":[],\"limit_orders\":[],\"call_orders\":[],\"settle_orders\":[],\"proposals\":[],\"assets\":[],\"withdraws\":[]}],[\"bilthon-2\",{\"account\":{\"id\":\"1.2.139207\",\"membership_expiration_date\":\"1970-01-01T00:00:00\",\"registrar\":\"1.2.117600\",\"referrer\":\"1.2.90200\",\"lifetime_referrer\":\"1.2.90200\",\"network_fee_percentage\":2000,\"lifetime_referrer_fee_percentage\":3000,\"referrer_rewards_percentage\":9000,\"name\":\"bilthon-2\",\"owner\":{\"weight_threshold\":1,\"account_auths\":[],\"key_auths\":[[\"BTS7gD2wtSauXpSCBin1rYctBcPWeZieX7YrVk1DuQpg9peczSqTv\",1]],\"address_auths\":[]},\"active\":{\"weight_threshold\":1,\"account_auths\":[],\"key_auths\":[[\"BTS7gD2wtSauXpSCBin1rYctBcPWeZieX7YrVk1DuQpg9peczSqTv\",1]],\"address_auths\":[]},\"options\":{\"memo_key\":\"BTS7gD2wtSauXpSCBin1rYctBcPWeZieX7YrVk1DuQpg9peczSqTv\",\"voting_account\":\"1.2.5\",\"num_witness\":0,\"num_committee\":0,\"votes\":[],\"extensions\":[]},\"statistics\":\"2.6.139207\",\"whitelisting_accounts\":[],\"blacklisting_accounts\":[],\"whitelisted_accounts\":[],\"blacklisted_accounts\":[],\"owner_special_authority\":[0,{}],\"active_special_authority\":[0,{}],\"top_n_control_flags\":0},\"statistics\":{\"id\":\"2.6.139207\",\"owner\":\"1.2.139207\",\"name\":\"bilthon-2\",\"most_recent_op\":\"2.9.6159244\",\"total_ops\":1,\"removed_ops\":0,\"total_core_in_orders\":0,\"core_in_balance\":0,\"has_cashback_vb\":false,\"is_voting\":false,\"lifetime_fees_paid\":0,\"pending_fees\":0,\"pending_vested_fees\":0},\"registrar_name\":\"bitshares-munich-faucet\",\"referrer_name\":\"bitshares-munich\",\"lifetime_referrer_name\":\"bitshares-munich\",\"votes\":[],\"balances\":[],\"vesting_balances\":[],\"limit_orders\":[],\"call_orders\":[],\"settle_orders\":[],\"proposals\":[],\"assets\":[],\"withdraws\":[]}]]}";
Gson gson = new GsonBuilder()
.registerTypeAdapter(FullAccountDetails.class, new FullAccountDetails.FullAccountDeserializer())
.registerTypeAdapter(Authority.class, new Authority.AuthorityDeserializer())
.registerTypeAdapter(Memo.class, new Memo.MemoDeserializer())
.registerTypeAdapter(AccountOptions.class, new AccountOptions.AccountOptionsDeserializer())
.create();
Type FullAccountDetailsResponse = new TypeToken<JsonRpcResponse<List<FullAccountDetails>>>() {}.getType();
JsonRpcResponse<List<FullAccountDetails>> response = gson.fromJson(serialized, FullAccountDetailsResponse);
Assert.assertNotNull(response.result);
Assert.assertNull(response.error);
List<FullAccountDetails> fullAccountDetailsList = response.result;
Assert.assertNotNull(fullAccountDetailsList);
Assert.assertEquals(2, fullAccountDetailsList.size());
Assert.assertNotNull(fullAccountDetailsList.get(0).getAccount());
Assert.assertEquals("bilthon-1", fullAccountDetailsList.get(0).getAccount().name);
}
}

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.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

@ -0,0 +1,59 @@
package cy.agorise.graphenej.models;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.junit.Assert;
import org.junit.Test;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.AssetAmount;
import cy.agorise.graphenej.Transaction;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.Memo;
import cy.agorise.graphenej.operations.CustomOperation;
import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
import cy.agorise.graphenej.operations.TransferOperation;
public class JsonRpcNotificationTest {
private String text = "{\"method\":\"notice\",\"params\":[3,[[{\"id\":\"2.1.0\",\"head_block_number\":30071834,\"head_block_id\":\"01cadc1a5f3f517e2eba9588111aef3af3c59916\",\"time\":\"2018-08-30T18:19:45\",\"current_witness\":\"1.6.74\",\"next_maintenance_time\":\"2018-08-30T19:00:00\",\"last_budget_time\":\"2018-08-30T18:00:00\",\"witness_budget\":80800000,\"accounts_registered_this_interval\":9,\"recently_missed_count\":0,\"current_aslot\":30228263,\"recent_slots_filled\":\"340282366920938463463374607431768211455\",\"dynamic_flags\":0,\"last_irreversible_block_num\":30071813}]]]}";
@Test
public void failResponseDeserialization(){
Gson gson = new Gson();
JsonRpcResponse<?> response = gson.fromJson(text, JsonRpcResponse.class);
// The result field of this de-serialized object should be null
Assert.assertNull(response.result);
}
@Test
public void succeedNotificationDeserialization(){
Gson gson = new GsonBuilder()
.registerTypeAdapter(Transaction.class, new Transaction.TransactionDeserializer())
.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer())
.registerTypeAdapter(LimitOrderCreateOperation.class, new LimitOrderCreateOperation.LimitOrderCreateDeserializer())
.registerTypeAdapter(CustomOperation.class, new CustomOperation.CustomOperationDeserializer())
.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer())
.registerTypeAdapter(DynamicGlobalProperties.class, new DynamicGlobalProperties.DynamicGlobalPropertiesDeserializer())
.registerTypeAdapter(Memo.class, new Memo.MemoDeserializer())
.registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer())
.registerTypeAdapter(JsonRpcNotification.class, new JsonRpcNotification.JsonRpcNotificationDeserializer())
.create();
JsonRpcNotification notification = gson.fromJson(text, JsonRpcNotification.class);
// Should deserialize a 'params' array with 2 elements
Assert.assertEquals(2, notification.params.size());
// The first element should be the number 3
Assert.assertEquals(3, notification.params.get(0));
ArrayList<Serializable> secondArgument = (ArrayList<Serializable>) notification.params.get(1);
// The second element should be an array of length 1
Assert.assertEquals(1, secondArgument.size());
// Extracting the payload, which should be in itself another array
DynamicGlobalProperties payload = (DynamicGlobalProperties) secondArgument.get(0);
// Dynamic global properties head_block_number should match
Assert.assertEquals(30071834, payload.head_block_number);
}
}

View File

@ -0,0 +1,20 @@
package cy.agorise.graphenej.models;
import com.google.gson.Gson;
import junit.framework.Assert;
import org.junit.Test;
public class JsonRpcResponseTest {
@Test
public void deserializeJsonRpcResponse(){
String text = "{\"id\":4,\"jsonrpc\":\"2.0\",\"result\":[{\"id\":\"2.1.0\",\"head_block_number\":30071833,\"head_block_id\":\"01cadc1964cb04ab551463e26033ab0f159bc8e1\",\"time\":\"2018-08-30T18:19:42\",\"current_witness\":\"1.6.71\",\"next_maintenance_time\":\"2018-08-30T19:00:00\",\"last_budget_time\":\"2018-08-30T18:00:00\",\"witness_budget\":80900000,\"accounts_registered_this_interval\":9,\"recently_missed_count\":0,\"current_aslot\":30228262,\"recent_slots_filled\":\"340282366920938463463374607431768211455\",\"dynamic_flags\":0,\"last_irreversible_block_num\":30071813}]}";
Gson gson = new Gson();
JsonRpcResponse<?> response = gson.fromJson(text, JsonRpcResponse.class);
System.out.println("response: "+response.result);
Assert.assertNotNull(response);
Assert.assertNotNull(response.result);
}
}

View File

@ -13,6 +13,7 @@ import org.junit.Test;
import java.math.BigInteger;
import cy.agorise.graphenej.Address;
import cy.agorise.graphenej.Memo;
import cy.agorise.graphenej.PublicKey;
import cy.agorise.graphenej.TestAccounts;
import cy.agorise.graphenej.Util;

1
sample/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

47
sample/build.gradle Normal file
View File

@ -0,0 +1,47 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 27
defaultConfig {
applicationId "cy.agorise.labs.sample"
minSdkVersion 14
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api project(':graphenej')
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
implementation 'com.jakewharton:butterknife:8.8.1'
implementation 'com.google.code.gson:gson:2.8.4'
implementation 'com.google.guava:guava:25.0-jre'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', {
exclude group: 'com.android.support', module: 'support-annotations'
})
implementation 'com.android.support:multidex:1.0.1'
}

21
sample/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,26 @@
package cy.sample.labs.sample;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.luminiasoft.labs.sample", appContext.getPackageName());
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cy.agorise.labs.sample">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".SampleApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".SubscriptionActivity" />
<activity android:name=".CallsActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".PerformCallActivity"></activity>
</application>
</manifest>

View File

@ -0,0 +1,95 @@
package cy.agorise.labs.sample;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import cy.agorise.graphenej.RPC;
public class CallsActivity extends AppCompatActivity {
private final String TAG = this.getClass().getName();
@BindView(R.id.call_list)
RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_calls);
ButterKnife.bind(this);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setLayoutManager(linearLayoutManager);
mRecyclerView.addItemDecoration(new DividerItemDecoration(this, linearLayoutManager.getOrientation()));
mRecyclerView.setAdapter(new CallAdapter());
}
private final class CallAdapter extends RecyclerView.Adapter<CallAdapter.ViewHolder> {
private String[] supportedCalls = new String[]{
RPC.CALL_GET_OBJECTS,
RPC.CALL_GET_ACCOUNTS,
RPC.CALL_GET_BLOCK,
RPC.CALL_GET_BLOCK_HEADER,
RPC.CALL_GET_MARKET_HISTORY,
RPC.CALL_GET_RELATIVE_ACCOUNT_HISTORY,
RPC.CALL_GET_REQUIRED_FEES,
RPC.CALL_LOOKUP_ASSET_SYMBOLS,
RPC.CALL_LIST_ASSETS,
RPC.CALL_GET_ACCOUNT_BY_NAME,
RPC.CALL_GET_LIMIT_ORDERS,
RPC.CALL_GET_ACCOUNT_HISTORY_BY_OPERATIONS,
RPC.CALL_GET_FULL_ACCOUNTS,
RPC.CALL_SET_SUBSCRIBE_CALLBACK
};
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
TextView v = (TextView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_call, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
String name = supportedCalls[position];
String formattedName = name.replace("_", " ").toUpperCase();
holder.mCallNameView.setText(formattedName);
holder.mCallNameView.setOnClickListener((view) -> {
String selectedCall = supportedCalls[position];
Intent intent;
if(selectedCall.equals(RPC.CALL_SET_SUBSCRIBE_CALLBACK)){
intent = new Intent(CallsActivity.this, SubscriptionActivity.class);
}else{
intent = new Intent(CallsActivity.this, PerformCallActivity.class);
intent.putExtra(Constants.KEY_SELECTED_CALL, selectedCall);
}
startActivity(intent);
});
}
@Override
public int getItemCount() {
return supportedCalls.length;
}
public class ViewHolder extends RecyclerView.ViewHolder {
public TextView mCallNameView;
public ViewHolder(TextView view) {
super(view);
this.mCallNameView = view;
}
}
}
}

View File

@ -0,0 +1,62 @@
package cy.agorise.labs.sample;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import cy.agorise.graphenej.api.android.NetworkService;
public abstract class ConnectedActivity extends AppCompatActivity implements ServiceConnection {
private final String TAG = this.getClass().getName();
/* Network service connection */
protected NetworkService mNetworkService;
/**
* Flag used to keep track of the NetworkService binding state
*/
private boolean mShouldUnbindNetwork;
private ServiceConnection mNetworkServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className,
IBinder service) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
NetworkService.LocalBinder binder = (NetworkService.LocalBinder) service;
mNetworkService = binder.getService();
ConnectedActivity.this.onServiceConnected(className, service);
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
ConnectedActivity.this.onServiceDisconnected(componentName);
}
};
@Override
protected void onStart() {
super.onStart();
// Binding to NetworkService
Intent intent = new Intent(this, NetworkService.class);
if(bindService(intent, mNetworkServiceConnection, Context.BIND_AUTO_CREATE)){
mShouldUnbindNetwork = true;
}else{
Log.e(TAG,"Binding to the network service failed.");
}
}
@Override
protected void onPause() {
super.onPause();
// Unbinding from network service
if(mShouldUnbindNetwork){
unbindService(mNetworkServiceConnection);
mShouldUnbindNetwork = false;
}
}
}

View File

@ -0,0 +1,8 @@
package cy.agorise.labs.sample;
public class Constants {
/**
* Key used to pass the selected call as an intent extra
*/
public static final String KEY_SELECTED_CALL = "key_call";
}

View File

@ -0,0 +1,448 @@
package cy.agorise.labs.sample;
import android.content.ComponentName;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.IBinder;
import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout;
import android.text.InputType;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.ArrayList;
import java.util.Arrays;
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.GetFullAccounts;
import cy.agorise.graphenej.api.calls.GetLimitOrders;
import cy.agorise.graphenej.api.calls.GetObjects;
import cy.agorise.graphenej.api.calls.ListAssets;
import cy.agorise.graphenej.models.JsonRpcResponse;
import cy.agorise.graphenej.Memo;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
public class PerformCallActivity extends ConnectedActivity {
private final String TAG = this.getClass().getName();
@BindView(R.id.response)
TextView mResponseView;
@BindView(R.id.container_param1)
TextInputLayout mParam1View;
@BindView(R.id.container_param2)
TextInputLayout mParam2View;
@BindView(R.id.container_param3)
TextInputLayout mParam3View;
@BindView(R.id.container_param4)
TextInputLayout mParam4View;
@BindView(R.id.param1)
TextInputEditText param1;
@BindView(R.id.param2)
TextInputEditText param2;
@BindView(R.id.param3)
TextInputEditText param3;
@BindView(R.id.param4)
TextInputEditText param4;
@BindView(R.id.button_send)
Button mButtonSend;
// Field used to map a request id to its type
private HashMap<Long, String> responseMap = new HashMap<>();
// Current request type. Ex: 'get_objects', 'get_accounts', etc
private String mRPC;
private Disposable mDisposable;
private Gson gson = new GsonBuilder()
.setExclusionStrategies(new DeserializationMap.SkipAccountOptionsStrategy(), new DeserializationMap.SkipAssetOptionsStrategy())
.registerTypeAdapter(Memo.class, new Memo.MemoSerializer())
.create();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_perform_call);
ButterKnife.bind(this);
mRPC = getIntent().getStringExtra(Constants.KEY_SELECTED_CALL);
Log.d(TAG,"Selected call: "+mRPC);
switch (mRPC){
case RPC.CALL_GET_OBJECTS:
setupGetObjects();
break;
case RPC.CALL_GET_ACCOUNTS:
setupGetAccounts();
break;
case RPC.CALL_GET_BLOCK:
setupGetBlock();
break;
case RPC.CALL_GET_BLOCK_HEADER:
setupGetBlockHeader();
break;
case RPC.CALL_GET_MARKET_HISTORY:
setupGetMarketHistory();
break;
case RPC.CALL_GET_RELATIVE_ACCOUNT_HISTORY:
setupGetRelativeAccountHistory();
break;
case RPC.CALL_GET_REQUIRED_FEES:
break;
case RPC.CALL_LOOKUP_ASSET_SYMBOLS:
setupLookupAssetSymbols();
break;
case RPC.CALL_LIST_ASSETS:
setupListAssets();
break;
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();
case RPC.CALL_GET_FULL_ACCOUNTS:
setupGetFullAccounts();
break;
default:
Log.d(TAG,"Default called");
}
mDisposable = RxBus.getBusInstance()
.asFlowable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object message) throws Exception {
Log.d(TAG,"accept. Msg class: "+message.getClass());
if(message instanceof ConnectionStatusUpdate){
// TODO: Update UI ?
}else if(message instanceof JsonRpcResponse){
handleJsonRpcResponse((JsonRpcResponse) message);
}
}
});
}
private void setupGetObjects(){
requiredInput(1);
mParam1View.setHint(getResources().getString(R.string.get_objects_arg1));
}
private void setupGetAccounts(){
requiredInput(1);
mParam1View.setHint(getResources().getString(R.string.get_accounts_arg1));
}
private void setupGetBlock(){
requiredInput(1);
mParam1View.setHint(getResources().getString(R.string.get_block_arg1));
}
private void setupGetBlockHeader(){
requiredInput(1);
mParam1View.setHint(getResources().getString(R.string.get_block_arg1));
}
private void setupGetMarketHistory(){
requiredInput(4);
Resources resources = getResources();
mParam1View.setHint(resources.getString(R.string.get_market_history_arg1));
mParam2View.setHint(resources.getString(R.string.get_market_history_arg2));
mParam3View.setHint(resources.getString(R.string.get_market_history_arg3));
mParam4View.setHint(resources.getString(R.string.get_market_history_arg4));
}
private void setupGetRelativeAccountHistory(){
requiredInput(4);
Resources resources = getResources();
mParam1View.setHint(resources.getString(R.string.get_relative_account_history_arg1));
mParam2View.setHint(resources.getString(R.string.get_relative_account_history_arg2));
mParam3View.setHint(resources.getString(R.string.get_relative_account_history_arg3));
mParam4View.setHint(resources.getString(R.string.get_relative_account_history_arg4));
}
private void setupLookupAssetSymbols(){
requiredInput(4);
Resources resources = getResources();
mParam1View.setHint(resources.getString(R.string.lookup_asset_symbols_arg1));
mParam2View.setHint(resources.getString(R.string.lookup_asset_symbols_arg2));
mParam3View.setHint(resources.getString(R.string.lookup_asset_symbols_arg3));
mParam4View.setHint(resources.getString(R.string.lookup_asset_symbols_arg4));
}
private void setupListAssets(){
requiredInput(2);
Resources resources = getResources();
mParam1View.setHint(resources.getString(R.string.list_assets_arg1));
mParam2View.setHint(resources.getString(R.string.list_assets_arg2));
param2.setInputType(InputType.TYPE_CLASS_NUMBER);
}
private void setupAccountByName(){
requiredInput(1);
Resources resources = getResources();
mParam1View.setHint(resources.getString(R.string.get_accounts_by_name_arg1));
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();
mParam1View.setHint(resources.getString(R.string.get_limit_orders_arg1));
mParam2View.setHint(resources.getString(R.string.get_limit_orders_arg2));
mParam3View.setHint(resources.getString(R.string.get_limit_orders_arg3));
param1.setInputType(InputType.TYPE_CLASS_TEXT);
param2.setInputType(InputType.TYPE_CLASS_TEXT);
param3.setInputType(InputType.TYPE_CLASS_NUMBER);
}
private void setupGetFullAccounts(){
requiredInput(1);
mParam1View.setHint(getString(R.string.get_full_accounts_arg1));
param1.setInputType(InputType.TYPE_CLASS_TEXT);
}
private void requiredInput(int inputCount){
if(inputCount == 1){
mParam1View.setVisibility(View.VISIBLE);
mParam2View.setVisibility(View.GONE);
mParam3View.setVisibility(View.GONE);
mParam4View.setVisibility(View.GONE);
}else if(inputCount == 2){
mParam1View.setVisibility(View.VISIBLE);
mParam2View.setVisibility(View.VISIBLE);
mParam3View.setVisibility(View.GONE);
mParam4View.setVisibility(View.GONE);
}else if(inputCount == 3){
mParam1View.setVisibility(View.VISIBLE);
mParam2View.setVisibility(View.VISIBLE);
mParam3View.setVisibility(View.VISIBLE);
mParam4View.setVisibility(View.GONE);
}else if(inputCount == 4){
mParam1View.setVisibility(View.VISIBLE);
mParam2View.setVisibility(View.VISIBLE);
mParam3View.setVisibility(View.VISIBLE);
mParam4View.setVisibility(View.VISIBLE);
}
}
@OnClick(R.id.button_send)
public void onSendClicked(Button v){
switch (mRPC){
case RPC.CALL_GET_OBJECTS:
sendGetObjectsRequest();
break;
case RPC.CALL_GET_ACCOUNTS:
sendGetAccountsRequest();
break;
case RPC.CALL_GET_BLOCK:
break;
case RPC.CALL_GET_BLOCK_HEADER:
break;
case RPC.CALL_GET_MARKET_HISTORY:
break;
case RPC.CALL_GET_RELATIVE_ACCOUNT_HISTORY:
break;
case RPC.CALL_GET_REQUIRED_FEES:
break;
case RPC.CALL_LOOKUP_ASSET_SYMBOLS:
break;
case RPC.CALL_LIST_ASSETS:
sendListAssets();
break;
case RPC.CALL_GET_ACCOUNT_BY_NAME:
getAccountByName();
break;
case RPC.CALL_GET_LIMIT_ORDERS:
getLimitOrders();
break;
case RPC.CALL_GET_ACCOUNT_HISTORY_BY_OPERATIONS:
getAccountHistoryByOperations();
break;
case RPC.CALL_GET_FULL_ACCOUNTS:
getFullAccounts();
default:
Log.d(TAG,"Default called");
}
}
private void sendGetObjectsRequest(){
String objectId = param1.getText().toString();
if(objectId.matches("\\d\\.\\d{1,3}\\.\\d{1,10}")){
ArrayList<String> array = new ArrayList<>();
array.add(objectId);
GetObjects getObjects = new GetObjects(array);
long id = mNetworkService.sendMessage(getObjects, GetObjects.REQUIRED_API);
responseMap.put(id, mRPC);
}else{
param1.setError(getResources().getString(R.string.error_input_id));
}
}
private void sendGetAccountsRequest(){
String userId = param1.getText().toString();
if(userId.matches("\\d\\.\\d{1,3}\\.\\d{1,10}")){
GetAccounts getAccounts = new GetAccounts(new UserAccount(userId));
long id = mNetworkService.sendMessage(getAccounts, GetBlock.REQUIRED_API);
responseMap.put(id, mRPC);
}else{
param1.setError(getResources().getString(R.string.error_input_id));
}
}
private void sendListAssets(){
try{
String lowerBound = param1.getText().toString();
int limit = Integer.parseInt(param2.getText().toString());
ListAssets listAssets = new ListAssets(lowerBound, limit);
long id = mNetworkService.sendMessage(listAssets, ListAssets.REQUIRED_API);
responseMap.put(id, mRPC);
}catch(NumberFormatException e){
Toast.makeText(this, getString(R.string.error_number_format), Toast.LENGTH_SHORT).show();
Log.e(TAG,"NumberFormatException while reading limit value. Msg: "+e.getMessage());
}
}
private void getAccountByName(){
String accountName = param1.getText().toString();
long id = mNetworkService.sendMessage(new GetAccountByName(accountName), GetAccountByName.REQUIRED_API);
responseMap.put(id, mRPC);
}
private void getLimitOrders(){
String assetA = param1.getText().toString();
String assetB = param2.getText().toString();
try{
int limit = Integer.parseInt(param3.getText().toString());
long id = mNetworkService.sendMessage(new GetLimitOrders(assetA, assetB, limit), GetLimitOrders.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 limit value. Msg: "+e.getMessage());
}
}
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);
responseMap.put(id, mRPC);
}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());
}
}
private void getFullAccounts(){
ArrayList<String> accounts = new ArrayList<>();
accounts.addAll(Arrays.asList(param1.getText().toString().split(",")));
long id = mNetworkService.sendMessage(new GetFullAccounts(accounts, false), GetFullAccounts.REQUIRED_API);
responseMap.put(id, mRPC);
}
/**
* Internal method that will decide what to do with each JSON-RPC response
*
* @param response The JSON-RPC api call response
*/
private void handleJsonRpcResponse(JsonRpcResponse response){
long id = response.id;
if(responseMap.get(id) != null){
String request = responseMap.get(id);
switch(request){
case RPC.CALL_GET_ACCOUNTS:
case RPC.CALL_GET_BLOCK:
case RPC.CALL_GET_BLOCK_HEADER:
case RPC.CALL_GET_MARKET_HISTORY:
case RPC.CALL_GET_ACCOUNT_HISTORY:
case RPC.CALL_GET_RELATIVE_ACCOUNT_HISTORY:
case RPC.CALL_GET_REQUIRED_FEES:
case RPC.CALL_LOOKUP_ASSET_SYMBOLS:
case RPC.CALL_LIST_ASSETS:
case RPC.CALL_GET_ACCOUNT_BY_NAME:
case RPC.CALL_GET_LIMIT_ORDERS:
case RPC.CALL_GET_ACCOUNT_HISTORY_BY_OPERATIONS:
case RPC.CALL_GET_FULL_ACCOUNTS:
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
break;
default:
Log.w(TAG,"Case not handled");
mResponseView.setText(mResponseView.getText() + response.result.toString());
}
// Remember to remove the used id entry from the map, as it would
// otherwise just increase the app's memory usage
responseMap.remove(id);
}else{
Log.d(TAG,"No entry");
mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n");
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if(!mDisposable.isDisposed())
mDisposable.dispose();
}
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
// Called upon NetworkService connection
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
// Called upon NetworkService disconnection
}
}

View File

@ -0,0 +1,41 @@
package cy.agorise.labs.sample;
import android.app.Application;
import android.preference.PreferenceManager;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.api.android.NetworkService;
import cy.agorise.graphenej.api.android.NetworkServiceManager;
/**
* Sample application class
*/
public class SampleApplication extends Application {
private final String TAG = this.getClass().getName();
@Override
public void onCreate() {
super.onCreate();
// This variable would hold a list of custom nodes
String customNodes = "wss://mydomain.net/ws,wss://myotherdomain.com/ws";
// Specifying some important information regarding the connection, such as the
// credentials and the requested API accesses
int requestedApis = ApiAccess.API_DATABASE | ApiAccess.API_HISTORY | ApiAccess.API_NETWORK_BROADCAST;
PreferenceManager.getDefaultSharedPreferences(this)
.edit()
.putString(NetworkService.KEY_USERNAME, "nelson")
.putString(NetworkService.KEY_PASSWORD, "secret")
.putInt(NetworkService.KEY_REQUESTED_APIS, requestedApis)
// .putString(NetworkService.KEY_CUSTOM_NODE_URLS, customNodes)
.apply();
/*
* Registering this class as a listener to all activity's callback cycle events, in order to
* better estimate when the user has left the app and it is safe to disconnect the websocket connection
*/
registerActivityLifecycleCallbacks(new NetworkServiceManager(this));
}
}

View File

@ -0,0 +1,113 @@
package cy.agorise.labs.sample;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import cy.agorise.graphenej.api.android.NetworkService;
import cy.agorise.graphenej.api.android.RxBus;
import cy.agorise.graphenej.api.calls.CancelAllSubscriptions;
import cy.agorise.graphenej.api.calls.SetSubscribeCallback;
import cy.agorise.graphenej.models.JsonRpcNotification;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
public class SubscriptionActivity extends AppCompatActivity {
private final String TAG = this.getClass().getName();
@BindView(R.id.text_field)
TextView mTextField;
// In case we want to interact directly with the service
private NetworkService mService;
private Disposable mDisposable;
// Notification counter
private int counter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
ButterKnife.bind(this);
mDisposable = RxBus.getBusInstance()
.asFlowable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object message) throws Exception {
if(message instanceof String){
Log.d(TAG,"Got text message: "+(message));
mTextField.setText(mTextField.getText() + ((String) message) + "\n");
}else if(message instanceof JsonRpcNotification){
counter++;
mTextField.setText(String.format("Got %d notifications so far", counter));
}
}
});
}
@Override
protected void onStart() {
super.onStart();
// Bind to LocalService
Intent intent = new Intent(this, NetworkService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onPause() {
super.onPause();
unbindService(mConnection);
}
@Override
protected void onDestroy() {
super.onDestroy();
mDisposable.dispose();
}
/** Defines callbacks for backend binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className,
IBinder service) {
Log.d(TAG,"onServiceConnected");
// We've bound to LocalService, cast the IBinder and get LocalService instance
NetworkService.LocalBinder binder = (NetworkService.LocalBinder) service;
mService = binder.getService();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
Log.d(TAG,"onServiceDisconnected");
}
};
@OnClick(R.id.subscribe)
public void onTransferFeeUsdClicked(View v){
mService.sendMessage(new SetSubscribeCallback(true), SetSubscribeCallback.REQUIRED_API);
}
@OnClick(R.id.unsubscribe)
public void onTransferFeeBtsClicked(View v){
mService.sendMessage(new CancelAllSubscriptions(), CancelAllSubscriptions.REQUIRED_API);
}
}

View File

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/call_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CallsActivity">
</android.support.v7.widget.RecyclerView>

View File

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PerformCallActivity">
<ScrollView
android:id="@+id/output_text_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toTopOf="@+id/container_param1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/response"
tools:text="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
android:layout_width="match_parent"
android:layout_height="match_parent" />
</ScrollView>
<android.support.design.widget.TextInputLayout
android:id="@+id/container_param1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toTopOf="@+id/container_param2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/output_text_container">
<android.support.design.widget.TextInputEditText
android:id="@+id/param1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/container_param2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toTopOf="@+id/container_param3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/container_param1">
<android.support.design.widget.TextInputEditText
android:id="@+id/param2"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/container_param3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toTopOf="@+id/container_param4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/container_param2">
<android.support.design.widget.TextInputEditText
android:id="@+id/param3"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/container_param4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toTopOf="@+id/button_send"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/container_param3">
<android.support.design.widget.TextInputEditText
android:id="@+id/param4"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.TextInputLayout>
<Button
android:id="@+id/button_send"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:text="@string/action_send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/container_param4" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cy.agorise.labs.sample.SubscriptionActivity">
<TextView
android:id="@+id/text_field"
android:layout_width="0dp"
android:layout_height="0dp"
android:lines="20"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toTopOf="@+id/buttons_container"
app:layout_constraintTop_toTopOf="parent"/>
<LinearLayout
android:id="@+id/buttons_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:weightSum="2"
app:layout_constraintBottom_toBottomOf="parent">
<Button
android:id="@+id/subscribe"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="11sp"
android:text="Subscribe"/>
<Button
android:id="@+id/unsubscribe"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="11sp"
android:text="Unsubscribe"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:gravity="center"
android:textSize="18sp"
android:textStyle="bold"
android:padding="16dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
tools:text="Sample">
</TextView>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View File

@ -0,0 +1,57 @@
<resources>
<string name="app_name">Sample</string>
<string name="error_input_id">The entered value doesn\'t seem to be an object id</string>
<string name="error_number_format">Illegal number format</string>
<!-- Actions, buttons, etc -->
<string name="action_send">Send</string>
<!-- GetObjects input field -->
<string name="get_objects_arg1">Object id</string>
<!-- GetAccounts input field -->
<string name="get_accounts_arg1">Account id</string>
<!-- GetBlock & GetBlockHeader input field -->
<string name="get_block_arg1">Block id</string>
<!-- GetMarketHistory input fields -->
<string name="get_market_history_arg1">Base asset</string>
<string name="get_market_history_arg2">Quote asset</string>
<string name="get_market_history_arg3">Start timestamp (Latest)</string>
<string name="get_market_history_arg4">End timestamp (Earliest)</string>
<!-- GetRelativeAccountHistory input fields -->
<string name="get_relative_account_history_arg1">User account</string>
<string name="get_relative_account_history_arg2">Stop timestamp (Latest)</string>
<string name="get_relative_account_history_arg3">Limit</string>
<string name="get_relative_account_history_arg4">Start timestamp (Earliest)</string>
<!-- LookupAssetSymbols input fields -->
<string name="lookup_asset_symbols_arg1">Asset 1 id</string>
<string name="lookup_asset_symbols_arg2">Asset 2 id</string>
<string name="lookup_asset_symbols_arg3">Asset 3 id</string>
<string name="lookup_asset_symbols_arg4">Asset 4 id</string>
<!-- List assets input fields -->
<string name="list_assets_arg1">Lower bound of symbol names to retrieve</string>
<string name="list_assets_arg2">Maximum number of assets to fetch (must not exceed 100)</string>
<!-- 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>
<string name="get_limit_orders_arg3">Number of orders</string>
<!-- GetFullAccounts input fields -->
<string name="get_full_accounts_arg1">Account names or ids, separated by commas</string>
</resources>

View File

@ -0,0 +1,11 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View File

@ -0,0 +1,17 @@
package cy.sample.labs.sample;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View File

@ -1,3 +1,4 @@
include ':sample'
rootProject.name = "Graphenej"
include ":graphenej"