diff --git a/.gitignore b/.gitignore index 4c2a140..729bc70 100644 --- a/.gitignore +++ b/.gitignore @@ -104,4 +104,3 @@ graphenej/build local.properties -sample diff --git a/build.gradle b/build.gradle index 676dacc..5740673 100644 --- a/build.gradle +++ b/build.gradle @@ -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' } } \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/graphenej/build.gradle b/graphenej/build.gradle index 8d86c09..ee89cb7 100644 --- a/graphenej/build.gradle +++ b/graphenej/build.gradle @@ -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' } \ No newline at end of file diff --git a/graphenej/src/main/AndroidManifest.xml b/graphenej/src/main/AndroidManifest.xml index 7e71960..1531629 100644 --- a/graphenej/src/main/AndroidManifest.xml +++ b/graphenej/src/main/AndroidManifest.xml @@ -1,6 +1,14 @@ + package="cy.agorise.graphenej"> + - + + + + + \ No newline at end of file diff --git a/graphenej/src/main/java/cy/agorise/graphenej/AuthorityType.java b/graphenej/src/main/java/cy/agorise/graphenej/AuthorityType.java index 2fb52b8..14f6f97 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/AuthorityType.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/AuthorityType.java @@ -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()); + } } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/BaseOperation.java b/graphenej/src/main/java/cy/agorise/graphenej/BaseOperation.java index bafbea9..3d5666c 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/BaseOperation.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/BaseOperation.java @@ -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; } + + /** + *

+ * De-serializer used to unpack data from a generic operation. The general format used in the + * JSON-RPC blockchain API is the following: + *

+ * + * [OPERATION_ID, OPERATION_OBJECT]
+ * + *

+ * Where OPERATION_ID is one of the operations defined in {@link cy.agorise.graphenej.OperationType} + * and OPERATION_OBJECT is the actual operation serialized in the JSON format. + *

+ * Here's an example of this serialized form for a transfer operation:

+ *
+     *[
+     *   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": []
+     *   }
+     *]
+     *

+ * If this class is used, this serialized data will be translated to a TransferOperation object instance.
+ * + * TODO: Add support for operations other than the 'transfer' + */ + public static class OperationDeserializer implements JsonDeserializer { + + @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; + } + } } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/BrainKey.java b/graphenej/src/main/java/cy/agorise/graphenej/BrainKey.java index e54e3b7..c1808f7 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/BrainKey.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/BrainKey.java @@ -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(); + } } \ No newline at end of file diff --git a/graphenej/src/main/java/cy/agorise/graphenej/Extensions.java b/graphenej/src/main/java/cy/agorise/graphenej/Extensions.java index ada71a3..9e6bc66 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/Extensions.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/Extensions.java @@ -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 { + @Override + public Extensions deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return null; + } + } } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/objects/Memo.java b/graphenej/src/main/java/cy/agorise/graphenej/Memo.java similarity index 96% rename from graphenej/src/main/java/cy/agorise/graphenej/objects/Memo.java rename to graphenej/src/main/java/cy/agorise/graphenej/Memo.java index 2f77218..e0c9f03 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/objects/Memo.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/Memo.java @@ -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()); } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java b/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java index f383a08..62aa4f4 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java @@ -132,4 +132,8 @@ public class OrderBook { } return obtainedBase; } + + public List getLimitOrders(){ + return limitOrders; + } } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/PublicKey.java b/graphenej/src/main/java/cy/agorise/graphenej/PublicKey.java index f20f648..4dc5c05 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/PublicKey.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/PublicKey.java @@ -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(); + } } \ No newline at end of file diff --git a/graphenej/src/main/java/cy/agorise/graphenej/RPC.java b/graphenej/src/main/java/cy/agorise/graphenej/RPC.java index ea6c509..028130c 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/RPC.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/RPC.java @@ -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"; diff --git a/graphenej/src/main/java/cy/agorise/graphenej/UserAccount.java b/graphenej/src/main/java/cy/agorise/graphenej/UserAccount.java index dee9197..addee7a 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/UserAccount.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/UserAccount.java @@ -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()); } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/ApiAccess.java b/graphenej/src/main/java/cy/agorise/graphenej/api/ApiAccess.java new file mode 100644 index 0000000..503293f --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/ApiAccess.java @@ -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; +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/ConnectionStatusUpdate.java b/graphenej/src/main/java/cy/agorise/graphenej/api/ConnectionStatusUpdate.java new file mode 100644 index 0000000..15c5a82 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/ConnectionStatusUpdate.java @@ -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; + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/GetObjects.java b/graphenej/src/main/java/cy/agorise/graphenej/api/GetObjects.java index 91d83be..e9c0c4f 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/api/GetObjects.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/GetObjects.java @@ -75,11 +75,9 @@ public class GetObjects extends BaseGrapheneHandler { public void onConnected(WebSocket websocket, Map> headers) throws Exception { ArrayList params = new ArrayList<>(); ArrayList 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()); } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/GetRelativeAccountHistory.java b/graphenej/src/main/java/cy/agorise/graphenej/api/GetRelativeAccountHistory.java index 935618f..6a8d9d7 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/api/GetRelativeAccountHistory.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/GetRelativeAccountHistory.java @@ -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>>(){}.getType(); + Type RelativeAccountHistoryResponse = new TypeToken>>(){}.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> transfersResponse = gsonBuilder.create().fromJson(response, RelativeAccountHistoryResponse); + WitnessResponse> transfersResponse = gsonBuilder.create().fromJson(response, RelativeAccountHistoryResponse); mListener.onSuccess(transfersResponse); } } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/SubscriptionMessagesHub.java b/graphenej/src/main/java/cy/agorise/graphenej/api/SubscriptionMessagesHub.java index b6e3b43..1e234d2 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/api/SubscriptionMessagesHub.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/SubscriptionMessagesHub.java @@ -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 mHandlerMap = new HashMap<>(); private List 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{ diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/android/DeserializationMap.java b/graphenej/src/main/java/cy/agorise/graphenej/api/android/DeserializationMap.java new file mode 100644 index 0000000..449bf22 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/DeserializationMap.java @@ -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 mClassMap = new HashMap<>(); + + private HashMap 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; + } + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java new file mode 100644 index 0000000..eb9c12e --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java @@ -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 mApiIds = new HashMap(); + + private ArrayList 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 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 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>() {}.getType(); + JsonRpcResponse 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>() {}.getType(); + JsonRpcResponse 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>() {}.getType(); + JsonRpcResponse 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>() {}.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>(){}.getType(); + parsedResponse = gson.fromJson(text, GetBlockHeaderResponse); + } else if(responsePayloadClass == AccountProperties.class){ + Type GetAccountByNameResponse = new TypeToken>(){}.getType(); + parsedResponse = gson.fromJson(text, GetAccountByNameResponse); + } else if(responsePayloadClass == HistoryOperationDetail.class){ + Type GetAccountHistoryByOperationsResponse = new TypeToken>(){}.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> + // so we proceed with that + Type GetAccountsResponse = new TypeToken>>(){}.getType(); + parsedResponse = gson.fromJson(text, GetAccountsResponse); + }else if(requestClass == GetRequiredFees.class){ + Type GetRequiredFeesResponse = new TypeToken>>(){}.getType(); + parsedResponse = gson.fromJson(text, GetRequiredFeesResponse); + }else if(requestClass == GetRelativeAccountHistory.class){ + Type RelativeAccountHistoryResponse = new TypeToken>>(){}.getType(); + parsedResponse = gson.fromJson(text, RelativeAccountHistoryResponse); + }else if(requestClass == GetMarketHistory.class){ + Type GetMarketHistoryResponse = new TypeToken>>(){}.getType(); + parsedResponse = gson.fromJson(text, GetMarketHistoryResponse); + }else if(requestClass == GetObjects.class){ + parsedResponse = handleGetObject(text); + }else if(requestClass == ListAssets.class){ + Type LisAssetsResponse = new TypeToken>>(){}.getType(); + parsedResponse = gson.fromJson(text, LisAssetsResponse); + }else if(requestClass == GetLimitOrders.class){ + Type GetLimitOrdersResponse = new TypeToken>>() {}.getType(); + parsedResponse = gson.fromJson(text, GetLimitOrdersResponse); + } else if (requestClass == GetFullAccounts.class) { + Type GetFullAccountsResponse = new TypeToken>>(){}.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 getNodeUrls() { + return mNodeUrls; + } + + public void setNodeUrls(ArrayList mNodeUrls) { + this.mNodeUrls = mNodeUrls; + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java new file mode 100644 index 0000000..2f31c0d --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java @@ -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 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); + } + + @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) {} + }; +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/android/RxBus.java b/graphenej/src/main/java/cy/agorise/graphenej/api/android/RxBus.java new file mode 100644 index 0000000..f246f2d --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/RxBus.java @@ -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 _bus = PublishRelay.create().toSerialized(); + + public void send(Object o) { + _bus.accept(o); + } + + public Flowable asFlowable() { + return _bus.toFlowable(BackpressureStrategy.LATEST); + } + + public boolean hasObservers() { + return _bus.hasObservers(); + } +} \ No newline at end of file diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/bitshares/Nodes.java b/graphenej/src/main/java/cy/agorise/graphenej/api/bitshares/Nodes.java new file mode 100644 index 0000000..46e61dc --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/bitshares/Nodes.java @@ -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 + }; +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/ApiCallable.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/ApiCallable.java new file mode 100644 index 0000000..32ebecb --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/ApiCallable.java @@ -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); +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/CancelAllSubscriptions.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/CancelAllSubscriptions.java new file mode 100644 index 0000000..9ebc831 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/CancelAllSubscriptions.java @@ -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(), RPC.VERSION, sequenceId); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountByName.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountByName.java new file mode 100644 index 0000000..3aeff26 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountByName.java @@ -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 accountParams = new ArrayList<>(); + accountParams.add(this.accountName); + return new ApiCall(apiId, RPC.CALL_GET_ACCOUNT_BY_NAME, accountParams, RPC.VERSION, sequenceId); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountHistory.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountHistory.java new file mode 100644 index 0000000..0a43ef4 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountHistory.java @@ -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 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountHistoryByOperations.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountHistoryByOperations.java new file mode 100644 index 0000000..4a94783 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountHistoryByOperations.java @@ -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 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 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 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 params = new ArrayList<>(); + if(mUserAccount.getName() != null){ + params.add(mUserAccount.getName()); + }else{ + params.add(mUserAccount.getObjectId()); + } + ArrayList 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccounts.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccounts.java new file mode 100644 index 0000000..e2c036a --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccounts.java @@ -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 mUserAccounts; + + public GetAccounts(List accountList){ + mUserAccounts = accountList; + } + + public GetAccounts(UserAccount userAccount){ + mUserAccounts = new ArrayList<>(); + mUserAccounts.add(userAccount); + } + + @Override + public ApiCall toApiCall(int apiId, long sequenceId) { + ArrayList params = new ArrayList<>(); + ArrayList 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetBlock.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetBlock.java new file mode 100644 index 0000000..beb6c05 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetBlock.java @@ -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 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetBlockHeader.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetBlockHeader.java new file mode 100644 index 0000000..5c8045a --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetBlockHeader.java @@ -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 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetFullAccounts.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetFullAccounts.java new file mode 100644 index 0000000..ac7a704 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetFullAccounts.java @@ -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 mUserAccounts; + private boolean mSubscribe; + + public GetFullAccounts(List accounts, boolean subscribe){ + this.mUserAccounts = accounts; + this.mSubscribe = subscribe; + } + + @Override + public ApiCall toApiCall(int apiId, long sequenceId) { + ArrayList params = new ArrayList<>(); + ArrayList accounts = new ArrayList(mUserAccounts); + params.add(accounts); + params.add(mSubscribe); + return new ApiCall(apiId, RPC.CALL_GET_FULL_ACCOUNTS, params, RPC.VERSION, sequenceId); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetLimitOrders.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetLimitOrders.java new file mode 100644 index 0000000..9061b8c --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetLimitOrders.java @@ -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 get_limit_orders API doc + * + */ +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 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetMarketHistory.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetMarketHistory.java new file mode 100644 index 0000000..d9746e9 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetMarketHistory.java @@ -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 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetObjects.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetObjects.java new file mode 100644 index 0000000..d6661d3 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetObjects.java @@ -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 ids; + + public GetObjects(List ids){ + this.ids = ids; + } + + @Override + public ApiCall toApiCall(int apiId, long sequenceId) { + ArrayList params = new ArrayList<>(); + ArrayList subParams = new ArrayList<>(ids); + params.add(subParams); + return new ApiCall(apiId, RPC.CALL_GET_OBJECTS, params, RPC.VERSION, sequenceId); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetRelativeAccountHistory.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetRelativeAccountHistory.java new file mode 100644 index 0000000..362d9c1 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetRelativeAccountHistory.java @@ -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 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetRequiredFees.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetRequiredFees.java new file mode 100644 index 0000000..773a208 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetRequiredFees.java @@ -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 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 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/ListAssets.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/ListAssets.java new file mode 100644 index 0000000..f7b7688 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/ListAssets.java @@ -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 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/LookupAssetSymbols.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/LookupAssetSymbols.java new file mode 100644 index 0000000..3114ff0 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/LookupAssetSymbols.java @@ -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 mAssetList; + + public LookupAssetSymbols(List assetList){ + this.mAssetList = assetList; + } + + public LookupAssetSymbols(Asset asset){ + mAssetList = new ArrayList(); + mAssetList.add(asset); + } + + @Override + public ApiCall toApiCall(int apiId, long sequenceId) { + ArrayList params = new ArrayList<>(); + ArrayList 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); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/calls/SetSubscribeCallback.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/SetSubscribeCallback.java new file mode 100644 index 0000000..d0797a2 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/SetSubscribeCallback.java @@ -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 subscriptionParams = new ArrayList<>(); + subscriptionParams.add(new Long(sequenceId)); + subscriptionParams.add(clearFilter); + return new ApiCall(apiId, RPC.CALL_SET_SUBSCRIBE_CALLBACK, subscriptionParams, RPC.VERSION, sequenceId); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/ApiCall.java b/graphenej/src/main/java/cy/agorise/graphenej/models/ApiCall.java index 58707ae..78f49a2 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/models/ApiCall.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/ApiCall.java @@ -19,6 +19,7 @@ import cy.agorise.graphenej.interfaces.JsonSerializable; * @see Websocket Calls & Notifications */ 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 listArgument = (ArrayList) 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 listArgument = (ArrayList) 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); diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/BaseResponse.java b/graphenej/src/main/java/cy/agorise/graphenej/models/BaseResponse.java index 50b7622..850af33 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/models/BaseResponse.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/BaseResponse.java @@ -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; diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/BlockHeader.java b/graphenej/src/main/java/cy/agorise/graphenej/models/BlockHeader.java index 9ebdef7..503f074 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/models/BlockHeader.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/BlockHeader.java @@ -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; } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/FullAccountDetails.java b/graphenej/src/main/java/cy/agorise/graphenej/models/FullAccountDetails.java new file mode 100644 index 0000000..55ed6b6 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/FullAccountDetails.java @@ -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 { + + @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); + } + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/HistoricalTransfer.java b/graphenej/src/main/java/cy/agorise/graphenej/models/HistoricalTransfer.java deleted file mode 100644 index d14e5b8..0000000 --- a/graphenej/src/main/java/cy/agorise/graphenej/models/HistoricalTransfer.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/HistoryOperationDetail.java b/graphenej/src/main/java/cy/agorise/graphenej/models/HistoryOperationDetail.java new file mode 100644 index 0000000..096df22 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/HistoryOperationDetail.java @@ -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 operation_history_objs; + + public long getTotalCount() { + return total_count; + } + + public void setTotalCount(long total_count) { + this.total_count = total_count; + } + + public List getOperationHistoryObjs() { + return operation_history_objs; + } + + public void setOperationHistoryObjs(List operation_history_objs) { + this.operation_history_objs = operation_history_objs; + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/JsonRpcNotification.java b/graphenej/src/main/java/cy/agorise/graphenej/models/JsonRpcNotification.java new file mode 100644 index 0000000..dfb5632 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/JsonRpcNotification.java @@ -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 params; + + /** + * Inner static class used to parse and deserialize subscription notifications. + */ + public static class JsonRpcNotificationDeserializer implements JsonDeserializer { + + @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 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; + } + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/JsonRpcResponse.java b/graphenej/src/main/java/cy/agorise/graphenej/models/JsonRpcResponse.java new file mode 100644 index 0000000..00d216b --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/JsonRpcResponse.java @@ -0,0 +1,31 @@ +package cy.agorise.graphenej.models; + +/** + * Used to represent a JSON-RPC response object + */ + +public class JsonRpcResponse { + 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; + } + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/OperationHistory.java b/graphenej/src/main/java/cy/agorise/graphenej/models/OperationHistory.java new file mode 100644 index 0000000..09f5561 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/OperationHistory.java @@ -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 { + + @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; + } + } +} \ No newline at end of file diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/SubscriptionResponse.java b/graphenej/src/main/java/cy/agorise/graphenej/models/SubscriptionResponse.java index 8edbfcd..700b36e 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/models/SubscriptionResponse.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/SubscriptionResponse.java @@ -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 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 } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/WitnessResponse.java b/graphenej/src/main/java/cy/agorise/graphenej/models/WitnessResponse.java index 2cabae9..e6f3ffc 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/models/WitnessResponse.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/WitnessResponse.java @@ -2,6 +2,7 @@ package cy.agorise.graphenej.models; /** * Generic witness response + * @deprecated Use {@link JsonRpcResponse} instead */ public class WitnessResponse extends BaseResponse{ public static final String KEY_ID = "id"; diff --git a/graphenej/src/main/java/cy/agorise/graphenej/operations/TransferOperation.java b/graphenej/src/main/java/cy/agorise/graphenej/operations/TransferOperation.java index 232a2a1..63b28c4 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/operations/TransferOperation.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/operations/TransferOperation.java @@ -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. diff --git a/graphenej/src/main/java/cy/agorise/graphenej/operations/TransferOperationBuilder.java b/graphenej/src/main/java/cy/agorise/graphenej/operations/TransferOperationBuilder.java index 709589b..c1516cf 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/operations/TransferOperationBuilder.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/operations/TransferOperationBuilder.java @@ -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 diff --git a/graphenej/src/test/java/cy/agorise/graphenej/BrainKeyTest.java b/graphenej/src/test/java/cy/agorise/graphenej/BrainKeyTest.java index c0a1342..a5915af 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/BrainKeyTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/BrainKeyTest.java @@ -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()); + } } \ No newline at end of file diff --git a/graphenej/src/test/java/cy/agorise/graphenej/TransactionTest.java b/graphenej/src/test/java/cy/agorise/graphenej/TransactionTest.java index d4dd064..92719f2 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/TransactionTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/TransactionTest.java @@ -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; diff --git a/graphenej/src/test/java/cy/agorise/graphenej/api/GetAccountsTest.java b/graphenej/src/test/java/cy/agorise/graphenej/api/GetAccountsTest.java new file mode 100644 index 0000000..ef836a1 --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/api/GetAccountsTest.java @@ -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 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 accounts = (List) 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()); + } + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/api/GetObjectsTest.java b/graphenej/src/test/java/cy/agorise/graphenej/api/GetObjectsTest.java index 439c9d6..cd7bab8 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/api/GetObjectsTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/api/GetObjectsTest.java @@ -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 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 result = (List) 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{ diff --git a/graphenej/src/test/java/cy/agorise/graphenej/api/GetRelativeAccountHistoryTest.java b/graphenej/src/test/java/cy/agorise/graphenej/api/GetRelativeAccountHistoryTest.java index 220a523..8b2a4e5 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/api/GetRelativeAccountHistoryTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/api/GetRelativeAccountHistoryTest.java @@ -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> resp = response; - for(HistoricalTransfer historicalTransfer : resp.result){ + WitnessResponse> 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(), diff --git a/graphenej/src/test/java/cy/agorise/graphenej/api/SubscriptionMessagesHubTest.java b/graphenej/src/test/java/cy/agorise/graphenej/api/SubscriptionMessagesHubTest.java index b6f8fda..be510be 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/api/SubscriptionMessagesHubTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/api/SubscriptionMessagesHubTest.java @@ -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 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: "); + } + } + } + } + } + }); + mWebSocket.addListener(mMessagesHub); mWebSocket.connect(); diff --git a/graphenej/src/test/java/cy/agorise/graphenej/api/calls/GetAccountHistoryTest.java b/graphenej/src/test/java/cy/agorise/graphenej/api/calls/GetAccountHistoryTest.java new file mode 100644 index 0000000..f14b7f6 --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/api/calls/GetAccountHistoryTest.java @@ -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); + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/models/FullAccountDetailsTest.java b/graphenej/src/test/java/cy/agorise/graphenej/models/FullAccountDetailsTest.java new file mode 100644 index 0000000..33d49f8 --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/models/FullAccountDetailsTest.java @@ -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>>() {}.getType(); + JsonRpcResponse> response = gson.fromJson(serialized, FullAccountDetailsResponse); + Assert.assertNotNull(response.result); + Assert.assertNull(response.error); + List 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); + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/models/HistoryOperationDetailsTest.java b/graphenej/src/test/java/cy/agorise/graphenej/models/HistoryOperationDetailsTest.java new file mode 100644 index 0000000..2b1a492 --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/models/HistoryOperationDetailsTest.java @@ -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>(){}.getType(); + JsonRpcResponse response = gson.fromJson(text, GetAccountHistoryByOperationsResponse); + Assert.assertNotNull(response.result); + Assert.assertNotNull(response.result.operation_history_objs); + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/models/JsonRpcNotificationTest.java b/graphenej/src/test/java/cy/agorise/graphenej/models/JsonRpcNotificationTest.java new file mode 100644 index 0000000..2dcbf9a --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/models/JsonRpcNotificationTest.java @@ -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 secondArgument = (ArrayList) 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); + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/models/JsonRpcResponseTest.java b/graphenej/src/test/java/cy/agorise/graphenej/models/JsonRpcResponseTest.java new file mode 100644 index 0000000..335fd89 --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/models/JsonRpcResponseTest.java @@ -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); + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/objects/MemoTest.java b/graphenej/src/test/java/cy/agorise/graphenej/objects/MemoTest.java index 303b140..a614225 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/objects/MemoTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/objects/MemoTest.java @@ -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; diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..de76e02 --- /dev/null +++ b/sample/build.gradle @@ -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' +} diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/sample/proguard-rules.pro @@ -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 diff --git a/sample/src/androidTest/java/cy/sample/labs/sample/ExampleInstrumentedTest.java b/sample/src/androidTest/java/cy/sample/labs/sample/ExampleInstrumentedTest.java new file mode 100644 index 0000000..9b859ad --- /dev/null +++ b/sample/src/androidTest/java/cy/sample/labs/sample/ExampleInstrumentedTest.java @@ -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 Testing documentation + */ +@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()); + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..de61cce --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java b/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java new file mode 100644 index 0000000..4db3d3e --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java @@ -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 { + + 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; + } + } + } +} diff --git a/sample/src/main/java/cy/agorise/labs/sample/ConnectedActivity.java b/sample/src/main/java/cy/agorise/labs/sample/ConnectedActivity.java new file mode 100644 index 0000000..b9e6389 --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/ConnectedActivity.java @@ -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; + } + } +} diff --git a/sample/src/main/java/cy/agorise/labs/sample/Constants.java b/sample/src/main/java/cy/agorise/labs/sample/Constants.java new file mode 100644 index 0000000..a77ed93 --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/Constants.java @@ -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"; +} diff --git a/sample/src/main/java/cy/agorise/labs/sample/PerformCallActivity.java b/sample/src/main/java/cy/agorise/labs/sample/PerformCallActivity.java new file mode 100644 index 0000000..08254ef --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/PerformCallActivity.java @@ -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 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() { + + @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 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 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 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 + } +} diff --git a/sample/src/main/java/cy/agorise/labs/sample/SampleApplication.java b/sample/src/main/java/cy/agorise/labs/sample/SampleApplication.java new file mode 100644 index 0000000..ba6a0d1 --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/SampleApplication.java @@ -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)); + } +} diff --git a/sample/src/main/java/cy/agorise/labs/sample/SubscriptionActivity.java b/sample/src/main/java/cy/agorise/labs/sample/SubscriptionActivity.java new file mode 100644 index 0000000..a6b7daa --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/SubscriptionActivity.java @@ -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() { + + @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); + } +} diff --git a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/sample/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/sample/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/res/layout/activity_calls.xml b/sample/src/main/res/layout/activity_calls.xml new file mode 100644 index 0000000..e2f87a0 --- /dev/null +++ b/sample/src/main/res/layout/activity_calls.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_perform_call.xml b/sample/src/main/res/layout/activity_perform_call.xml new file mode 100644 index 0000000..1a44ab0 --- /dev/null +++ b/sample/src/main/res/layout/activity_perform_call.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + +