diff --git a/.gitignore b/.gitignore index 0790958..729bc70 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,4 @@ release.properties graphenej/build local.properties + diff --git a/build.gradle b/build.gradle index 676dacc..77fec39 100644 --- a/build.gradle +++ b/build.gradle @@ -3,11 +3,27 @@ subprojects { mavenCentral() } } +allprojects { + repositories { + mavenCentral() + jcenter() + maven { + url "https://maven.google.com" + } + } +} buildscript { repositories { mavenCentral() + maven { + url 'https://maven.google.com/' + name 'Google' + } + google() + jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.novoda:bintray-release:0.9.1' } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 5bfcab3..59500a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,8 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=0.4.6 -VERSION_CODE=9 +VERSION_NAME=0.4.7-alpha3 +VERSION_CODE=12 GROUP=com.github.bilthon POM_DESCRIPTION=A Java library for mobile app Developers; Graphene/Bitshares blockchain. diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/graphenej/build.gradle b/graphenej/build.gradle index 0d30268..22d2d47 100644 --- a/graphenej/build.gradle +++ b/graphenej/build.gradle @@ -1,34 +1,54 @@ -group 'cy.agorise' -version '0.4.6' - +apply plugin: 'com.novoda.bintray-release' 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" +publish { + userOrg = 'bilthon' + groupId = 'cy.agorise.graphenej' + artifactId = 'graphenej' + publishVersion = '0.6.0' + repoName = 'Graphenej' + desc = 'A Java library for mobile app Developers; Graphene/Bitshares blockchain.' + website = 'https://github.com/Agorise/graphenej' } - android { - compileSdkVersion 24 - buildToolsVersion "25.0.0" + compileSdkVersion 28 defaultConfig { - minSdkVersion 9 - targetSdkVersion 24 - versionCode 9 - versionName "0.4.6" + minSdkVersion 14 + targetSdkVersion 28 + versionCode 12 + versionName "0.6.0" 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' } } + defaultConfig { + multiDexEnabled true + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } +} + +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 'com.google.code.gson:gson:2.8.5' + implementation 'org.tukaani:xz:1.6' + + androidTestImplementation 'com.android.support:support-annotations:28.0.0' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test:rules:1.0.2' + + // Rx dependencies + api 'io.reactivex.rxjava2:rxandroid:2.1.0' + api 'io.reactivex.rxjava2:rxjava:2.2.2' + api 'com.jakewharton.rxrelay2:rxrelay:2.1.0' + api 'com.squareup.okhttp3:okhttp:3.12.2' } \ No newline at end of file diff --git a/graphenej/src/androidTest/java/cy/agorise/graphenej/NodeLatencyVerifierTest.java b/graphenej/src/androidTest/java/cy/agorise/graphenej/NodeLatencyVerifierTest.java new file mode 100644 index 0000000..f8542b0 --- /dev/null +++ b/graphenej/src/androidTest/java/cy/agorise/graphenej/NodeLatencyVerifierTest.java @@ -0,0 +1,90 @@ +package cy.agorise.graphenej; + +import android.support.test.runner.AndroidJUnit4; +import android.util.Log; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import cy.agorise.graphenej.network.FullNode; +import cy.agorise.graphenej.network.LatencyNodeProvider; +import cy.agorise.graphenej.network.NodeLatencyVerifier; +import cy.agorise.graphenej.network.NodeProvider; +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; +import io.reactivex.subjects.PublishSubject; + +@RunWith(AndroidJUnit4.class) +public class NodeLatencyVerifierTest { + private final String TAG = this.getClass().getName(); + + String[] nodeURLs = new String[]{ + "wss://bitshares.openledger.info/ws", + "wss://us.nodes.bitshares.ws", + "wss://eu.nodes.bitshares.ws", + "wss://citadel.li/node", + "wss://api.bts.mobi/ws" + }; + + @Test + public void testNodeLatencyTest() throws Exception { + ArrayList nodeList = new ArrayList<>(); + nodeList.add(new FullNode(nodeURLs[0])); + nodeList.add(new FullNode(nodeURLs[1])); + nodeList.add(new FullNode(nodeURLs[2])); + final NodeLatencyVerifier nodeLatencyVerifier = new NodeLatencyVerifier(nodeList); + PublishSubject subject = nodeLatencyVerifier.start(); + final NodeProvider nodeProvider = new LatencyNodeProvider(); + subject.subscribe(new Observer() { + int counter = 0; + + @Override + public void onSubscribe(Disposable d) {} + + @Override + public void onNext(FullNode fullNode) { + Log.i(TAG,String.format("Avg latency: %.2f, url: %s", fullNode.getLatencyValue(), fullNode.getUrl())); + + // Updating node provider + nodeProvider.updateNode(fullNode); + List sortedNodes = nodeProvider.getSortedNodes(); + for(FullNode node : sortedNodes){ + Log.d(TAG,String.format("> %.2f, url: %s", node.getLatencyValue(), node.getUrl())); + } + + // Finish test after certain amount of rounds + if(counter > 3){ + synchronized (NodeLatencyVerifierTest.this){ + nodeLatencyVerifier.stop(); + NodeLatencyVerifierTest.this.notifyAll(); + } + } + + counter++; + } + + @Override + public void onError(Throwable e) { + Log.e(TAG,"onError.Msg: "+e.getMessage()); + synchronized (NodeLatencyVerifierTest.this){ + NodeLatencyVerifierTest.this.notifyAll(); + } + } + + @Override + public void onComplete() { + Log.d(TAG,"onComplete"); + } + }); + try { + synchronized(this) { + wait(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/graphenej/src/main/AndroidManifest.xml b/graphenej/src/main/AndroidManifest.xml index 4df96c3..d333e17 100644 --- a/graphenej/src/main/AndroidManifest.xml +++ b/graphenej/src/main/AndroidManifest.xml @@ -1,8 +1,14 @@ - - + package="cy.agorise.graphenej"> + + + + + + + \ No newline at end of file diff --git a/graphenej/src/main/java/cy/agorise/graphenej/AccountOptions.java b/graphenej/src/main/java/cy/agorise/graphenej/AccountOptions.java index 3a58cab..f32f800 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/AccountOptions.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/AccountOptions.java @@ -33,17 +33,51 @@ public class AccountOptions implements GrapheneSerializable { private Vote[] votes; private Extensions extensions; + /** + * Constructor used to instantiate only the following attributes: + *
    + *
  • voting_account
  • + *
  • votes
  • + *
  • extensions
  • + *
+ */ public AccountOptions(){ voting_account = new UserAccount(UserAccount.PROXY_TO_SELF); this.votes = new Vote[0]; this.extensions = new Extensions(); } + /** + * Constructor used to instantiate only the following attributes: + *
    + *
  • voting_account
  • + *
  • votes
  • + *
  • memo_key
  • + *
  • extensions
  • + *
+ */ public AccountOptions(PublicKey memoKey){ this(); this.memo_key = memoKey; } + /** + * Constructor that can be used to instantiate a version of the AccountOptions object + * with a null reference in the 'voting_account' attribute. This can be used to prevent + * a circular dependency situation when de-serializing the UserAccount instance. + * + * @param memoKey Memo public key used by this account + * @param includeAccount Whether or not to instantiate an UserAccount + */ + public AccountOptions(PublicKey memoKey, boolean includeAccount){ + if(includeAccount){ + voting_account = new UserAccount(UserAccount.PROXY_TO_SELF); + } + this.memo_key = memoKey; + this.votes = new Vote[0]; + this.extensions = new Extensions(); + } + //TODO: Implement constructor that takes a Vote array. public PublicKey getMemoKey() { @@ -149,13 +183,23 @@ public class AccountOptions implements GrapheneSerializable { */ public static class AccountOptionsDeserializer implements JsonDeserializer { + boolean mIncludeUserAccount; + + public AccountOptionsDeserializer(){ + this.mIncludeUserAccount = true; + } + + public AccountOptionsDeserializer(boolean includeUserAccount){ + this.mIncludeUserAccount = includeUserAccount; + } + @Override public AccountOptions deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { JsonObject baseObject = json.getAsJsonObject(); AccountOptions options; try { Address address = new Address(baseObject.get(KEY_MEMO_KEY).getAsString()); - options = new AccountOptions(address.getPublicKey()); + options = new AccountOptions(address.getPublicKey(), mIncludeUserAccount); } catch (MalformedAddressException e) { System.out.println("MalformedAddressException. Msg: "+e.getMessage()); options = new AccountOptions(); diff --git a/graphenej/src/main/java/cy/agorise/graphenej/Address.java b/graphenej/src/main/java/cy/agorise/graphenej/Address.java index 0a60048..94340c0 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/Address.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/Address.java @@ -1,5 +1,6 @@ package cy.agorise.graphenej; +import com.google.common.base.Objects; import com.google.common.primitives.Bytes; import cy.agorise.graphenej.errors.MalformedAddressException; import org.bitcoinj.core.Base58; @@ -61,4 +62,18 @@ public class Address { ripemd160Digest.doFinal(checksum, 0); return Arrays.copyOfRange(checksum, 0, 4); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Address address = (Address) o; + return Objects.equal(publicKey, address.publicKey) && + Objects.equal(prefix, address.prefix); + } + + @Override + public int hashCode() { + return Objects.hashCode(publicKey, prefix); + } } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/AssetAmount.java b/graphenej/src/main/java/cy/agorise/graphenej/AssetAmount.java index 6ac3a7a..37d087a 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/AssetAmount.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/AssetAmount.java @@ -191,6 +191,11 @@ public class AssetAmount implements ByteSerializable, JsonSerializable { return jsonAmount; } + @Override + public String toString() { + return String.format("(asset=%s, amount=%s)", asset.getObjectId(), amount.toString(10)); + } + /** * Custom serializer used to translate this object into the JSON-formatted entry we need for a transaction. */ 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/GrapheneObject.java b/graphenej/src/main/java/cy/agorise/graphenej/GrapheneObject.java index 072355b..865df39 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/GrapheneObject.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/GrapheneObject.java @@ -2,6 +2,8 @@ package cy.agorise.graphenej; import com.google.gson.annotations.Expose; +import java.util.Locale; + /** *

* Generic class used to represent a graphene object as defined in @@ -34,15 +36,15 @@ public class GrapheneObject { /** * - * @return: A String containing the full object apiId in the form {space}.{type}.{instance} + * @return A String containing the full object apiId in the form {space}.{type}.{instance} */ public String getObjectId(){ - return String.format("%d.%d.%d", space, type, instance); + return String.format(Locale.US, "%d.%d.%d", space, type, instance); } /** * Returns the type of this object. - * @return: Instance of the ObjectType enum. + * @return Instance of the ObjectType enum. */ public ObjectType getObjectType(){ switch(space){ @@ -78,6 +80,8 @@ public class GrapheneObject { return ObjectType.WORKER_OBJECT; case 15: return ObjectType.BALANCE_OBJECT; + case 16: + return ObjectType.HTLC_OBJECT; } case IMPLEMENTATION_SPACE: switch(type){ diff --git a/graphenej/src/main/java/cy/agorise/graphenej/Htlc.java b/graphenej/src/main/java/cy/agorise/graphenej/Htlc.java new file mode 100644 index 0000000..6fc462d --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/Htlc.java @@ -0,0 +1,43 @@ +package cy.agorise.graphenej; + +import com.google.gson.JsonElement; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; + +import cy.agorise.graphenej.interfaces.ByteSerializable; +import cy.agorise.graphenej.interfaces.JsonSerializable; + +/** + * Class used to represent an existing HTLC contract. + */ +public class Htlc extends GrapheneObject implements ByteSerializable, JsonSerializable { + + public Htlc(String id) { + super(id); + } + + @Override + public byte[] toBytes() { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DataOutput out = new DataOutputStream(byteArrayOutputStream); + try { + Varint.writeUnsignedVarLong(this.instance, out); + } catch (IOException e) { + e.printStackTrace(); + } + return byteArrayOutputStream.toByteArray(); + } + + @Override + public String toJsonString() { + return this.getObjectId(); + } + + @Override + public JsonElement toJsonObject() { + return null; + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java new file mode 100644 index 0000000..aa0b6d1 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java @@ -0,0 +1,45 @@ +package cy.agorise.graphenej; + +import com.google.common.primitives.Bytes; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +import cy.agorise.graphenej.interfaces.ByteSerializable; +import cy.agorise.graphenej.interfaces.JsonSerializable; + +/** + * Class used to represent a HTLC hash. + */ +public class HtlcHash implements ByteSerializable, JsonSerializable { + private HtlcHashType hashType; + private byte[] hash; + + public HtlcHash(HtlcHashType hashType, byte[] hash) { + this.hashType = hashType; + this.hash = hash; + } + + public HtlcHashType getType(){ + return this.hashType; + } + + @Override + public byte[] toBytes() { + byte[] hashTypeBytes = new byte[] { Util.revertInteger(hashType.ordinal())[3] }; + return Bytes.concat(hashTypeBytes, hash); + } + + @Override + public String toJsonString() { + JsonElement element = toJsonObject(); + return element.toString(); + } + + @Override + public JsonElement toJsonObject() { + JsonArray array = new JsonArray(); + array.add(hashType.ordinal()); + array.add(Util.byteToString(hash)); + return array; + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/HtlcHashType.java b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHashType.java new file mode 100644 index 0000000..ba66fec --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHashType.java @@ -0,0 +1,11 @@ +package cy.agorise.graphenej; + +/** + * Used to enumerate the possible hash algorithms used in HTLCs. + * @see htlc.hpp + */ +public enum HtlcHashType { + RIPEMD160, + SHA1, + SHA256 +} 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 93% 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..dddd10d 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/objects/Memo.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/Memo.java @@ -1,15 +1,11 @@ -package cy.agorise.graphenej.objects; +package cy.agorise.graphenej; import com.google.common.primitives.Bytes; -import com.google.gson.Gson; -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 com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; - +import com.google.gson.*; +import cy.agorise.graphenej.errors.ChecksumException; +import cy.agorise.graphenej.errors.MalformedAddressException; +import cy.agorise.graphenej.interfaces.ByteSerializable; +import cy.agorise.graphenej.interfaces.JsonSerializable; import org.bitcoinj.core.ECKey; import org.spongycastle.math.ec.ECPoint; @@ -19,20 +15,11 @@ 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; -import cy.agorise.graphenej.interfaces.JsonSerializable; - /** * Class used to represent a memo data structure * {@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"; @@ -190,10 +177,6 @@ public class Memo implements ByteSerializable, JsonSerializable { byte[] seed = Bytes.concat(nonceBytes, Util.hexlify(Util.bytesToHex(ss))); - // Calculating checksum - byte[] sha256Msg = sha256.digest(message); - - // Applying decryption byte[] temp = Util.decryptAES(message, seed); byte[] checksum = Arrays.copyOfRange(temp, 0, 4); @@ -205,7 +188,7 @@ public class Memo implements ByteSerializable, JsonSerializable { throw new ChecksumException("Invalid checksum found while performing decryption"); } } catch (NoSuchAlgorithmException e) { - System.out.println("NoSuchAlgotithmException. Msg:"+ e.getMessage()); + System.out.println("NoSuchAlgorithmException. Msg:"+ e.getMessage()); } return plaintext; } @@ -291,13 +274,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)); + memoObject.addProperty(KEY_NONCE, this.nonce.toString()); + if(this.message != null) + memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message)); } return memoObject; } @@ -310,8 +295,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/ObjectType.java b/graphenej/src/main/java/cy/agorise/graphenej/ObjectType.java index ee1ce37..ef49a28 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/ObjectType.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/ObjectType.java @@ -20,6 +20,7 @@ public enum ObjectType { VESTING_BALANCE_OBJECT, WORKER_OBJECT, BALANCE_OBJECT, + HTLC_OBJECT, GLOBAL_PROPERTY_OBJECT, DYNAMIC_GLOBAL_PROPERTY_OBJECT, ASSET_DYNAMIC_DATA, @@ -53,6 +54,7 @@ public enum ObjectType { case VESTING_BALANCE_OBJECT: case WORKER_OBJECT: case BALANCE_OBJECT: + case HTLC_OBJECT: space = 1; break; case GLOBAL_PROPERTY_OBJECT: @@ -123,6 +125,8 @@ public enum ObjectType { case BALANCE_OBJECT: type = 15; break; + case HTLC_OBJECT: + type = 16; case GLOBAL_PROPERTY_OBJECT: type = 0; break; diff --git a/graphenej/src/main/java/cy/agorise/graphenej/OperationType.java b/graphenej/src/main/java/cy/agorise/graphenej/OperationType.java index 9ea093d..a4d4377 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/OperationType.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/OperationType.java @@ -51,5 +51,15 @@ public enum OperationType { BLIND_TRANSFER_OPERATION, TRANSFER_FROM_BLIND_OPERATION, ASSET_SETTLE_CANCEL_OPERATION, // VIRTUAL - ASSET_CLAIM_FEES_OPERATION + ASSET_CLAIM_FEES_OPERATION, + FBA_DISTRIBUTE_OPERATION, + BID_COLLATERAL_OPERATION, + EXECUTE_BID_OPERATION, // VIRTUAL + ASSET_CLAIM_POOL_OPERATION, + ASSET_UPDATE_ISSUER_OPERATION, + HTLC_CREATE_OPERATION, + HTLC_REDEEM_OPERATION, + HTLC_REDEEMED_OPERATION, // VIRTUAL + HTLC_EXTEND_OPERATION, + HTLC_REFUND_OPERATION // VIRTUAL } 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/Price.java b/graphenej/src/main/java/cy/agorise/graphenej/Price.java index f45c56c..f0ea53c 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/Price.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/Price.java @@ -18,4 +18,13 @@ package cy.agorise.graphenej; public class Price { public AssetAmount base; public AssetAmount quote; + + @Override + public String toString() { + return String.format("base:[%s, %s], quote:[%s, %s]", + base.getAsset().getObjectId(), + base.getAmount().toString(), + quote.getAsset().getObjectId(), + quote.getAmount().toString()); + } } 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..d4ffc06 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/RPC.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/RPC.java @@ -14,15 +14,19 @@ 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 GET_ACCOUNT_BALANCES = "get_account_balances"; + public static final String CALL_GET_ASSETS = "get_assets"; + public static final String CALL_GET_OBJECTS = "get_objects"; + public static final String CALL_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"; public static final String CALL_GET_BLOCK = "get_block"; @@ -30,4 +34,5 @@ public class RPC { public static final String CALL_GET_TRADE_HISTORY = "get_trade_history"; public static final String CALL_GET_MARKET_HISTORY = "get_market_history"; public static final String CALL_GET_ALL_ASSET_HOLDERS = "get_all_asset_holders"; + public static final String CALL_GET_TRANSACTION = "get_transaction"; } \ No newline at end of file diff --git a/graphenej/src/main/java/cy/agorise/graphenej/Transaction.java b/graphenej/src/main/java/cy/agorise/graphenej/Transaction.java index acf5412..ff37342 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/Transaction.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/Transaction.java @@ -20,6 +20,7 @@ import java.lang.reflect.Type; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.TimeZone; @@ -97,16 +98,25 @@ public class Transaction implements ByteSerializable, JsonSerializable { public Transaction(BlockData blockData, List operationList){ this.blockData = blockData; this.operations = operationList; + this.extensions = new Extensions(); } /** - * Updates the block data + * Block data getter * @param blockData New block data */ public void setBlockData(BlockData blockData){ this.blockData = blockData; } + /** + * Block data setter + * @return BlockData instance + */ + public BlockData getBlockData(){ + return this.blockData; + } + /** * Updates the fees for all operations in this transaction. * @param fees: New fees to apply @@ -229,7 +239,12 @@ public class Transaction implements ByteSerializable, JsonSerializable { // Getting the signature before anything else, // since this might change the transaction expiration data slightly - byte[] signature = getGrapheneSignature(); + byte[] signature = null; + try{ + signature = getGrapheneSignature(); + }catch(Exception e){ + System.out.println("Could not generate signature"); + } // Formatting expiration time Date expirationTime = new Date(blockData.getExpiration() * 1000); @@ -239,10 +254,12 @@ public class Transaction implements ByteSerializable, JsonSerializable { // Adding expiration obj.addProperty(KEY_EXPIRATION, dateFormat.format(expirationTime)); - // Adding signatures - JsonArray signatureArray = new JsonArray(); - signatureArray.add(Util.bytesToHex(signature)); - obj.add(KEY_SIGNATURES, signatureArray); + if(signature != null){ + // Adding signature + JsonArray signatureArray = new JsonArray(); + signatureArray.add(Util.bytesToHex(signature)); + obj.add(KEY_SIGNATURES, signatureArray); + } JsonArray operationsArray = new JsonArray(); for(BaseOperation operation : operations){ @@ -259,7 +276,19 @@ public class Transaction implements ByteSerializable, JsonSerializable { obj.addProperty(KEY_REF_BLOCK_PREFIX, blockData.getRefBlockPrefix()); return obj; + } + /** + * Method that will return a hash of this transaction's data. The hash covers only the transaction + * attributes and not the signature or the chain id. + * + * @return A hash of the serialized transaction. + */ + public byte[] getHash(){ + byte[] txBytes = toBytes(); + byte[] toHash = Arrays.copyOfRange(txBytes, 32, txBytes.length); //Tx data only, without chain id + Sha256Hash hash = Sha256Hash.wrap(Sha256Hash.hash(toHash)); + return Arrays.copyOfRange(hash.getBytes(), 0, 20); // The hash is only the first 20 bytes } /** @@ -291,7 +320,8 @@ public class Transaction implements ByteSerializable, JsonSerializable { SimpleDateFormat dateFormat = new SimpleDateFormat(Util.TIME_DATE_FORMAT); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); Date expirationDate = dateFormat.parse(expiration, new ParsePosition(0)); - BlockData blockData = new BlockData(refBlockNum, refBlockPrefix, expirationDate.getTime()); + long relativeExpiration = expirationDate.getTime() / 1000; + BlockData blockData = new BlockData(refBlockNum, refBlockPrefix, relativeExpiration); // Parsing operation list BaseOperation operation = null; @@ -387,6 +417,26 @@ public class Transaction implements ByteSerializable, JsonSerializable { //TODO: Add operation deserialization support } else if (operationId == OperationType.ASSET_CLAIM_FEES_OPERATION.ordinal()) { //TODO: Add operation deserialization support + } else if (operationId == OperationType.FBA_DISTRIBUTE_OPERATION.ordinal()) { + //TODO: Add operation deserialization support + } else if (operationId == OperationType.BID_COLLATERAL_OPERATION.ordinal()) { + //TODO: Add operation deserialization support + } else if (operationId == OperationType.EXECUTE_BID_OPERATION.ordinal()) { + //TODO: Add operation deserialization support + } else if (operationId == OperationType.ASSET_CLAIM_POOL_OPERATION.ordinal()) { + //TODO: Add operation deserialization support + } else if (operationId == OperationType.ASSET_UPDATE_ISSUER_OPERATION.ordinal()) { + //TODO: Add operation deserialization support + } else if (operationId == OperationType.HTLC_CREATE_OPERATION.ordinal()) { + //TODO: Add operation deserialization support + } else if (operationId == OperationType.HTLC_REDEEM_OPERATION.ordinal()) { + //TODO: Add operation deserialization support + } else if (operationId == OperationType.HTLC_REDEEMED_OPERATION.ordinal()) { + //TODO: Add operation deserialization support + } else if (operationId == OperationType.HTLC_EXTEND_OPERATION.ordinal()) { + //TODO: Add operation deserialization support + } else if (operationId == OperationType.HTLC_REFUND_OPERATION.ordinal()) { + //TODO: Add operation deserialization support } if (operation != null) operationList.add(operation); operation = null; @@ -401,4 +451,9 @@ public class Transaction implements ByteSerializable, JsonSerializable { return new Transaction(blockData, operationList); } } + + @Override + public String toString() { + return this.toJsonString(); + } } \ No newline at end of file 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/Util.java b/graphenej/src/main/java/cy/agorise/graphenej/Util.java index 4f80a85..c360142 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/Util.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/Util.java @@ -5,6 +5,10 @@ import com.google.common.primitives.UnsignedLong; import org.spongycastle.crypto.DataLengthException; import org.spongycastle.crypto.InvalidCipherTextException; +import org.spongycastle.crypto.digests.GeneralDigest; +import org.spongycastle.crypto.digests.RIPEMD160Digest; +import org.spongycastle.crypto.digests.SHA1Digest; +import org.spongycastle.crypto.digests.SHA256Digest; import org.spongycastle.crypto.engines.AESFastEngine; import org.spongycastle.crypto.modes.CBCBlockCipher; import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; @@ -320,7 +324,9 @@ public class Util { } byte[] temp = new byte[count]; - System.arraycopy(out, out.length - count, temp, 0, temp.length); + int srcPos = out.length - count > 0 ? out.length - count : 0; + int length = count < out.length ? count : out.length; + System.arraycopy(out, srcPos, temp, 0, length); byte[] temp2 = new byte[count]; Arrays.fill(temp2, (byte) count); if (Arrays.equals(temp, temp2)) { @@ -383,4 +389,36 @@ public class Util { public static long toBase(double value, int precision){ return (long) (value * Math.pow(10, precision)); } + + /** + * Creates a hash for HTLC operations. + * + * @param preimage The data we want to operate on. + * @param hashType The type of hash. + * @return The hash. + * @throws NoSuchAlgorithmException + */ + public static byte[] htlcHash(byte[] preimage, HtlcHashType hashType) throws NoSuchAlgorithmException { + byte[] out = null; + GeneralDigest digest = null; + switch(hashType){ + case RIPEMD160: + digest = new RIPEMD160Digest(); + out = new byte[20]; + break; + case SHA1: + digest = new SHA1Digest(); + out = new byte[20]; + break; + case SHA256: + digest = new SHA256Digest(); + out = new byte[32]; + break; + default: + throw new IllegalArgumentException("Not supported hash function!"); + } + digest.update(preimage, 0, preimage.length); + digest.doFinal(out, 0); + return out; + } } 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/GetAccountBalances.java b/graphenej/src/main/java/cy/agorise/graphenej/api/GetAccountBalances.java index 5d65de3..15ed08f 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/api/GetAccountBalances.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/GetAccountBalances.java @@ -79,7 +79,7 @@ public class GetAccountBalances extends BaseGrapheneHandler { } params.add(mUserAccount.getObjectId()); params.add(assetList); - ApiCall apiCall = new ApiCall(0, RPC.GET_ACCOUNT_BALANCES, params, RPC.VERSION, requestId); + ApiCall apiCall = new ApiCall(0, RPC.CALL_GET_ACCOUNT_BALANCES, params, RPC.VERSION, requestId); websocket.sendText(apiCall.toJsonString()); } 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 ab65206..e9c0c4f 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/api/GetObjects.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/GetObjects.java @@ -5,12 +5,10 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonParser; -import com.google.gson.reflect.TypeToken; import com.neovisionaries.ws.client.WebSocket; import com.neovisionaries.ws.client.WebSocketFrame; import java.io.Serializable; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -24,7 +22,9 @@ import cy.agorise.graphenej.RPC; import cy.agorise.graphenej.UserAccount; import cy.agorise.graphenej.interfaces.WitnessResponseListener; import cy.agorise.graphenej.models.ApiCall; +import cy.agorise.graphenej.models.AssetFeed; import cy.agorise.graphenej.models.BitAssetData; +import cy.agorise.graphenej.models.ReportedAssetFeed; import cy.agorise.graphenej.models.WitnessResponse; /** @@ -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()); } @@ -91,6 +89,9 @@ public class GetObjects extends BaseGrapheneHandler { String response = frame.getPayloadText(); GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(BitAssetData.class, new BitAssetData.BitAssetDataDeserializer()); + gsonBuilder.registerTypeAdapter(AssetFeed.class, new AssetFeed.AssetFeedDeserializer()); + gsonBuilder.registerTypeAdapter(ReportedAssetFeed.class, new ReportedAssetFeed.ReportedAssetFeedDeserializer()); gsonBuilder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer()); gsonBuilder.registerTypeAdapter(Asset.class, new Asset.AssetDeserializer()); gsonBuilder.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountFullDeserializer()); @@ -102,7 +103,8 @@ public class GetObjects extends BaseGrapheneHandler { JsonParser parser = new JsonParser(); JsonArray resultArray = parser.parse(response).getAsJsonObject().get(WitnessResponse.KEY_RESULT).getAsJsonArray(); - for(JsonElement element : resultArray){ + for(int i = 0; i < resultArray.size(); i++){ + JsonElement element = resultArray.get(i); String id = element.getAsJsonObject().get(GrapheneObject.KEY_ID).getAsString(); GrapheneObject grapheneObject = new GrapheneObject(id); switch (grapheneObject.getObjectType()){ @@ -115,10 +117,9 @@ public class GetObjects extends BaseGrapheneHandler { parsedResult.add(account); break; case ASSET_BITASSET_DATA: - Type BitAssetDataType = new TypeToken>>(){}.getType(); - WitnessResponse> witnessResponse = gsonBuilder.create().fromJson(response, BitAssetDataType); - BitAssetData bitAssetData = witnessResponse.result.get(0); + BitAssetData bitAssetData = gson.fromJson(element, BitAssetData.class); parsedResult.add(bitAssetData); + break; } } 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 1509be3..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,6 +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 = true; // State variables private boolean isUnsubscribing; @@ -95,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(); } @@ -141,7 +144,7 @@ public class SubscriptionMessagesHub extends BaseGrapheneHandler implements Subs @Override public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { String message = frame.getPayloadText(); - System.out.println("<< "+message); + if(printLogs) System.out.println("<< "+message); if(currentId == LOGIN_ID){ currentId = GET_DATABASE_ID; ArrayList emptyParams = new ArrayList<>(); @@ -185,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{ @@ -316,4 +319,12 @@ public class SubscriptionMessagesHub extends BaseGrapheneHandler implements Subs } } } + + public void setPrintLogs(boolean printLogs){ + this.printLogs = printLogs; + } + + public boolean isPrintLogs(){ + return this.printLogs; + } } 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..0581848 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/DeserializationMap.java @@ -0,0 +1,265 @@ +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.Memo; +import cy.agorise.graphenej.Transaction; +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.api.calls.GetAccountBalances; +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.GetAssets; +import cy.agorise.graphenej.api.calls.GetBlock; +import cy.agorise.graphenej.api.calls.GetBlockHeader; +import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties; +import cy.agorise.graphenej.api.calls.GetFullAccounts; +import cy.agorise.graphenej.api.calls.GetKeyReferences; +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.GetTransaction; +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.AssetFeed; +import cy.agorise.graphenej.models.BitAssetData; +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.OperationHistory; +import cy.agorise.graphenej.models.ReportedAssetFeed; +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<>(); + + 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() + .registerTypeAdapter(Authority.class, new Authority.AuthorityDeserializer()) + .registerTypeAdapter(AccountOptions.class, new AccountOptions.AccountOptionsDeserializer(false)) + .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()) + .registerTypeAdapter(BitAssetData.class, new BitAssetData.BitAssetDataDeserializer()) + .registerTypeAdapter(ReportedAssetFeed.class, new ReportedAssetFeed.ReportedAssetFeedDeserializer()) + .registerTypeAdapter(AssetFeed.class, new AssetFeed.AssetFeedDeserializer()) + .registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer()) + .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); + + // GetDynamicGlobalProperties + mClassMap.put(GetDynamicGlobalProperties.class, DynamicGlobalProperties.class); + Gson getDynamicGlobalPropertiesGson = new GsonBuilder() + .registerTypeAdapter(DynamicGlobalProperties.class, new DynamicGlobalProperties.DynamicGlobalPropertiesDeserializer()) + .create(); + mGsonMap.put(GetDynamicGlobalProperties.class, getDynamicGlobalPropertiesGson); + + // GetKeyReferences + mClassMap.put(GetKeyReferences.class, List.class); + Gson getKeyReferencesGson = new GsonBuilder() + .registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer()) + .create(); + mGsonMap.put(GetKeyReferences.class, getKeyReferencesGson); + + // GetAccountBalances + mClassMap.put(GetAccountBalances.class, List.class); + Gson getAccountBalancesGson = new GsonBuilder() + .registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer()) + .create(); + mGsonMap.put(GetAccountBalances.class, getAccountBalancesGson); + + // GetAssets + mClassMap.put(GetAssets.class, List.class); + Gson getAssetsGson = new GsonBuilder() + .registerTypeAdapter(Asset.class, new Asset.AssetDeserializer()) + .create(); + mGsonMap.put(GetAssets.class, getAssetsGson); + + // GetTransaction + mClassMap.put(GetTransaction.class, Transaction.class); + Gson getTransactionGson = new GsonBuilder() + .registerTypeAdapter(Transaction.class, new Transaction.TransactionDeserializer()) + .registerTypeAdapter(Memo.class, new Memo.MemoDeserializer()) + .registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer()) + .registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer()) + .create(); + mGsonMap.put(GetTransaction.class, getTransactionGson); + } + + 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..32e6918 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java @@ -0,0 +1,795 @@ +package cy.agorise.graphenej.api.android; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +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.HashMap; +import java.util.List; +import java.util.MissingResourceException; +import java.util.concurrent.TimeUnit; + +import cy.agorise.graphenej.Asset; +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.BaseOperation; +import cy.agorise.graphenej.LimitOrder; +import cy.agorise.graphenej.Memo; +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.calls.ApiCallable; +import cy.agorise.graphenej.api.calls.GetAccountBalances; +import cy.agorise.graphenej.api.calls.GetAccounts; +import cy.agorise.graphenej.api.calls.GetAssets; +import cy.agorise.graphenej.api.calls.GetFullAccounts; +import cy.agorise.graphenej.api.calls.GetKeyReferences; +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.BitAssetData; +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.network.FullNode; +import cy.agorise.graphenej.network.LatencyNodeProvider; +import cy.agorise.graphenej.network.NodeLatencyVerifier; +import cy.agorise.graphenej.network.NodeProvider; +import cy.agorise.graphenej.operations.CustomOperation; +import cy.agorise.graphenej.operations.LimitOrderCreateOperation; +import cy.agorise.graphenej.operations.TransferOperation; +import cy.agorise.graphenej.stats.ExponentialMovingAverage; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.annotations.Nullable; +import io.reactivex.disposables.Disposable; +import io.reactivex.subjects.PublishSubject; +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(); + + public static final int NORMAL_CLOSURE_STATUS = 1000; + private static final int GOING_AWAY_STATUS = 1001; + + // Time to wait before retrying a connection attempt + private static final int DEFAULT_RETRY_DELAY = 500; + + // Default connection delay when using the node latency verification strategy. This initial + // delay is required in order ot make sure we have a fair selection of node latencies from + // which we can choose from. + private final int DEFAULT_INITIAL_DELAY = 500; + + /** + * Constant to be used as a key in order to pass the user name information, in case the + * provided API nodes might require this information. + */ + public static final String KEY_USERNAME = "key_username"; + + /** + * Constant to be used as a key in order to pass the password information, in case the + * provided API nodes might require this information. + *

+ * This information should be passed as an intent extra when calling the bindService + * or startService methods. + */ + public static final String KEY_PASSWORD = "key_password"; + + /** + * Constant used as a key in order to specify which APIs the application will be requiring. + *

+ * This information should be passed as an intent extra when calling the bindService + * or startService methods. + */ + public static final String KEY_REQUESTED_APIS = "key_requested_apis"; + + /** + * Constant used as a key in order to let the NetworkService know whether or not it should + * start a recurring node latency verification task. + *

+ * This information should be passed as an intent extra when calling the bindService + * or startService methods. + */ + public static final String KEY_ENABLE_LATENCY_VERIFIER = "key_enable_latency_verifier"; + + /** + * Constant used as a key in order to specify the alpha (or smoothing) factor to be used in + * the exponential moving average calculated from the different latency samples. This only + * makes sense if the latency verification feature is enabled of course. + *

+ * This information should be passed as an intent extra when calling the bindService + * or startService methods. + */ + public static final String KEY_NODE_LATENCY_SMOOTHING_FACTOR = "key_node_latency_smoothing_factor"; + + /** + * Key used to pass via intent a boolean extra to specify whether the connection should + * be automatically established. + *

+ * This information should be passed as an intent extra when calling the bindService + * or startService methods. + */ + public static final String KEY_AUTO_CONNECT = "key_auto_connect"; + + /** + * Key used to pass via intent a list of node URLs. The value passed should be a String + * containing a simple comma separated list of URLs. + *

+ * For example: + * + * wss://domain1.com/ws,wss://domain2.com/ws,wss://domain3.com/ws + *

+ * This information should be passed as an intent extra when calling the bindService + * or startService methods. + */ + public static final String KEY_NODE_URLS = "key_node_urls"; + + private final IBinder mBinder = new LocalBinder(); + + private WebSocket mWebSocket; + + // 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(); + + // Variable used as a source of node information + private NodeProvider nodeProvider = new LatencyNodeProvider(); + + // Class used to obtain frequent node latency updates + private NodeLatencyVerifier nodeLatencyVerifier; + + // PublishSubject used to announce full node latencies updates + private PublishSubject fullNodePublishSubject; + + // Counter used to trigger the connection only after we've received enough node latency updates + private long latencyUpdateCounter; + + // Property used to keep track of the currently active node + private FullNode mSelectedNode; + + private Handler mHandler = new Handler(Looper.getMainLooper()); + + 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(); + + /** + * Actually establishes a connection from this Service to one of the full nodes. + */ + public void connect(){ + OkHttpClient client = new OkHttpClient + .Builder() + .connectTimeout(2, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .writeTimeout(5, TimeUnit.SECONDS) + .build(); + + synchronized (mWebSocketListener){ + mSelectedNode = nodeProvider.getBestNode(); + if(mSelectedNode != null){ + Log.d(TAG,"Trying to connect to: "+ mSelectedNode.getUrl()); + Request request = new Request.Builder().url(mSelectedNode.getUrl()).build(); + mWebSocket = client.newWebSocket(request, mWebSocketListener); + }else{ + Log.d(TAG,"Could not find best node, reescheduling"); + // If no node could be found yet, schedule a new attempt in DEFAULT_INITIAL_DELAY ms + mHandler.postDelayed(mConnectAttempt, DEFAULT_INITIAL_DELAY); + } + } + } + + 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 synchronized 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); + + if(nodeLatencyVerifier != null) + nodeLatencyVerifier.stop(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + /** + * Initialize information and try to connect to a node accordingly. This methods were moved + * from onBind to avoid crashes due to components other than {@link NetworkServiceManager} + * binding to the service without submitting the proper information. + * + * @param extras Bundle that contains all required information for a proper initialization + */ + public void bootstrapService(Bundle extras) { + // Retrieving credentials and requested API data from the shared preferences + mUsername = extras.getString(NetworkService.KEY_USERNAME, ""); + mPassword = extras.getString(NetworkService.KEY_PASSWORD, ""); + mRequestedApis = extras.getInt(NetworkService.KEY_REQUESTED_APIS, 0); + boolean mAutoConnect = extras.getBoolean(NetworkService.KEY_AUTO_CONNECT, true); + boolean verifyNodeLatency = extras.getBoolean(NetworkService.KEY_ENABLE_LATENCY_VERIFIER, false); + + // If the user of the library desires, a custom list of node URLs can + // be passed using the KEY_NODE_URLS constant + String nodeURLStr = extras.getString(NetworkService.KEY_NODE_URLS, ""); + if(nodeURLStr.equals("")){ + throw new MissingResourceException("A comma-separated list of node URLs must be provided as an intent extra", String.class.getName(), NetworkService.KEY_NODE_URLS); + } + + // Adding user-provided list of node URLs + String[] urls = nodeURLStr.split(","); + + // Feeding all node information to the NodeProvider instance + for(String nodeUrl : urls){ + nodeProvider.addNode(new FullNode(nodeUrl)); + } + + if (!mAutoConnect && !verifyNodeLatency) { + throw new IllegalArgumentException("NetworkService$bootstrapService: verifyNodeLatency cannot be false when autoConnect is false too."); + } + + if (verifyNodeLatency) { + double alpha = extras.getDouble(KEY_NODE_LATENCY_SMOOTHING_FACTOR, ExponentialMovingAverage.DEFAULT_ALPHA); + ArrayList fullNodes = new ArrayList<>(); + for(String url : urls){ + fullNodes.add(new FullNode(url, alpha)); + } + nodeLatencyVerifier = new NodeLatencyVerifier(fullNodes); + fullNodePublishSubject = nodeLatencyVerifier.start(); + fullNodePublishSubject.observeOn(AndroidSchedulers.mainThread()).subscribe(nodeLatencyObserver); + } + + if (mAutoConnect) + connect(); + else + mHandler.postDelayed(mConnectAttempt, DEFAULT_INITIAL_DELAY); + + // TODO make sure (verifyNodeLatency==false && mAutoConnect==true) is a valid/useful combination, else simplify and use only one of those arguments + } + + /** + * Used to close the current connection and cause the service to attempt a reconnection. + */ + public void reconnectNode() { + mWebSocket.close(GOING_AWAY_STATUS, null); + } + + /** + * Runnable that will perform a connection attempt with the best node after DEFAULT_INITIAL_DELAY + * milliseconds. This is used only if the node latency verification is activated. + * + * The reason to delay the initial connection is that we want to ideally connect to the best node, + * meaning the one that offers the lowest latency value. But we have to give some time for the + * first node latency measurement round to finish in order to have at least a partial result set + * that could be used. + */ + private Runnable mConnectAttempt = new Runnable() { + @Override + public void run() { + FullNode fullNode = nodeProvider.getBestNode(); + if(fullNode != null){ + Log.i(TAG, String.format("Connected with %d latency results", latencyUpdateCounter)); + connect(); + }else{ + mHandler.postDelayed(this, DEFAULT_INITIAL_DELAY); + } + } + }; + + /** + * Observer used to be notified about node latency measurement updates. + */ + private Observer nodeLatencyObserver = new Observer() { + @Override + public void onSubscribe(Disposable d) { } + + @Override + public void onNext(FullNode fullNode) { + latencyUpdateCounter++; + // Updating the node with the new latency measurement + nodeProvider.updateNode(fullNode); + } + + @Override + public void onError(Throwable e) { + Log.e(TAG,"nodeLatencyObserver.onError.Msg: "+e.getMessage()); + } + + @Override + public void onComplete() { } + }; + + /** + * 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 synchronized void onOpen(WebSocket webSocket, Response response) { + super.onOpen(webSocket, response); + + // Marking the selected node as connected + mSelectedNode.setConnected(true); + + // Updating the selected node's 'connected' status on the NodeLatencyVerifier instance + if(nodeLatencyVerifier != null) + nodeLatencyVerifier.updateActiveNodeInformation(mSelectedNode); + + // 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 synchronized 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 == DynamicGlobalProperties.class){ + Type GetDynamicGlobalPropertiesResponse = new TypeToken>(){}.getType(); + parsedResponse = gson.fromJson(text, GetDynamicGlobalPropertiesResponse); + }else if(responsePayloadClass == Transaction.class){ + Type GetTransactionClass = new TypeToken>(){}.getType(); + parsedResponse = gson.fromJson(text, GetTransactionClass); + }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 if(requestClass == GetKeyReferences.class){ + Type GetKeyReferencesResponse = new TypeToken>>>(){}.getType(); + parsedResponse = gson.fromJson(text, GetKeyReferencesResponse); + } else if(requestClass == GetAccountBalances.class){ + Type GetAccountBalancesResponse = new TypeToken>>(){}.getType(); + parsedResponse = gson.fromJson(text, GetAccountBalancesResponse); + } else if(requestClass == GetAssets.class){ + Type GetAssetsResponse = new TypeToken>>(){}.getType(); + parsedResponse = gson.fromJson(text, GetAssetsResponse); + }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: Add support for other types of 'get_objects' request types + Gson gson = mDeserializationMap.getGson(GetObjects.class); + Type GetBitAssetResponse = new TypeToken>>(){}.getType(); + return gson.fromJson(response, GetBitAssetResponse); + } + + /** + * 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); + if(code == GOING_AWAY_STATUS) + handleWebSocketDisconnection(true, false); + else + handleWebSocketDisconnection(false, 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.v(TAG,String.format("%s#%s:%s", element.getClassName(), element.getMethodName(), element.getLineNumber())); + } + + // If there is a response, we print it + if(response != null){ + Log.e(TAG,"Response: "+response.message()); + } + + handleWebSocketDisconnection(true, true); + } + + /** + * Method that encapsulates the behavior of handling a disconnection to the current node, and + * potentially tries to reconnect to another one. + * + * @param tryReconnection States if a reconnection to other node should be tried. + * @param penalizeNode Whether or not to penalize the current node with a very high latency reading. + */ + private synchronized void handleWebSocketDisconnection(boolean tryReconnection, boolean penalizeNode) { + Log.d(TAG,"handleWebSocketDisconnection. try reconnection: " + tryReconnection + ", penalizeNode: " + penalizeNode); + RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE)); + isLoggedIn = false; + + // Clearing previous request id to class mappings + mRequestClassMap.clear(); + + if(mSelectedNode != null){ + // Marking the selected node as not connected + mSelectedNode.setConnected(false); + + // Updating the selected node's 'connected' status on the NodeLatencyVerifier instance + if(nodeLatencyVerifier != null) + nodeLatencyVerifier.updateActiveNodeInformation(mSelectedNode); + + if (penalizeNode){ + // Adding a very high latency value to this node in order to prevent + // us from getting it again + mSelectedNode.addLatencyValue(Long.MAX_VALUE); + nodeProvider.updateNode(mSelectedNode); + } + } + + if(tryReconnection) { + // Registering current status + mCurrentId = 0; + mApiIds.clear(); + + RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE)); + + if (nodeProvider.getBestNode() == null) { + Log.e(TAG, "Giving up on connections"); + stopSelf(); + } else { + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + connect(); + } + }, DEFAULT_RETRY_DELAY); + } + } + + // We have currently no selected node + mSelectedNode = null; + } + }; + + /** + * 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; + } + + /** + * Updates the full node details + * @param fullNode Updated {@link FullNode} instance + */ + public void updateNode(FullNode fullNode){ + nodeProvider.updateNode(fullNode); + } + + /** + * Returns a list of {@link FullNode} instances + * @return List of full nodes + */ + public List getNodes(){ + return nodeProvider.getSortedNodes(); + } + + + /** + * Returns the currently selected node + */ + public FullNode getSelectedNode() { return mSelectedNode; } + + /** + * Returns an observable that will notify its observers about node latency updates. + * @return Observer of {@link FullNode} instances. + */ + public PublishSubject getNodeLatencyObservable(){ + return fullNodePublishSubject; + } + + public NodeLatencyVerifier getNodeLatencyVerifier(){ return nodeLatencyVerifier; } +} 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..f3b65be --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java @@ -0,0 +1,325 @@ +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; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import cy.agorise.graphenej.stats.ExponentialMovingAverage; + +/** + * 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 static 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; + + // Attributes that might need to be passed to the NetworkService + private String mUserName = ""; + private String mPassword = ""; + private int mRequestedApis; + private List mCustomNodeUrls = new ArrayList<>(); + private boolean mAutoConnect; + private boolean mVerifyLatency; + // Flag used to make sure we only call 'bindService' once. +// private boolean mStartingService; + + /** + * 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)); + } + }; + + private NetworkServiceManager(Context context){ + mContextReference = new WeakReference<>(context); + } + + @Override + public void onActivityCreated(Activity activity, Bundle bundle) { } + + @Override + public void onActivityStarted(Activity activity) { } + + @Override + public void onActivityResumed(Activity activity) { + mHandler.removeCallbacks(mDisconnectRunnable); + if(mService == null){ + // Creating a new Intent that will be used to start the NetworkService + Context context = mContextReference.get(); + Intent intent = new Intent(context, NetworkService.class); + context.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); + } + } + + /** + * This method passes all the required information to the NetworkService to properly + * initialize itself + */ + private void passRequiredInfoToConfigureService() { + // Adding user-provided node URLs + StringBuilder stringBuilder = new StringBuilder(); + Iterator it = mCustomNodeUrls.iterator(); + while(it.hasNext()){ + stringBuilder.append(it.next()); + if(it.hasNext()) stringBuilder.append(","); + } + String customNodes = stringBuilder.toString(); + + Bundle b = new Bundle(); + + // Adding all + b.putString(NetworkService.KEY_USERNAME, mUserName); + b.putString(NetworkService.KEY_PASSWORD, mPassword); + b.putInt(NetworkService.KEY_REQUESTED_APIS, mRequestedApis); + b.putString(NetworkService.KEY_NODE_URLS, customNodes); + b.putBoolean(NetworkService.KEY_AUTO_CONNECT, mAutoConnect); + b.putBoolean(NetworkService.KEY_ENABLE_LATENCY_VERIFIER, mVerifyLatency); + + mService.bootstrapService(b); + } + + @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; + boolean passInfo = false; + if(mService == null){ + mService = binder.getService(); + // We only pass the required information in case this is the first time we get a reference + // to the NetworkService instance. + passInfo = true; + } + if(passInfo) + passRequiredInfoToConfigureService(); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + } + }; + + public String getUserName() { + return mUserName; + } + + public void setUserName(String userName) { + this.mUserName = userName; + } + + public String getPassword() { + return mPassword; + } + + public void setPassword(String mPassword) { + this.mPassword = mPassword; + } + + public int getRequestedApis() { + return mRequestedApis; + } + + public void setRequestedApis(int mRequestedApis) { + this.mRequestedApis = mRequestedApis; + } + + public List getCustomNodeUrls() { + return mCustomNodeUrls; + } + + public void setCustomNodeUrls(List mCustomNodeUrls) { + this.mCustomNodeUrls = mCustomNodeUrls; + } + + public boolean isAutoConnect() { + return mAutoConnect; + } + + public void setAutoConnect(boolean mAutoConnect) { + this.mAutoConnect = mAutoConnect; + } + + public boolean isVerifyLatency() { + return mVerifyLatency; + } + + public void setVerifyLatency(boolean mVerifyLatency) { + this.mVerifyLatency = mVerifyLatency; + } + + /** + * Class used to create a {@link NetworkServiceManager} with specific attributes. + */ + public static class Builder { + private String username; + private String password; + private int requestedApis; + private List customNodeUrls; + private boolean autoconnect = true; + private boolean verifyNodeLatency; + private double alpha = ExponentialMovingAverage.DEFAULT_ALPHA; + + /** + * Sets the user name, if required to connect to a node. + * @param name User name + * @return The Builder instance + */ + public Builder setUserName(String name){ + this.username = name; + return this; + } + + /** + * Sets the password, if required to connect to a node. + * @param password Password + * @return The Builder instance + */ + public Builder setPassword(String password){ + this.password = password; + return this; + } + + /** + * Sets an integer with the requested APIs encoded as binary flags. + * @param apis Integer representing the different APIs we require from the node. + * @return The Builder instance + */ + public Builder setRequestedApis(int apis){ + this.requestedApis = apis; + return this; + } + + /** + * Adds a list of custom node URLs. + * @param nodeUrls List of custom full node URLs. + * @return The Builder instance + */ + public Builder setCustomNodeUrls(List nodeUrls){ + this.customNodeUrls = nodeUrls; + return this; + } + + /** + * Adds a list of custom node URLs. + * @param nodeUrls List of custom full node URLs. + * @return The Builder instance + */ + public Builder setCustomNodeUrls(String nodeUrls){ + String[] urls = nodeUrls.split(","); + for(String url : urls){ + if(customNodeUrls == null) customNodeUrls = new ArrayList<>(); + customNodeUrls.add(url); + } + return this; + } + + /** + * Sets the autoconnect flag. This is true by default. + * @param autoConnect True if we want the service to connect automatically, false otherwise. + * @return The Builder instance + */ + public Builder setAutoConnect(boolean autoConnect){ + this.autoconnect = autoConnect; + return this; + } + + /** + * Sets the node-verification flag. This is false by default. + * @param verifyLatency True if we want the service to perform a latency analysis before connecting. + * @return The Builder instance. + */ + public Builder setNodeLatencyVerification(boolean verifyLatency){ + this.verifyNodeLatency = verifyLatency; + return this; + } + + /** + * Sets the node latency verification's exponential moving average alpha parameter. + * @param alpha The alpha parameter to use when computing the exponential moving average of the + * measured latencies. + * @return The Builder instance. + */ + public Builder setLatencyAverageAlpha(double alpha){ + this.alpha = alpha; + return this; + } + + /** + * Method used to build a {@link NetworkServiceManager} instance with all of the characteristics + * passed as parameters. + * @param context A Context of the application package implementing + * this class. + * @return Instance of the NetworkServiceManager class. + */ + public NetworkServiceManager build(Context context){ + NetworkServiceManager manager = new NetworkServiceManager(context); + if(username != null) manager.setUserName(username); else manager.setUserName(""); + if(password != null) manager.setPassword(password); else manager.setPassword(""); + if(customNodeUrls != null) manager.setCustomNodeUrls(customNodeUrls); + manager.setRequestedApis(requestedApis); + manager.setAutoConnect(autoconnect); + manager.setVerifyLatency(verifyNodeLatency); + return manager; + } + } +} 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/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/BroadcastTransaction.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/BroadcastTransaction.java new file mode 100644 index 0000000..8dc1ba5 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/BroadcastTransaction.java @@ -0,0 +1,27 @@ +package cy.agorise.graphenej.api.calls; + +import java.io.Serializable; +import java.util.ArrayList; + +import cy.agorise.graphenej.RPC; +import cy.agorise.graphenej.Transaction; +import cy.agorise.graphenej.api.ApiAccess; +import cy.agorise.graphenej.models.ApiCall; + +public class BroadcastTransaction implements ApiCallable { + public static final int REQUIRED_API = ApiAccess.API_NETWORK_BROADCAST; + + private Transaction mTransaction; + + public BroadcastTransaction(Transaction transaction){ + if(!transaction.hasPrivateKey()) throw new IllegalStateException("The Transaction instance has to be provided with a private key in order to be broadcasted"); + mTransaction = transaction; + } + + @Override + public ApiCall toApiCall(int apiId, long sequenceId) { + ArrayList transactions = new ArrayList<>(); + transactions.add(mTransaction); + return new ApiCall(apiId, RPC.CALL_BROADCAST_TRANSACTION, transactions, RPC.VERSION, 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/GetAccountBalances.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountBalances.java new file mode 100644 index 0000000..4244519 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAccountBalances.java @@ -0,0 +1,42 @@ +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.UserAccount; +import cy.agorise.graphenej.api.ApiAccess; +import cy.agorise.graphenej.models.ApiCall; + +/** + * Wrapper around the 'get_account_balances' API call. + * + * @see get_account_balances API doc + */ +public class GetAccountBalances implements ApiCallable { + public static final int REQUIRED_API = ApiAccess.API_NONE; + + private UserAccount mUserAccount; + private List mAssetList; + + public GetAccountBalances(UserAccount userAccount, List assets){ + mUserAccount = userAccount; + mAssetList = assets; + } + + @Override + public ApiCall toApiCall(int apiId, long sequenceId) { + ArrayList params = new ArrayList<>(); + ArrayList assetList = new ArrayList<>(); + if(mAssetList != null){ + for(Asset asset : mAssetList){ + assetList.add(asset.getObjectId()); + } + } + params.add(mUserAccount.getObjectId()); + params.add(assetList); + return new ApiCall(apiId, RPC.CALL_GET_ACCOUNT_BALANCES, params, 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/GetAssets.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAssets.java new file mode 100644 index 0000000..91ca709 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetAssets.java @@ -0,0 +1,54 @@ +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 GetAssets implements ApiCallable { + public static final int REQUIRED_API = ApiAccess.API_NONE; + + private List assetList = new ArrayList<>(); + + /** + * Constructor that will receive a List of Asset instances. + * + * @param assets List of Asset instances. + */ + public GetAssets(List assets){ + assetList.addAll(assets); + } + + /** + * Constructor that will accept a string containing the asset id. + * + * @param id String containing the asset id of the desired asset. + */ + public GetAssets(String id){ + assetList.add(new Asset(id)); + } + + /** + * Constructor that accepts an {@link Asset} object instance. + * + * @param asset Asset class instance. + */ + public GetAssets(Asset asset){ + assetList.add(asset); + } + + @Override + public ApiCall toApiCall(int apiId, long sequenceId) { + ArrayList params = new ArrayList<>(); + ArrayList assetIds = new ArrayList<>(); + for(Asset asset : assetList){ + assetIds.add(asset.getObjectId()); + } + params.add(assetIds); + return new ApiCall(apiId, RPC.CALL_GET_ASSETS, 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/GetDynamicGlobalProperties.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetDynamicGlobalProperties.java new file mode 100644 index 0000000..eacedac --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetDynamicGlobalProperties.java @@ -0,0 +1,18 @@ +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 GetDynamicGlobalProperties implements ApiCallable { + public static final int REQUIRED_API = ApiAccess.API_NONE; + + @Override + public ApiCall toApiCall(int apiId, long sequenceId) { + ArrayList params = new ArrayList<>(); + return new ApiCall(apiId, RPC.CALL_GET_DYNAMIC_GLOBAL_PROPERTIES, 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..eb32047 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetFullAccounts.java @@ -0,0 +1,45 @@ +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_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(UserAccount userAccount, boolean subscribe){ + this.mUserAccounts = new ArrayList<>(); + if(userAccount.getName() != null && !userAccount.getName().equals("")){ + this.mUserAccounts.add(userAccount.getName()); + }else{ + this.mUserAccounts.add(userAccount.getObjectId()); + } + this.mSubscribe = subscribe; + } + + 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/GetKeyReferences.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetKeyReferences.java new file mode 100644 index 0000000..e0271af --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetKeyReferences.java @@ -0,0 +1,43 @@ +package cy.agorise.graphenej.api.calls; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import cy.agorise.graphenej.Address; +import cy.agorise.graphenej.RPC; +import cy.agorise.graphenej.api.ApiAccess; +import cy.agorise.graphenej.errors.MalformedAddressException; +import cy.agorise.graphenej.models.ApiCall; + +/** + * Wrapper around the 'get_key_references' API call. + */ +public class GetKeyReferences implements ApiCallable { + public static final int REQUIRED_API = ApiAccess.API_NONE; + + private List
addresses = new ArrayList<>(); + + public GetKeyReferences(String addr) throws MalformedAddressException, IllegalArgumentException { + this(new Address(addr)); + } + + public GetKeyReferences(Address address){ + addresses.add(address); + } + + public GetKeyReferences(List
addressList){ + addresses.addAll(addressList); + } + + @Override + public ApiCall toApiCall(int apiId, long sequenceId) { + ArrayList inner = new ArrayList(); + for(Address addr : addresses){ + inner.add(addr.toString()); + } + ArrayList params = new ArrayList<>(); + params.add(inner); + return new ApiCall(apiId, RPC.CALL_GET_KEY_REFERENCES, 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..e187fc0 --- /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_NONE; + + 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/GetTransaction.java b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetTransaction.java new file mode 100644 index 0000000..c55e2bd --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/calls/GetTransaction.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; + +public class GetTransaction implements ApiCallable { + public static final int REQUIRED_API = ApiAccess.API_NONE; + + private long blockNumber; + private long txIndex; + + public GetTransaction(long blockNumber, long txIndex){ + this.blockNumber = blockNumber; + this.txIndex = txIndex; + } + + @Override + public ApiCall toApiCall(int apiId, long sequenceId) { + List params = new ArrayList<>(); + params.add(blockNumber); + params.add(txIndex); + return new ApiCall(apiId, RPC.CALL_GET_TRANSACTION, params, 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/AssetFeed.java b/graphenej/src/main/java/cy/agorise/graphenej/models/AssetFeed.java index 66eef84..6ad6e49 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/models/AssetFeed.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/AssetFeed.java @@ -1,13 +1,80 @@ 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.lang.reflect.Type; + import cy.agorise.graphenej.Price; /** - * Created by nelson on 1/9/17. + * Price feed of a given asset. */ public class AssetFeed { - public Price settlement_price; - public long maintenance_collateral_ratio; - public long maximum_short_squeeze_ratio; - public Price core_exchange_rate; + public static final String KEY_SETTLEMENT_PRICE = "settlement_price"; + public static final String KEY_MAINTENANCE_COLLATERAL_RATIO = "maintenance_collateral_ratio"; + public static final String KEY_MAXIMUM_SHORT_SQUEEZE_RATIO = "maximum_short_squeeze_ratio"; + public static final String KEY_CORE_EXCHANGE_RATE = "core_exchange_rate"; + + private Price settlement_price; + private long maintenance_collateral_ratio; + private long maximum_short_squeeze_ratio; + private Price core_exchange_rate; + + public AssetFeed(Price settlementPrice, long maintenanceCollateralRatio, long maximumShortSqueezeRatio, Price coreExchangeRate){ + this.settlement_price = settlementPrice; + this.maintenance_collateral_ratio = maintenanceCollateralRatio; + this.maximum_short_squeeze_ratio = maximumShortSqueezeRatio; + this.core_exchange_rate = coreExchangeRate; + } + + public Price getSettlementPrice() { + return settlement_price; + } + + public void setSettlementPrice(Price settlement_price) { + this.settlement_price = settlement_price; + } + + public long getMaintenanceCollateralRatio() { + return maintenance_collateral_ratio; + } + + public void setMaintenanceCollateralRatio(long maintenance_collateral_ratio) { + this.maintenance_collateral_ratio = maintenance_collateral_ratio; + } + + public long getMaximumShortSqueezeRatio() { + return maximum_short_squeeze_ratio; + } + + public void setMaximumShortSqueezeRatio(long maximum_short_squeeze_ratio) { + this.maximum_short_squeeze_ratio = maximum_short_squeeze_ratio; + } + + public Price getCoreExchangeRate() { + return core_exchange_rate; + } + + public void setCoreExchangeRate(Price core_exchange_rate) { + this.core_exchange_rate = core_exchange_rate; + } + + public static class AssetFeedDeserializer implements JsonDeserializer { + + @Override + public AssetFeed deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + Price settlementPrice = context.deserialize(jsonObject.get(KEY_SETTLEMENT_PRICE).getAsJsonObject(), Price.class); + long collateralRatio = jsonObject.get(KEY_MAINTENANCE_COLLATERAL_RATIO).getAsLong(); + long maximumShortSqueezeRatio = jsonObject.get(KEY_MAXIMUM_SHORT_SQUEEZE_RATIO).getAsLong(); + Price coreExchangeRate = context.deserialize(jsonObject.get(KEY_CORE_EXCHANGE_RATE), Price.class); + AssetFeed assetFeed = new AssetFeed(settlementPrice, collateralRatio, maximumShortSqueezeRatio, coreExchangeRate); + return assetFeed; + } + } + } 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/BitAssetData.java b/graphenej/src/main/java/cy/agorise/graphenej/models/BitAssetData.java index 1d27222..79c7fbf 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/models/BitAssetData.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/BitAssetData.java @@ -1,25 +1,158 @@ 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; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; + import cy.agorise.graphenej.GrapheneObject; import cy.agorise.graphenej.Price; +import cy.agorise.graphenej.Util; /** * This is the representation of the response from the 'get_objects' call with * a 2.4.x id, which will retrieve a 'impl_asset_bitasset_data_type'. * - * Created by nelson on 1/8/17. */ public class BitAssetData extends GrapheneObject { - public Object[] feeds; - public AssetFeed current_feed; - public String current_feed_publication_time; - public Object options; - public long force_settled_volume; - public boolean is_prediction_market; - public Price settlement_price; - public long settlement_fund; + public static final String KEY_FEEDS = "feeds"; + public static final String KEY_CURRENT_FEED = "current_feed"; + public static final String KEY_CURRENT_FEED_PUBLICATION_TIME = "current_feed_publication_time"; + public static final String KEY_OPERATIONS = "operations"; + public static final String KEY_FORCE_SETTLED_VOLUME = "force_settled_volume"; + public static final String KEY_IS_PREDICTION_MARKET = "is_prediction_market"; + public static final String KEY_SETTLEMENT_PRICE = "settlement_price"; + public static final String KEY_SETTLEMENT_FUND = "settlement_fund"; + + private ReportedAssetFeed[] feeds; + private AssetFeed current_feed; + private Date current_feed_publication_time; + private Options options; + private long force_settled_volume; + private boolean is_prediction_market; + private Price settlement_price; + private long settlement_fund; public BitAssetData(String id) { super(id); } + + public ReportedAssetFeed[] getFeeds() { + return feeds; + } + + public void setFeeds(ReportedAssetFeed[] feeds) { + this.feeds = feeds; + } + + public AssetFeed getCurrentFeed() { + return current_feed; + } + + public void setCurrentFeed(AssetFeed current_feed) { + this.current_feed = current_feed; + } + + public Date getCurrentFeedPublicationTime() { + return current_feed_publication_time; + } + + public void setCurrentFeedPublicationTime(String currentFeedPublicationTime) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(Util.TIME_DATE_FORMAT); + try { + this.current_feed_publication_time = simpleDateFormat.parse(currentFeedPublicationTime); + } catch (ParseException e) { + e.printStackTrace(); + } + } + + public Options getOptions() { + return options; + } + + public void setOptions(Options options) { + this.options = options; + } + + public long getForceSettledVolume() { + return force_settled_volume; + } + + public void setForceSettledVolume(long force_settled_volume) { + this.force_settled_volume = force_settled_volume; + } + + public boolean isPredictionMarket() { + return is_prediction_market; + } + + public void setIsPredictionMarket(boolean is_prediction_market) { + this.is_prediction_market = is_prediction_market; + } + + public Price getSettlementPrice() { + return settlement_price; + } + + public void setSettlementPrice(Price settlementPrice) { + this.settlement_price = settlementPrice; + } + + public long getSettlementFund() { + return settlement_fund; + } + + public void setSettlementFund(long settlementFund) { + this.settlement_fund = settlementFund; + } + + /** + * Custom deserializer used to instantiate the BitAssetData class from the response of the + * 'get_objects' API call. + */ + public static class BitAssetDataDeserializer implements JsonDeserializer { + + @Override + public BitAssetData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + String id = jsonObject.get(GrapheneObject.KEY_ID).getAsString(); + BitAssetData bitAssetData = new BitAssetData(id); + ArrayList reportedAssetFeeds = new ArrayList<>(); + + JsonArray jsonAssetFeeds = jsonObject.get(KEY_FEEDS).getAsJsonArray(); + for(JsonElement jsonFeed : jsonAssetFeeds){ + ReportedAssetFeed reportedAssetFeed = context.deserialize(jsonFeed, ReportedAssetFeed.class); + reportedAssetFeeds.add(reportedAssetFeed); + } + + // Deserializing attributes + JsonElement jsonCurrentFeed = jsonObject.get(KEY_CURRENT_FEED).getAsJsonObject(); + AssetFeed assetFeed = context.deserialize(jsonCurrentFeed, AssetFeed.class); + String publicationTime = jsonObject.get(KEY_CURRENT_FEED_PUBLICATION_TIME).getAsString(); + Options options = context.deserialize(jsonObject.get(KEY_OPERATIONS), Options.class); + long forceSettledVolume = jsonObject.get(KEY_FORCE_SETTLED_VOLUME).getAsLong(); + boolean isPredictionMarket = jsonObject.get(KEY_IS_PREDICTION_MARKET).getAsBoolean(); + Price settlementPrice = context.deserialize(jsonObject.get(KEY_SETTLEMENT_PRICE), Price.class); + long settlementFund = jsonObject.get(KEY_SETTLEMENT_FUND).getAsLong(); + + // Setting attributes + bitAssetData.setFeeds(reportedAssetFeeds.toArray(new ReportedAssetFeed[reportedAssetFeeds.size()])); + bitAssetData.setCurrentFeed(assetFeed); + bitAssetData.setCurrentFeedPublicationTime(publicationTime); + bitAssetData.setOptions(options); + bitAssetData.setForceSettledVolume(forceSettledVolume); + bitAssetData.setIsPredictionMarket(isPredictionMarket); + bitAssetData.setSettlementPrice(settlementPrice); + bitAssetData.setSettlementFund(settlementFund); + return bitAssetData; + } + } } 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/Options.java b/graphenej/src/main/java/cy/agorise/graphenej/models/Options.java new file mode 100644 index 0000000..e9ea3ea --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/Options.java @@ -0,0 +1,15 @@ +package cy.agorise.graphenej.models; + +/** + * Class used to represent the 'options' object returned inside the response obtained after + * querying for an object of type 'asset_bitasset_data' (2.4.x) + */ + +public class Options { + private long feed_lifetime_sec; + private long minimum_feeds; + private long force_settlement_delay_sec; + private long force_settlement_offset_percent; + private long maximum_force_settlement_volume; + private String short_backing_asset; +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/models/ReportedAssetFeed.java b/graphenej/src/main/java/cy/agorise/graphenej/models/ReportedAssetFeed.java new file mode 100644 index 0000000..ecde713 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/models/ReportedAssetFeed.java @@ -0,0 +1,78 @@ +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.JsonParseException; + +import java.lang.reflect.Type; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.Util; + +/** + * Witness-provided asset price feed + */ + +public class ReportedAssetFeed { + private UserAccount reporter; + private AssetFeed assetFeed; + private Date reportedDate; + + public ReportedAssetFeed(UserAccount userAccount, Date date, AssetFeed assetFeed){ + this.reporter = userAccount; + this.reportedDate = date; + this.assetFeed = assetFeed; + } + + public UserAccount getReporter() { + return reporter; + } + + public void setReporter(UserAccount reporter) { + this.reporter = reporter; + } + + public AssetFeed getAssetFeed() { + return assetFeed; + } + + public void setAssetFeed(AssetFeed assetFeed) { + this.assetFeed = assetFeed; + } + + public Date getReportedDate() { + return reportedDate; + } + + public void setReportedDate(Date reportedDate) { + this.reportedDate = reportedDate; + } + + public static class ReportedAssetFeedDeserializer implements JsonDeserializer { + + @Override + public ReportedAssetFeed deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonArray array = json.getAsJsonArray(); + String userId = array.get(0).getAsString(); + JsonArray subArray = (JsonArray) array.get(1); + String dateString = subArray.get(0).getAsString(); + + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(Util.TIME_DATE_FORMAT); + Date reportDate = null; + try { + reportDate = simpleDateFormat.parse(dateString); + } catch (ParseException e) { + e.printStackTrace(); + } + + AssetFeed assetFeed = context.deserialize(subArray.get(1), AssetFeed.class); + UserAccount userAccount = new UserAccount(userId); + return new ReportedAssetFeed(userAccount, reportDate, assetFeed); + } + } +} 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/network/FullNode.java b/graphenej/src/main/java/cy/agorise/graphenej/network/FullNode.java new file mode 100644 index 0000000..4226b23 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/network/FullNode.java @@ -0,0 +1,121 @@ +package cy.agorise.graphenej.network; + +import cy.agorise.graphenej.stats.ExponentialMovingAverage; + +/** + * Class that represents a full node and is used to keep track of its round-trip time measured in milliseconds. + */ +public class FullNode implements Comparable { + + private String mUrl; + private ExponentialMovingAverage mLatency; + private boolean isConnected; + private boolean isRemoved; + + private FullNode(){} + + /** + * Constructor used to specify both the node URL and the alpha parameter that one wishes to set the + * exponential moving average with. + *

+ * The alpha parameter represents the degree of weighting decrease, and can be specified as any value + * between 0 and 1. A higher alpha discounts older observations faster. + * + * @param url The node URL. + * @param alpha The alpha parameter used to compute the exponential moving average. + */ + public FullNode(String url, double alpha){ + mLatency = new ExponentialMovingAverage(alpha); + mUrl = url; + } + + /** + * Constructor used to specify only the node URL. + *

+ * The alpha parameter is set to the value specified at {@link ExponentialMovingAverage#DEFAULT_ALPHA} + * + * @param url The node URL. + */ + public FullNode(String url){ + this(url, ExponentialMovingAverage.DEFAULT_ALPHA); + } + + /** + * Full node URL getter + * @return + */ + public String getUrl() { + return mUrl; + } + + /** + * Full node URL setter + * @param mUrl + */ + public void setUrl(String mUrl) { + this.mUrl = mUrl; + } + + /** + * + * @return The latest latency average value. If no measurement has been taken yet, the + * maximum allows value of a long primitive, or 263-1 will be returned. + */ + public double getLatencyValue() { + double average = mLatency.getAverage(); + if(average == 0){ + return Long.MAX_VALUE; + }else{ + return average; + } + } + + public boolean isConnected() { + return isConnected; + } + + public void setConnected(boolean connected) { + isConnected = connected; + } + + public boolean isRemoved() { + return isRemoved; + } + + public void setRemoved(boolean removed) { + isRemoved = removed; + } + + /** + * Method that updates the mLatency average with a new value. + * @param latency Most recent mLatency sample to be added to the exponential average + */ + public void addLatencyValue(double latency) { + this.mLatency.updateValue(latency); + } + + @Override + public int compareTo(Object o) { + FullNode node = (FullNode) o; + double doubleResult = getLatencyValue() - node.getLatencyValue(); + if(doubleResult > 0) + return 1; + else if(doubleResult < 0) + return -1; + else + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FullNode fullNode = (FullNode) o; + return mUrl.equals(fullNode.getUrl()); + } + + @Override + public int hashCode() { + return mUrl.hashCode(); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java b/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java new file mode 100644 index 0000000..df0630b --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java @@ -0,0 +1,65 @@ +package cy.agorise.graphenej.network; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.PriorityBlockingQueue; + +public class LatencyNodeProvider implements NodeProvider { + + private PriorityBlockingQueue mFullNodeHeap; + + public LatencyNodeProvider(){ + mFullNodeHeap = new PriorityBlockingQueue<>(); + } + + @Override + public FullNode getBestNode() { + return mFullNodeHeap.peek(); + } + + @Override + public void addNode(FullNode fullNode) { + mFullNodeHeap.add(fullNode); + } + + @Override + public boolean updateNode(FullNode fullNode) { + mFullNodeHeap.remove(fullNode); + return mFullNodeHeap.offer(fullNode); + } + + /** + * Updates an existing node with the new latency value. + * + * @param fullNode Existing full node instance + * @param latency New latency measurement + * @return True if the node priority was updated successfully + */ + public boolean updateNode(FullNode fullNode, int latency){ + boolean existed = mFullNodeHeap.remove(fullNode); + if(existed){ + fullNode.addLatencyValue(latency); + return mFullNodeHeap.add(fullNode); + } + return false; + } + + @Override + public void removeNode(FullNode fullNode) { + mFullNodeHeap.remove(fullNode); + } + + @Override + public List getSortedNodes() { + FullNode[] nodeArray = mFullNodeHeap.toArray(new FullNode[mFullNodeHeap.size()]); + ArrayList nodeList = new ArrayList<>(); + for(FullNode fullNode : nodeArray){ + if(fullNode != null){ + nodeList.add(fullNode); + } + } + Collections.sort(nodeList); + return nodeList; + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java b/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java new file mode 100644 index 0000000..69b23b1 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java @@ -0,0 +1,210 @@ +package cy.agorise.graphenej.network; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.util.HashMap; +import java.util.List; + +import cy.agorise.graphenej.api.android.NetworkService; +import io.reactivex.subjects.PublishSubject; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; + +/** + * Class that encapsulates the node latency verification task + */ +public class NodeLatencyVerifier { + private final String TAG = this.getClass().getName(); + + private static final int DEFAULT_LATENCY_VERIFICATION_PERIOD = 5 * 1000; + + // Variable used to store the list of nodes that should be verified + private List mNodeList; + + // Variable used to store the desired verification period + private long verificationPeriod; + + // Subject used to publish the result to interested parties + private PublishSubject subject = PublishSubject.create(); + + private HashMap nodeURLMap = new HashMap<>(); + + // Map used to store the first timestamp required for a RTT (Round Trip Time) measurement. + // If: + // RTT = t2 - t1 + // This map will hold the value of t1 for each one of the nodes to be measured. + private HashMap timestamps = new HashMap<>(); + + private HashMap requestMap = new HashMap<>(); + + private Handler mHandler = new Handler(Looper.getMainLooper()); + + private OkHttpClient client; + + public NodeLatencyVerifier(List nodes){ + this(nodes, DEFAULT_LATENCY_VERIFICATION_PERIOD); + } + + public NodeLatencyVerifier(List nodes, long period){ + mNodeList = nodes; + verificationPeriod = period; + } + + /** + * Method used to start the latency verification task. + *

+ * The returning object can be used for interested parties to receive constant updates + * regarding new latency measurements for every full node. + *

+ * @return A {@link PublishSubject} class instance. + */ + public PublishSubject start(){ + mHandler.post(mVerificationTask); + return subject; + } + + /** + * Method used to cancel the verification task. + */ + public void stop(){ + mHandler.removeCallbacks(mVerificationTask); + } + + /** + * Node latency verification task. + */ + private final Runnable mVerificationTask = new Runnable() { + @Override + public void run() { + for(FullNode fullNode : mNodeList){ + long before = System.currentTimeMillis(); + timestamps.put(fullNode, before); + + // We want to reuse the same OkHttpClient instance if possible + if(client == null) client = new OkHttpClient(); + + // Same thing with the Request instance, we want to reuse them. But since + // we might have one request per node, we keep them in a map. + Request request; + if(requestMap.containsKey(fullNode.getUrl())){ + request = requestMap.get(fullNode.getUrl()); + }else{ + // If the map had no entry for the request we want, we create one + // and add it to the map. + request = new Request.Builder().url(fullNode.getUrl()).build(); + requestMap.put(fullNode.getUrl(), request); + } + + String normalURL = fullNode.getUrl().replace("wss://", "https://"); + HttpUrl key = HttpUrl.parse(normalURL); + if(!nodeURLMap.containsKey(key)){ + nodeURLMap.put(key, fullNode); + } + client.newWebSocket(request, mWebSocketListener); + } + mHandler.postDelayed(this, verificationPeriod); + } + }; + + /** + * Listener that will be called upon a server response. + */ + private WebSocketListener mWebSocketListener = new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + super.onOpen(webSocket, response); + handleResponse(webSocket, response); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + super.onFailure(webSocket, t, response); + handleResponse(webSocket, response); + } + + /** + * Method used to handle the node's first response. The idea here is to obtain + * the RTT (Round Trip Time) measurement and publish it using the PublishSubject. + * + * @param webSocket WebSocket instance + * @param response Response instance + */ + private void handleResponse(WebSocket webSocket, Response response){ + synchronized (this){ + // Obtaining the HttpUrl instance that was previously used as a key + HttpUrl url = webSocket.request().url(); + if(nodeURLMap.containsKey(url)){ + FullNode fullNode = nodeURLMap.get(url); + long delay; + + if(response == null) { + // There is no internet connection, or the node is unreachable. We are just + // putting an artificial delay. + delay = Long.MAX_VALUE; + } else { + long after = System.currentTimeMillis(); + long before = timestamps.get(fullNode); + delay = after - before; + } + if(fullNode != null){ + fullNode.addLatencyValue(delay); + subject.onNext(fullNode); + }else{ + Log.w(TAG,"Could not extract FullNode instance from the map"); + } + }else{ + // We cannot properly handle a response to a request whose + // URL was not registered at the nodeURLMap. This is because without this, + // we cannot know to which node this response corresponds. This should not happen. + Log.e(TAG,"nodeURLMap does not contain url: "+url); + for(HttpUrl key : nodeURLMap.keySet()){ + Log.e(TAG,"> "+key); + } + } + webSocket.close(NetworkService.NORMAL_CLOSURE_STATUS, null); + } + } + }; + + /** + * Updates the 'isConnected' attribute of a specific node. + * @param fullNode The node we want to update. + */ + public void updateActiveNodeInformation(FullNode fullNode){ + for(FullNode node : mNodeList){ + if(node.equals(fullNode)){ + node.setConnected(fullNode.isConnected()); + } + } + } + + /** + * Removes the given node from the nodes list + * @param fullNode The node to remove + */ + public void removeNode(FullNode fullNode){ + for(FullNode node : mNodeList){ + if(node.equals(fullNode)){ + mNodeList.remove(node); + + String normalURL = node.getUrl().replace("wss://", "https://"); + HttpUrl key = HttpUrl.parse(normalURL); + nodeURLMap.remove(key); + + node.setRemoved(true); + subject.onNext(node); + break; + } + } + } + + public List getNodeList(){ + return mNodeList; + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/network/NodeProvider.java b/graphenej/src/main/java/cy/agorise/graphenej/network/NodeProvider.java new file mode 100644 index 0000000..400158e --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/network/NodeProvider.java @@ -0,0 +1,44 @@ +package cy.agorise.graphenej.network; + +import java.util.List; + +/** + * Interface used to describe the high level characteristics of a class that will + * hold and manage a list of {@link FullNode} instances. + * + * The idea is that the class implementing this interface should provide node instances + * and thus URLs for the {@link cy.agorise.graphenej.api.android.NetworkService} with + * different sorting heuristics. + */ +public interface NodeProvider { + + /** + * Returns the node with the best characteristics. Returns null if there is no {@link FullNode} + * @return A FullNode instance + */ + FullNode getBestNode(); + + /** + * Adds a new node to the queue + * @param fullNode {@link FullNode} instance to add. + */ + void addNode(FullNode fullNode); + + /** + * Updates the rating of a specific node that is already in the NodeProvider + * @param fullNode The node tu update + */ + boolean updateNode(FullNode fullNode); + + /** + * Removes the given node from the nodes list + * @param fullNode The node to remove + */ + void removeNode(FullNode fullNode); + + /** + * Returns an ordered list of {@link FullNode} instances. + * @return The sorted list of nodes. + */ + List getSortedNodes(); +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/operations/AccountUpgradeOperation.java b/graphenej/src/main/java/cy/agorise/graphenej/operations/AccountUpgradeOperation.java new file mode 100644 index 0000000..d2681e4 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/operations/AccountUpgradeOperation.java @@ -0,0 +1,145 @@ +package cy.agorise.graphenej.operations; + +import com.google.common.primitives.Bytes; +import com.google.gson.GsonBuilder; +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 com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; + +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.BaseOperation; +import cy.agorise.graphenej.OperationType; +import cy.agorise.graphenej.UserAccount; + +/** + * Created by henry on 19/5/2018. + */ + +public class AccountUpgradeOperation extends BaseOperation { + + private static final String KEY_ACCOUNT = "account_to_upgrade"; + private static final String KEY_UPGRADE = "upgrade_to_lifetime_member"; + + private AssetAmount fee; + private UserAccount accountToUpgrade; + private boolean upgradeToLifeTimeMember; + + public AccountUpgradeOperation(UserAccount accountToUpgrade, boolean upgradeToLifeTimeMember) { + super(OperationType.ACCOUNT_UPGRADE_OPERATION); + this.accountToUpgrade = accountToUpgrade; + this.upgradeToLifeTimeMember = upgradeToLifeTimeMember; + } + + public AccountUpgradeOperation(UserAccount accountToUpgrade, boolean upgradeToLifeTimeMember, AssetAmount fee) { + super(OperationType.ACCOUNT_UPGRADE_OPERATION); + this.accountToUpgrade = accountToUpgrade; + this.upgradeToLifeTimeMember = upgradeToLifeTimeMember; + this.fee = fee; + } + + public AssetAmount getFee() { + return fee; + } + + public UserAccount getAccountToUpgrade() { + return accountToUpgrade; + } + + public void setAccountToUpgrade(UserAccount accountToUpgrade) { + this.accountToUpgrade = accountToUpgrade; + } + + public boolean isUpgradeToLifeTimeMember() { + return upgradeToLifeTimeMember; + } + + public void setUpgradeToLifeTimeMember(boolean upgradeToLifeTimeMember) { + this.upgradeToLifeTimeMember = upgradeToLifeTimeMember; + } + + @Override + public void setFee(AssetAmount assetAmount) { + this.fee = assetAmount; + } + + @Override + public byte[] toBytes() { + byte[] feeBytes = fee.toBytes(); + byte[] accountBytes = accountToUpgrade.toBytes(); + byte[] upgradeToLifeTimeMemberBytes = this.upgradeToLifeTimeMember ? new byte[]{ 0x1 } : new byte[]{ 0x0 }; + byte[] extensions = this.extensions.toBytes(); + return Bytes.concat(feeBytes, accountBytes, upgradeToLifeTimeMemberBytes, extensions); + } + + @Override + public String toJsonString() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(AccountUpgradeOperation.class, new AccountUpgradeSerializer()); + return gsonBuilder.create().toJson(this); + } + + @Override + public JsonElement toJsonObject() { + JsonArray array = new JsonArray(); + array.add(this.getId()); + JsonObject jsonObject = new JsonObject(); + if(fee != null) + jsonObject.add(KEY_FEE, fee.toJsonObject()); + jsonObject.addProperty(KEY_ACCOUNT, accountToUpgrade.getObjectId()); + jsonObject.addProperty(KEY_UPGRADE, this.upgradeToLifeTimeMember ? "true" : "false"); + jsonObject.add(KEY_EXTENSIONS, new JsonArray()); + array.add(jsonObject); + return array; + } + + public static class AccountUpgradeSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(AccountUpgradeOperation accountUpgrade, Type type, JsonSerializationContext jsonSerializationContext) { + return accountUpgrade.toJsonObject(); + } + } + + + public static class AccountUpgradeDeserializer implements JsonDeserializer { + + @Override + public AccountUpgradeOperation deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if(json.isJsonArray()){ + // This block is used just to check if we are in the first step of the deserialization + // when we are dealing with an array. + JsonArray serializedAccountUpgrade = json.getAsJsonArray(); + if(serializedAccountUpgrade.get(0).getAsInt() != OperationType.ACCOUNT_UPGRADE_OPERATION.ordinal()){ + // If the operation type does not correspond to a transfer operation, we return null + return null; + }else{ + // Calling itself recursively, this is only done once, so there will be no problems. + return context.deserialize(serializedAccountUpgrade.get(1), AccountUpgradeOperation.class); + } + }else{ + JsonObject jsonObject = json.getAsJsonObject(); + + // Deserializing AssetAmount objects + AssetAmount fee = context.deserialize(jsonObject.get(KEY_FEE), AssetAmount.class); + + // Deserializing UserAccount objects + UserAccount accountToUpgrade = new UserAccount(jsonObject.get(KEY_ACCOUNT).getAsString()); + + boolean upgradeToLifeTime = jsonObject.get(KEY_UPGRADE).getAsBoolean(); + AccountUpgradeOperation upgrade = new AccountUpgradeOperation(accountToUpgrade, upgradeToLifeTime, fee); + + return upgrade; + } + } + } + + +} + diff --git a/graphenej/src/main/java/cy/agorise/graphenej/operations/AccountUpgradeOperationBuilder.java b/graphenej/src/main/java/cy/agorise/graphenej/operations/AccountUpgradeOperationBuilder.java new file mode 100644 index 0000000..d65c3aa --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/operations/AccountUpgradeOperationBuilder.java @@ -0,0 +1,46 @@ +package cy.agorise.graphenej.operations; + +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.errors.MalformedOperationException; + +/** + * Created by henry on 19/5/2018. + */ + +public class AccountUpgradeOperationBuilder extends BaseOperationBuilder { + + private UserAccount accountToUpgrade; + private AssetAmount fee; + private boolean isUpgrade = true; + + public AccountUpgradeOperationBuilder setAccountToUpgrade(UserAccount accountToUpgrade) { + this.accountToUpgrade = accountToUpgrade; + return this; + } + + public AccountUpgradeOperationBuilder setFee(AssetAmount fee) { + this.fee = fee; + return this; + } + + public AccountUpgradeOperationBuilder setIsUpgrade(Boolean isUpgrade) { + this.isUpgrade = isUpgrade; + return this; + } + + @Override + public AccountUpgradeOperation build(){ + AccountUpgradeOperation accountUpgrade; + if(accountToUpgrade == null ){ + throw new MalformedOperationException("Missing account to upgrade information"); + } + + if(fee != null){ + accountUpgrade = new AccountUpgradeOperation(accountToUpgrade, isUpgrade, fee); + }else{ + accountUpgrade = new AccountUpgradeOperation(accountToUpgrade, isUpgrade); + } + return accountUpgrade; + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java b/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java new file mode 100644 index 0000000..bbd8946 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java @@ -0,0 +1,146 @@ +package cy.agorise.graphenej.operations; + +import com.google.common.primitives.Bytes; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.BaseOperation; +import cy.agorise.graphenej.HtlcHash; +import cy.agorise.graphenej.OperationType; +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.Util; + +public class CreateHtlcOperation extends BaseOperation { + static final String KEY_FROM = "from"; + static final String KEY_TO = "to"; + static final String KEY_AMOUNT = "amount"; + static final String KEY_PREIMAGE_HASH = "preimage_hash"; + static final String KEY_PREIMAGE_SIZE = "preimage_size"; + static final String KEY_CLAIM_PERIOD_SECONDS = "claim_period_seconds"; + + private AssetAmount fee; + private UserAccount from; + private UserAccount to; + private AssetAmount amount; + private HtlcHash preimageHash; + private short preimageSize; + private int claimPeriodSeconds; + + /** + * Public constructor + * + * @param fee The operation fee. + * @param from The source account. + * @param to The destination account. + * @param amount The amount to be traded. + * @param hash The pre-image hash. + * @param preimageSize The pre-image size. + * @param claimPeriodSeconds The claim period, in seconds. + */ + public CreateHtlcOperation(AssetAmount fee, UserAccount from, UserAccount to, AssetAmount amount, HtlcHash hash, short preimageSize, int claimPeriodSeconds) { + super(OperationType.HTLC_CREATE_OPERATION); + this.fee = fee; + this.from = from; + this.to = to; + this.amount = amount; + this.preimageHash = hash; + this.preimageSize = preimageSize; + this.claimPeriodSeconds = claimPeriodSeconds; + } + + @Override + public void setFee(AssetAmount newFee){ + this.fee = newFee; + } + + public AssetAmount getFee() { + return fee; + } + + public UserAccount getFrom() { + return from; + } + + public void setFrom(UserAccount from) { + this.from = from; + } + + public UserAccount getTo() { + return to; + } + + public void setTo(UserAccount to) { + this.to = to; + } + + public AssetAmount getAmount() { + return amount; + } + + public void setAmount(AssetAmount amount) { + this.amount = amount; + } + + public HtlcHash getPreimageHash() { + return preimageHash; + } + + public void setPreimageHash(HtlcHash preimageHash) { + this.preimageHash = preimageHash; + } + + public short getPreimageSize() { + return preimageSize; + } + + public void setPreimageSize(short preimageSize) { + this.preimageSize = preimageSize; + } + + public int getClaimPeriodSeconds() { + return claimPeriodSeconds; + } + + public void setClaimPeriodSeconds(int claimPeriodSeconds) { + this.claimPeriodSeconds = claimPeriodSeconds; + } + + @Override + public byte[] toBytes() { + byte[] feeBytes = fee.toBytes(); + byte[] fromBytes = from.toBytes(); + byte[] toBytes = to.toBytes(); + byte[] amountBytes = amount.toBytes(); + byte[] htlcHashBytes = preimageHash.toBytes(); + byte[] preimageSizeBytes = Util.revertShort(preimageSize); + byte[] claimPeriodBytes = Util.revertInteger(claimPeriodSeconds); + byte[] extensionsBytes = extensions.toBytes(); + return Bytes.concat(feeBytes, fromBytes, toBytes, amountBytes, htlcHashBytes, preimageSizeBytes, claimPeriodBytes, extensionsBytes); + } + + @Override + public JsonElement toJsonObject() { + JsonArray array = new JsonArray(); + array.add(this.getId()); + JsonObject jsonObject = new JsonObject(); + jsonObject.add(KEY_FEE, fee.toJsonObject()); + jsonObject.addProperty(KEY_FROM, from.getObjectId()); + jsonObject.addProperty(KEY_TO, to.getObjectId()); + jsonObject.add(KEY_AMOUNT, amount.toJsonObject()); + jsonObject.add(KEY_PREIMAGE_HASH, preimageHash.toJsonObject()); + jsonObject.addProperty(KEY_PREIMAGE_SIZE, preimageSize); + jsonObject.addProperty(KEY_CLAIM_PERIOD_SECONDS, claimPeriodSeconds); + jsonObject.add(KEY_EXTENSIONS, new JsonArray()); + array.add(jsonObject); + return array; + } + + @Override + public String toJsonString() { + GsonBuilder gsonBuilder = new GsonBuilder(); + return gsonBuilder.create().toJson(this); + } +} diff --git a/graphenej/src/main/java/cy/agorise/graphenej/operations/RedeemHtlcOperation.java b/graphenej/src/main/java/cy/agorise/graphenej/operations/RedeemHtlcOperation.java new file mode 100644 index 0000000..0889cde --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/operations/RedeemHtlcOperation.java @@ -0,0 +1,111 @@ +package cy.agorise.graphenej.operations; + +import com.google.common.primitives.Bytes; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; + +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.BaseOperation; +import cy.agorise.graphenej.Htlc; +import cy.agorise.graphenej.OperationType; +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.Util; +import cy.agorise.graphenej.Varint; + +/** + * Class used to encapsulate the redeem_htlc operation. + */ +public class RedeemHtlcOperation extends BaseOperation { + static final String KEY_REDEEMER = "redeemer"; + static final String KEY_PREIMAGE = "preimage"; + static final String KEY_HTLC_ID = "htlc_id"; + + private AssetAmount fee; + private UserAccount redeemer; + private Htlc htlc; + private byte[] preimage; + + /** + * Public constructor + * + * @param fee The fee associated with this operation. + * @param redeemer The user account that will redeem the HTLC. + * @param htlc The existing HTLC operation. + */ + public RedeemHtlcOperation(AssetAmount fee, UserAccount redeemer, Htlc htlc, byte[] preimage) { + super(OperationType.HTLC_REDEEM_OPERATION); + this.fee = fee; + this.redeemer = redeemer; + this.htlc = htlc; + this.preimage = preimage; + } + + @Override + public void setFee(AssetAmount fee) { + this.fee = fee; + } + + public AssetAmount getFee(){ + return this.fee; + } + + public UserAccount getRedeemer() { + return redeemer; + } + + public void setRedeemer(UserAccount redeemer) { + this.redeemer = redeemer; + } + + public Htlc getHtlc() { + return htlc; + } + + public void setHtlc(Htlc htlc) { + this.htlc = htlc; + } + + @Override + public byte[] toBytes() { + byte[] feeBytes = this.fee.toBytes(); + byte[] htlcBytes = this.htlc.toBytes(); + byte[] redeemerBytes = this.redeemer.toBytes(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DataOutput out = new DataOutputStream(byteArrayOutputStream); + try{ + Varint.writeUnsignedVarLong(this.preimage.length, out); + } catch (IOException e) { + e.printStackTrace(); + } + byte[] preimageLength = byteArrayOutputStream.toByteArray(); + byte[] extensionsBytes = extensions.toBytes(); + return Bytes.concat(feeBytes, htlcBytes, redeemerBytes, preimageLength, this.preimage, extensionsBytes); + } + + @Override + public JsonElement toJsonObject() { + JsonArray array = new JsonArray(); + array.add(this.getId()); + JsonObject jsonObject = new JsonObject(); + jsonObject.add(KEY_FEE, fee.toJsonObject()); + jsonObject.addProperty(KEY_REDEEMER, this.redeemer.getObjectId()); + jsonObject.addProperty(KEY_PREIMAGE, Util.bytesToHex(this.preimage)); + jsonObject.addProperty(KEY_HTLC_ID, this.htlc.getObjectId()); + jsonObject.add(KEY_EXTENSIONS, new JsonArray()); + array.add(jsonObject); + return array; + } + + @Override + public String toJsonString() { + GsonBuilder gsonBuilder = new GsonBuilder(); + return gsonBuilder.create().toJson(this); + } +} 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/main/java/cy/agorise/graphenej/stats/ExponentialMovingAverage.java b/graphenej/src/main/java/cy/agorise/graphenej/stats/ExponentialMovingAverage.java new file mode 100644 index 0000000..65bec39 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/stats/ExponentialMovingAverage.java @@ -0,0 +1,49 @@ +package cy.agorise.graphenej.stats; + +/** + * Class used to compute the Exponential Moving Average of a sequence of values. + * For more details see here. + */ +public class ExponentialMovingAverage { + public static final double DEFAULT_ALPHA = 0.5; + private double alpha; + private Double accumulatedValue; + + /** + * Constructor, which takes only the alpha parameter as an argument. + * + * @param alpha The coefficient alpha represents the degree of weighting decrease, a constant + * smoothing factor between 0 and 1. A higher alpha discounts older observations faster. + */ + public ExponentialMovingAverage(double alpha) { + this.alpha = alpha; + } + + /** + * Method that updates the average with a new sample + * @param value New value + * @return The updated average value + */ + public double updateValue(double value) { + if (accumulatedValue == null) { + accumulatedValue = value; + return value; + } + double newValue = accumulatedValue + alpha * (value - accumulatedValue); + accumulatedValue = newValue; + return newValue; + } + + /** + * + * @return Returns the current average value + */ + public double getAverage(){ + return accumulatedValue == null ? 0 : accumulatedValue; + } + + public void setAlpha(double alpha){ + this.alpha = alpha; + this.accumulatedValue = null; + } +} \ No newline at end of file diff --git a/graphenej/src/test/java/cy/agorise/graphenej/AuthorityTest.java b/graphenej/src/test/java/cy/agorise/graphenej/AuthorityTest.java index 23109f1..f364fbc 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/AuthorityTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/AuthorityTest.java @@ -7,6 +7,7 @@ import org.junit.Test; import java.util.HashMap; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; /** * Created by nelson on 12/16/16. @@ -48,7 +49,7 @@ public class AuthorityTest { @Test public void equals() throws Exception { assertEquals("Equal authorities", authority, sameAuthority); - assertEquals("Different authorities ", authority, differentAuthority); + assertNotEquals("Different authorities ", authority, differentAuthority); assertEquals("Two public keys with the same public key should be equal", keyAuthority1, keyAuthority2); } 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/HtlcTest.java b/graphenej/src/test/java/cy/agorise/graphenej/HtlcTest.java new file mode 100644 index 0000000..c5c02c7 --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/HtlcTest.java @@ -0,0 +1,26 @@ +package cy.agorise.graphenej; + +import org.junit.Assert; +import org.junit.Test; + +public class HtlcTest { + private final Htlc htlc = new Htlc("1.16.124"); + + @Test + public void testByteSerialization(){ + Htlc htlc1 = new Htlc("1.16.1"); + Htlc htlc2 = new Htlc("1.16.100"); + Htlc htlc3 = new Htlc("1.16.500"); + Htlc htlc4 = new Htlc("1.16.1000"); + + byte[] expected_1 = Util.hexToBytes("01"); + byte[] expected_2 = Util.hexToBytes("64"); + byte[] expected_3 = Util.hexToBytes("f403"); + byte[] expected_4 = Util.hexToBytes("e807"); + + Assert.assertArrayEquals(expected_1, htlc1.toBytes()); + Assert.assertArrayEquals(expected_2, htlc2.toBytes()); + Assert.assertArrayEquals(expected_3, htlc3.toBytes()); + Assert.assertArrayEquals(expected_4, htlc4.toBytes()); + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/TransactionTest.java b/graphenej/src/test/java/cy/agorise/graphenej/TransactionTest.java index d4dd064..8c34375 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/TransactionTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/TransactionTest.java @@ -13,6 +13,7 @@ import org.junit.Test; import java.io.IOException; import java.math.BigInteger; import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -24,7 +25,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; @@ -153,7 +153,8 @@ public class TransactionTest { PublicKey to2 = new PublicKey(ECKey.fromPublicOnly(new BrainKey(BILTHON_16_BRAIN_KEY, 0).getPublicKey())); // Creating memo - BigInteger nonce = BigInteger.ONE; + SecureRandom random = new SecureRandom(); + BigInteger nonce = BigInteger.valueOf(random.nextLong()); byte[] encryptedMessage = Memo.encryptMessage(sourcePrivateKey, to1, nonce, "another message"); Memo memo = new Memo(new Address(ECKey.fromPublicOnly(sourcePrivateKey.getPubKey())), new Address(to1.getKey()), nonce, encryptedMessage); @@ -346,4 +347,21 @@ public class TransactionTest { // Broadcasting transaction broadcastTransaction(sourcePrivateKey, operationList, listener, null); } + + @Test + public void testTransactionHash(){ + ArrayList operations = new ArrayList<>(); + TransferOperation transferOperation = new TransferOperationBuilder() + .setTransferAmount(new AssetAmount(UnsignedLong.valueOf("363"), new Asset("1.3.0"))) + .setFee(new AssetAmount(UnsignedLong.valueOf("10420"), new Asset("1.3.0"))) + .setSource(new UserAccount("1.2.1029856")) + .setDestination(new UserAccount("1.2.390320")) + .build(); + BlockData blockData = new BlockData(50885, 2948192884L, 1543548351); + operations.add(transferOperation); + Transaction transaction = new Transaction(blockData, operations); + byte[] testHash = transaction.getHash(); + // Making sure the generated hash matches the one we expect from the block explorer + Assert.assertArrayEquals(Util.hexToBytes("4fec588ccdd04daaf80666a3646a48b5189df041"), testHash); + } } \ No newline at end of file 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 5b76e68..cd7bab8 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/api/GetObjectsTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/api/GetObjectsTest.java @@ -11,6 +11,7 @@ import java.util.List; import cy.agorise.graphenej.Asset; import cy.agorise.graphenej.GrapheneObject; +import cy.agorise.graphenej.Price; import cy.agorise.graphenej.UserAccount; import cy.agorise.graphenej.interfaces.WitnessResponseListener; import cy.agorise.graphenej.models.BaseResponse; @@ -26,7 +27,8 @@ 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 final String bitAssetId = "2.4.13"; + private UserAccount ltmAccount = new UserAccount("1.2.99700"); + private final String[] bitAssetIds = new String[]{"2.4.21", "2.4.83"}; @Test public void testGetAsset(){ @@ -108,24 +110,87 @@ 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{ ArrayList ids = new ArrayList<>(); - ids.add(bitAssetId); + for(String bitAssetId : bitAssetIds){ + ids.add(bitAssetId); + } mWebSocket.addListener(new GetObjects(ids, new WitnessResponseListener() { @Override public void onSuccess(WitnessResponse response) { System.out.println("onSuccess"); - List list = (List) response.result; - BitAssetData bitAssetData = (BitAssetData) list.get(0); - System.out.println("feed time: " + bitAssetData.current_feed_publication_time); + List list = (List) response.result; + System.out.println("Response array length: "+list.size()); + BitAssetData bitAssetData1 = list.get(0); + BitAssetData bitAssetData2 = list.get(1); + + Price price1 = bitAssetData1.getCurrentFeed().getSettlementPrice(); + Price price2 = bitAssetData2.getCurrentFeed().getSettlementPrice(); + + System.out.println("Bitasset data 1"); + System.out.println("Price 1: "+price1.toString()); + + System.out.println("Bitasset data 2"); + System.out.println("Price 1: "+price2.toString()); + + synchronized (GetObjectsTest.this){ + GetObjectsTest.this.notifyAll(); + } } @Override public void onError(BaseResponse.Error error) { System.out.println("onError"); + synchronized (GetObjectsTest.this){ + GetObjectsTest.this.notifyAll(); + } } })); 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/network/FullNodeTest.java b/graphenej/src/test/java/cy/agorise/graphenej/network/FullNodeTest.java new file mode 100644 index 0000000..cb0897d --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/network/FullNodeTest.java @@ -0,0 +1,30 @@ +package cy.agorise.graphenej.network; + +import junit.framework.Assert; + +import org.junit.Test; + +public class FullNodeTest { + + @Test + public void testFullNodeComparable(){ + FullNode nodeA = new FullNode("wss://dummy"); + FullNode nodeB = new FullNode("wss://dummy"); + FullNode nodeC = new FullNode("wss://dummy"); + nodeA.addLatencyValue(100); + nodeB.addLatencyValue(200); + nodeC.addLatencyValue(100); + Assert.assertTrue("Makes sure the node nodeA.compareTo(nodeB) returns a negative value", nodeA.compareTo(nodeB) < 0); + Assert.assertTrue("Makes sure nodeA.compareTo(nodeB) returns zero", nodeA.compareTo(nodeC) == 0); + Assert.assertTrue("Makes sure nodeB.compareTo(nodeA) returns a positive value", nodeB.compareTo(nodeA) > 0); + } + + @Test + public void testFullNodeAverageLatency(){ + FullNode fullNode = new FullNode("wss://dummy", 0.5); + fullNode.addLatencyValue(100); + Assert.assertEquals(100.0, fullNode.getLatencyValue()); + fullNode.addLatencyValue(50); + Assert.assertEquals(75.0, fullNode.getLatencyValue()); + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/network/LatencyNodeProviderTest.java b/graphenej/src/test/java/cy/agorise/graphenej/network/LatencyNodeProviderTest.java new file mode 100644 index 0000000..5f8c9a3 --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/network/LatencyNodeProviderTest.java @@ -0,0 +1,179 @@ +package cy.agorise.graphenej.network; + +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class LatencyNodeProviderTest { + private FullNode nodeA, nodeB, nodeC; + private LatencyNodeProvider latencyNodeProvider; + + private void setupTestNodes(){ + // Creating 3 nodes with different latencies + nodeA = new FullNode("wss://nodeA"); + nodeB = new FullNode("wss://nodeB"); + nodeC = new FullNode("wss://nodeC"); + + // Adding latencies measurements + nodeA.addLatencyValue(100); + nodeB.addLatencyValue(50); + nodeC.addLatencyValue(20); + + // Creating a node provider and adding the nodes created previously + latencyNodeProvider = new LatencyNodeProvider(); + latencyNodeProvider.addNode(nodeC); + latencyNodeProvider.addNode(nodeA); + latencyNodeProvider.addNode(nodeB); + } + + @Test + public void testSortedList(){ + setupTestNodes(); + + // Confirming that the getSortedNodes gives us a sorted list of nodes in increasing latency order + List fullNodeList = latencyNodeProvider.getSortedNodes(); + assertEquals(nodeC, fullNodeList.get(0)); + assertEquals(nodeB, fullNodeList.get(1)); + assertEquals(nodeA, fullNodeList.get(2)); + + // Adding more nodes with different latencies measurements + FullNode nodeD = new FullNode("wss://nodeD"); + FullNode nodeE = new FullNode("wss://nodeE"); + FullNode nodeF = new FullNode("wss://nodef"); + + // Adding latencies measurements + nodeD.addLatencyValue(900); + nodeE.addLatencyValue(1); + nodeF.addLatencyValue(1500); + + // Updating the LatencyNodeProvider + latencyNodeProvider.updateNode(nodeD); + latencyNodeProvider.updateNode(nodeE); + latencyNodeProvider.updateNode(nodeF); + + FullNode bestNode = latencyNodeProvider.getBestNode(); + // Checking for best node + assertEquals("Verifying that the nodeE is the best now", nodeE, bestNode); + fullNodeList = latencyNodeProvider.getSortedNodes(); + FullNode worstNode = fullNodeList.get(fullNodeList.size() - 1); + // Checking for worst node + assertEquals("Verifying that the nodeF is the worst now", nodeF, worstNode); + } + + @Test + public void testScoreUpdate(){ + setupTestNodes(); + + // Confirming that the best node is nodeC + FullNode bestNode = latencyNodeProvider.getBestNode(); + assertEquals("Check that the best node is nodeC", nodeC, bestNode); + + // Improving nodeA score by feeding it with new better latency measurements + latencyNodeProvider.updateNode(nodeA, 10); + latencyNodeProvider.updateNode(nodeA, 10); + latencyNodeProvider.updateNode(nodeA, 10); + latencyNodeProvider.updateNode(nodeA, 10); + + // Updating the nodeA position in the provider + latencyNodeProvider.updateNode(nodeA); + bestNode = latencyNodeProvider.getBestNode(); + System.out.println("Best node latency after update: "+bestNode.getLatencyValue()); + assertEquals("Check that the best node now is the nodeA", nodeA, bestNode); + } + + @Test + public void testLargeNumbers(){ + setupTestNodes(); + nodeA.addLatencyValue(Long.MAX_VALUE); + latencyNodeProvider.updateNode(nodeA); + FullNode best = latencyNodeProvider.getBestNode(); + assertEquals(nodeC, best); + } + + @Test + public void realisticSituationTest(){ + FullNode node1 = new FullNode("wss://fi.bts.dcn.cx/ws"); + FullNode node2 = new FullNode("wss://mx.palmpay.io/ws"); + FullNode node3 = new FullNode("wss://miami.bitshares.apasia.tech/ws"); + FullNode node4 = new FullNode("wss://valley.bitshares.apasia.tech/ws"); + FullNode node5 = new FullNode("wss://atlanta.bitshares.apasia.tech/ws"); + FullNode node6 = new FullNode("wss://dallas.bitshares.apasia.tech/ws"); + FullNode node7 = new FullNode("wss://eu-west-2.bts.crypto-bridge.org"); + FullNode node8 = new FullNode("wss://england.bitshares.apasia.tech/ws"); + FullNode node9 = new FullNode("wss://eu-west-1.bts.crypto-bridge.org"); + FullNode node11 = new FullNode("wss://netherlands.bitshares.apasia.tech/ws"); + FullNode node12 = new FullNode("wss://api.bts.blckchnd.com"); + FullNode node13 = new FullNode("wss://bitshares.nu/ws"); + FullNode node14 = new FullNode("wss://bitshares.openledger.info/ws"); + FullNode node15 = new FullNode("wss://citadel.li/node"); + FullNode node16 = new FullNode("wss://api-ru.bts.blckchnd.com"); + FullNode node17 = new FullNode("wss://dex.rnglab.org"); + FullNode node18 = new FullNode("wss://nl.palmpay.io/ws"); + FullNode node19 = new FullNode("wss://bitshares.crypto.fans/ws"); + FullNode node20 = new FullNode("wss://bit.btsabc.org/ws"); + + LatencyNodeProvider provider = new LatencyNodeProvider(); + provider.addNode(node1); + provider.addNode(node2); + provider.addNode(node3); + provider.addNode(node4); + provider.addNode(node5); + provider.addNode(node6); + provider.addNode(node7); + provider.addNode(node8); + provider.addNode(node9); + provider.addNode(node11); + provider.addNode(node12); + provider.addNode(node13); + provider.addNode(node14); + provider.addNode(node15); + provider.addNode(node16); + provider.addNode(node17); + provider.addNode(node18); + provider.addNode(node19); + provider.addNode(node20); + + node3.addLatencyValue(458.41); + node4.addLatencyValue(458.40); + node5.addLatencyValue(620.12); + node6.addLatencyValue(682.64); + node7.addLatencyValue(842.88); + node8.addLatencyValue(842.05); + node9.addLatencyValue(911.38); + node11.addLatencyValue(930.58); + node12.addLatencyValue(1002.27); + node13.addLatencyValue(1069.96); + node14.addLatencyValue(1060.20); + node15.addLatencyValue(1025.14); + node16.addLatencyValue(1060.55); + node17.addLatencyValue(1001.44); + node18.addLatencyValue(1036.69); + node19.addLatencyValue(1047.19); + node20.addLatencyValue(1286.89); + + provider.updateNode(node1); + provider.updateNode(node2); + provider.updateNode(node3); + provider.updateNode(node4); + provider.updateNode(node5); + provider.updateNode(node6); + provider.updateNode(node7); + provider.updateNode(node8); + provider.updateNode(node9); + provider.updateNode(node11); + provider.updateNode(node12); + provider.updateNode(node13); + provider.updateNode(node14); + provider.updateNode(node15); + provider.updateNode(node16); + provider.updateNode(node17); + provider.updateNode(node18); + provider.updateNode(node19); + provider.updateNode(node20); + + FullNode best = provider.getBestNode(); + assertEquals("Expects node4 to be the best", node4, best); + } +} 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..ac10cef 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; @@ -56,6 +57,14 @@ public class MemoTest { destinationAddress = new Address(publicKey.getKey()); } + @Test + public void canObtainSharedSecret(){ + byte[] secret1 = sourcePrivate.getPubKeyPoint().multiply(destinationPrivate.getPrivKey()).normalize().getXCoord().getEncoded(); + byte[] secret2 = destinationPrivate.getPubKeyPoint().multiply(sourcePrivate.getPrivKey()).normalize().getXCoord().getEncoded(); + System.out.println(String.format("Secret 1: %s, Secret 2: %s", Util.bytesToHex(secret1), Util.bytesToHex(secret2))); + Assert.assertArrayEquals(secret1, secret2); + } + @Test public void shouldMatchPredefinedCiphertext(){ byte[] encrypted = Memo.encryptMessage(sourcePrivate, destinationAddress, shortEncryptedMessageNonce, shortMessage); @@ -117,6 +126,19 @@ public class MemoTest { } } + @Test + public void shouldDecryptOwnMessage(){ + try{ + BigInteger nonce = new BigInteger("123456789"); + byte[] encrypted = Memo.encryptMessage(sourcePrivate, destinationAddress, nonce, longerMessage); + String decrypted = Memo.decryptMessage(sourcePrivate, destinationAddress, nonce, encrypted); + System.out.println("Decrypted: "+decrypted); + Assert.assertEquals(longerMessage, decrypted); + }catch (ChecksumException e) { + e.printStackTrace(); + } + } + @Test(expected = ChecksumException.class) public void shouldThrowException() throws ChecksumException { byte[] corrupted = Memo.encryptMessage(sourcePrivate, destinationAddress, longEncryptedMessageNonce, longerMessage); diff --git a/graphenej/src/test/java/cy/agorise/graphenej/operations/AccountUpgradeOperationTest.java b/graphenej/src/test/java/cy/agorise/graphenej/operations/AccountUpgradeOperationTest.java new file mode 100644 index 0000000..48841bd --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/operations/AccountUpgradeOperationTest.java @@ -0,0 +1,58 @@ +package cy.agorise.graphenej.operations; + +import com.google.common.primitives.Bytes; +import com.google.common.primitives.UnsignedLong; + +import org.bitcoinj.core.ECKey; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; + +import cy.agorise.graphenej.Asset; +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.BaseOperation; +import cy.agorise.graphenej.BlockData; +import cy.agorise.graphenej.BrainKey; +import cy.agorise.graphenej.Chains; +import cy.agorise.graphenej.Transaction; +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.Util; + +public class AccountUpgradeOperationTest { + private static final String BILTHON_16_BRAIN_KEY = "SOAPILY GASSING FIFIE OZONATE WHYO TOPLINE PRISMY ZEUGMA GLOTTIC DAVEN CORODY PFUI"; + + private final Asset CORE = new Asset("1.3.0"); + + // This was obtained by calling the 'serialized_transaction' command of the cli_wallet utility and stripping it away + // from its signature. + // + // Ex: + // >>> serialize_transaction {"expiration":"2017-04-18T21:42:47","signatures":["207dd63cb89d05266ed4af0935270f269839154d5f04229041997e6c98f10bba21752d6638eb7539c69fb22874fb1e39aad00a5eed6dd5e81e14845e79b60a0590"],"operations":[[8,{"fee":{"amount":69470219,"asset_id":"1.3.0"},"account_to_upgrade":"1.2.143569","upgrade_to_lifetime_member":"true","extensions":[]}]],"extensions":[],"ref_block_num":3703,"ref_block_prefix":2015738269} + private final String SERIALIZED_TX = "770e9db925785788f65801080b0824040000000000d1e108010000"; + + @Test + public void testOperationSerialization(){ + AccountUpgradeOperationBuilder builder = new AccountUpgradeOperationBuilder() + .setAccountToUpgrade(new UserAccount("1.2.143569")) + .setFee(new AssetAmount(UnsignedLong.valueOf(69470219), CORE)) + .setIsUpgrade(true); + + AccountUpgradeOperation upgradeOperation = builder.build(); + + ArrayList operations = new ArrayList<>(); + operations.add(upgradeOperation); + ECKey privateKey = new BrainKey(BILTHON_16_BRAIN_KEY, 0).getPrivateKey(); + BlockData blockData = new BlockData(3703, 2015738269, 1492551764); + + Transaction tx = new Transaction(privateKey, blockData, operations); + + // Serialized transaction + byte[] serialized = tx.toBytes(); + + // The expected serialized transaction is a concatenation of the chain id + the serialized tx + byte[] expectedTx = Bytes.concat(Util.hexToBytes(Chains.BITSHARES.CHAIN_ID),Util.hexToBytes(SERIALIZED_TX)); + + Assert.assertArrayEquals(expectedTx, serialized); + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java new file mode 100644 index 0000000..4c38531 --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java @@ -0,0 +1,114 @@ +package cy.agorise.graphenej.operations; + +import com.google.common.primitives.Bytes; +import com.google.common.primitives.UnsignedLong; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import org.junit.Assert; +import org.junit.Test; + +import java.security.NoSuchAlgorithmException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.TimeZone; + +import cy.agorise.graphenej.Asset; +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.BaseOperation; +import cy.agorise.graphenej.BlockData; +import cy.agorise.graphenej.Chains; +import cy.agorise.graphenej.HtlcHash; +import cy.agorise.graphenej.HtlcHashType; +import cy.agorise.graphenej.Transaction; +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.Util; + +public class CreateHtlcOperationTest { + private final String SERIALIZED_OP = "0000000000000000007b7c80241100000000000000a06e327ea7388c18e4740e350ed4e60f2e04fc41c8007800000000"; + private final String SERIALIZED_TX = "f68585abf4dce7c8045701310000000000000000007b7c80241100000000000000a06e327ea7388c18e4740e350ed4e60f2e04fc41c800780000000000"; + private final String JSON_SERIALIZED_TX = "{\"expiration\":\"2016-04-06T08:29:27\",\"extensions\":[],\"operations\":[[49,{\"amount\":{\"amount\":1123456,\"asset_id\":\"1.3.0\"},\"claim_period_seconds\":120,\"extensions\":[],\"fee\":{\"amount\":0,\"asset_id\":\"1.3.0\"},\"from\":\"1.2.123\",\"preimage_hash\":[0,\"a06e327ea7388c18e4740e350ed4e60f2e04fc41\"],\"preimage_size\":200,\"to\":\"1.2.124\"}]],\"ref_block_num\":34294,\"ref_block_prefix\":3707022213,\"signatures\":[]}"; + private final String PREIMAGE_HEX = "666f6f626172"; + private final String HASH_RIPEMD160 = "a06e327ea7388c18e4740e350ed4e60f2e04fc41"; + private final String HASH_SHA1 = "8843d7f92416211de9ebb963ff4ce28125932878"; + private final String HASH_SHA256 = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"; + + private final Asset CORE = new Asset("1.3.0"); + + private CreateHtlcOperation buildCreateHtlcOperation() throws NoSuchAlgorithmException { + UserAccount from = new UserAccount("1.2.123"); + UserAccount to = new UserAccount("1.2.124"); + AssetAmount fee = new AssetAmount(UnsignedLong.valueOf(0), CORE); + AssetAmount amount = new AssetAmount(UnsignedLong.valueOf(1123456), CORE); + byte[] hashBytes = Util.htlcHash("foobar".getBytes(), HtlcHashType.RIPEMD160); + HtlcHash preimageHash = new HtlcHash(HtlcHashType.RIPEMD160, hashBytes); + return new CreateHtlcOperation(fee, from, to, amount, preimageHash, (short) 200, 120); + } + + @Test + public void testRipemd160(){ + try { + byte[] hashRipemd160 = Util.htlcHash(Util.hexToBytes(PREIMAGE_HEX), HtlcHashType.RIPEMD160); + String hexHash = Util.bytesToHex(hashRipemd160); + Assert.assertEquals(HASH_RIPEMD160, hexHash); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + + @Test + public void testSha1(){ + try { + byte[] hashSha1 = Util.htlcHash(Util.hexToBytes(PREIMAGE_HEX), HtlcHashType.SHA1); + String hexHash = Util.bytesToHex(hashSha1); + Assert.assertEquals(HASH_SHA1, hexHash); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + + @Test + public void testSha256(){ + try { + byte[] hashSha256 = Util.htlcHash(Util.hexToBytes(PREIMAGE_HEX), HtlcHashType.SHA256); + String hexHash = Util.bytesToHex(hashSha256); + Assert.assertEquals(HASH_SHA256, hexHash); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + + @Test + public void testOperationSerialization() throws NoSuchAlgorithmException { + CreateHtlcOperation operation = this.buildCreateHtlcOperation(); + byte[] opBytes = operation.toBytes(); + Assert.assertArrayEquals(Util.hexToBytes(SERIALIZED_OP), opBytes); + } + + @Test + public void testTransactionSerialization() throws NoSuchAlgorithmException, ParseException { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + Date expirationDate = dateFormat.parse("2016-04-06T08:29:27"); + BlockData blockData = new BlockData(34294, 3707022213L, (expirationDate.getTime() / 1000)); + ArrayList operations = new ArrayList<>(); + operations.add(buildCreateHtlcOperation()); + Transaction transaction = new Transaction(blockData, operations); + // Checking byte serialization + byte[] txBytes = transaction.toBytes(); + byte[] expected = Bytes.concat(Util.hexToBytes(Chains.BITSHARES.CHAIN_ID), Util.hexToBytes(SERIALIZED_TX)); + Assert.assertArrayEquals(expected, txBytes); + // Checking JSON serialization + JsonObject jsonObject = transaction.toJsonObject(); + JsonArray operationsArray = jsonObject.get("operations").getAsJsonArray().get(0).getAsJsonArray(); + JsonArray hashArray = operationsArray.get(1).getAsJsonObject().get("preimage_hash").getAsJsonArray(); + Assert.assertEquals("2016-04-06T08:29:27", jsonObject.get("expiration").getAsString()); + Assert.assertEquals(49, operationsArray.get(0).getAsInt()); + Assert.assertEquals("1.2.123", operationsArray.get(1).getAsJsonObject().get("from").getAsString()); + Assert.assertEquals("1.2.124", operationsArray.get(1).getAsJsonObject().get("to").getAsString()); + Assert.assertEquals(0, hashArray.get(0).getAsInt()); + Assert.assertEquals(HASH_RIPEMD160, hashArray.get(1).getAsString()); + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/operations/RedeemHtlcOperationTest.java b/graphenej/src/test/java/cy/agorise/graphenej/operations/RedeemHtlcOperationTest.java new file mode 100644 index 0000000..0ba79db --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/operations/RedeemHtlcOperationTest.java @@ -0,0 +1,72 @@ +package cy.agorise.graphenej.operations; + +import com.google.common.primitives.Bytes; +import com.google.common.primitives.UnsignedLong; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import org.junit.Assert; +import org.junit.Test; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.TimeZone; + +import cy.agorise.graphenej.Asset; +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.BaseOperation; +import cy.agorise.graphenej.BlockData; +import cy.agorise.graphenej.Chains; +import cy.agorise.graphenej.Htlc; +import cy.agorise.graphenej.Transaction; +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.Util; + +public class RedeemHtlcOperationTest { + private final String SERIALIZED_OP = "00000000000000000084017c06666f6f62617200"; + private final String SERIALIZED_TX = "f68585abf4dce7c80457013200000000000000000084017c06666f6f6261720000"; + private final Asset CORE = new Asset("1.3.0"); + private final String PREIMAGE_HEX = "666f6f626172"; + + private RedeemHtlcOperation buildRedeemdHtlcOperation(){ + AssetAmount fee = new AssetAmount(UnsignedLong.valueOf("0"), CORE); + UserAccount redeemer = new UserAccount("1.2.124"); + Htlc htlc = new Htlc("1.16.132"); + byte[] preimage = Util.hexToBytes(PREIMAGE_HEX); + return new RedeemHtlcOperation(fee, redeemer, htlc, preimage); + } + + @Test + public void testOperationSerialization(){ + RedeemHtlcOperation redeemHtlcOperation = this.buildRedeemdHtlcOperation(); + byte[] opBytes = redeemHtlcOperation.toBytes(); + Assert.assertArrayEquals(Util.hexToBytes(SERIALIZED_OP), opBytes); + } + + @Test + public void testTransactionSerialization() throws ParseException { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + Date expirationDate = dateFormat.parse("2016-04-06T08:29:27"); + BlockData blockData = new BlockData(34294, 3707022213L, (expirationDate.getTime() / 1000)); + ArrayList operations = new ArrayList<>(); + operations.add(buildRedeemdHtlcOperation()); + Transaction transaction = new Transaction(blockData, operations); + // Checking byte serialization + byte[] txBytes = transaction.toBytes(); + byte[] expected = Bytes.concat(Util.hexToBytes(Chains.BITSHARES.CHAIN_ID), Util.hexToBytes(SERIALIZED_TX)); + Assert.assertArrayEquals(expected, txBytes); + // Checking JSON serialization + JsonObject jsonObject = transaction.toJsonObject(); + JsonArray operationsArray = jsonObject.get("operations").getAsJsonArray().get(0).getAsJsonArray(); + int operationId = operationsArray.get(0).getAsInt(); + JsonObject operationJson = operationsArray.get(1).getAsJsonObject(); + Assert.assertEquals("2016-04-06T08:29:27", jsonObject.get("expiration").getAsString()); + Assert.assertEquals(50, operationId); + Assert.assertEquals("1.16.132", operationJson.getAsJsonObject().get("htlc_id").getAsString()); + Assert.assertEquals(PREIMAGE_HEX, operationJson.getAsJsonObject().get("preimage").getAsString()); + Assert.assertEquals("1.2.124", operationJson.getAsJsonObject().get("redeemer").getAsString()); + } +} 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..8124945 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,48 @@ +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']) + implementation project(':graphenej') + implementation 'org.bitcoinj:bitcoinj-core:0.14.3' + 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.3' + implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' + implementation 'com.jakewharton:butterknife:8.8.1' + implementation 'com.google.code.gson:gson:2.8.5' + 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.3' +} 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..6f72eb6 --- /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("cy.agorise.labs.sample", appContext.getPackageName()); + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e4528e8 --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/cy/agorise/labs/sample/BrainkeyActivity.java b/sample/src/main/java/cy/agorise/labs/sample/BrainkeyActivity.java new file mode 100644 index 0000000..e0f369d --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/BrainkeyActivity.java @@ -0,0 +1,60 @@ +package cy.agorise.labs.sample; + +import android.os.Bundle; +import android.support.design.widget.TextInputEditText; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.StyleSpan; +import android.view.View; +import android.widget.TextView; + +import java.util.Locale; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import cy.agorise.graphenej.BrainKey; + +public class BrainkeyActivity extends AppCompatActivity { + private final String TAG = this.getClass().getName(); + @BindView(R.id.brainkey) + TextInputEditText mBrainkeyView; + + @BindView(R.id.pubkey) + TextInputEditText mDesiredPubKey; + + @BindView(R.id.pubkey_display) + TextView mPubkeyDisplay; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_brainkey); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ButterKnife.bind(this); + } + + @OnClick(R.id.button_generate) + public void onGenerateClicked(View v){ + String target = mDesiredPubKey.getText().toString(); + String brainkeyText = mBrainkeyView.getText().toString(); + StringBuilder builder = new StringBuilder(); + for(int i = 0; i < 10; i++){ + BrainKey brainKey = new BrainKey(brainkeyText, i); + builder.append(String.format(Locale.ROOT, "%d -> ", i)) + .append(brainKey.getPublicAddress("BTS").toString()) + .append("\n"); + } + String derivationResult = builder.toString(); + mPubkeyDisplay.setText(derivationResult); + if(!target.isEmpty() && derivationResult.contains(target)){ + int start = derivationResult.indexOf(target); + SpannableStringBuilder sBuilder = new SpannableStringBuilder(derivationResult); + sBuilder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), start, (start + 53), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + mPubkeyDisplay.setText(sBuilder); + } + } +} 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..3e760e5 --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java @@ -0,0 +1,146 @@ +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.util.Log; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TextView; + +import butterknife.BindView; +import butterknife.ButterKnife; +import cy.agorise.graphenej.RPC; +import cy.agorise.graphenej.api.ConnectionStatusUpdate; +import cy.agorise.graphenej.api.android.RxBus; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; + +public class CallsActivity extends AppCompatActivity { + private final String TAG = this.getClass().getName(); + + private static final String RECONNECT_NODE = "reconnect_node"; + private static final String TEST_BRAINKEY_DERIVATION = "test_brainkey_derivation"; + public static final String CREATE_HTLC = "create_htlc"; + public static final String REDEEM_HTLC = "redeem_htlc"; + + @BindView(R.id.call_list) + RecyclerView mRecyclerView; + + private CompositeDisposable compositeDisposable = new CompositeDisposable(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_calls); + ButterKnife.bind(this); + mRecyclerView.setHasFixedSize(true); + mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + mRecyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL)); + mRecyclerView.setAdapter(new CallAdapter()); + + Disposable disposable = RxBus.getBusInstance() + .asFlowable() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + + @Override + public void accept(Object message) throws Exception { + if(message instanceof ConnectionStatusUpdate){ + ConnectionStatusUpdate statusUpdate = (ConnectionStatusUpdate) message; + Log.d(TAG, String.format("ConnectionStatusUpdate. code: %d, api: %d", statusUpdate.getUpdateCode(),statusUpdate.getApi())); + } + } + }); + compositeDisposable.add(disposable); + } + + 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_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, + RPC.CALL_GET_DYNAMIC_GLOBAL_PROPERTIES, + RPC.CALL_GET_KEY_REFERENCES, + RPC.CALL_GET_ACCOUNT_BALANCES, + RPC.CALL_BROADCAST_TRANSACTION, + RPC.CALL_GET_TRANSACTION, + RECONNECT_NODE, + TEST_BRAINKEY_DERIVATION, + CREATE_HTLC, + REDEEM_HTLC + }; + + @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 if (selectedCall.equals(RECONNECT_NODE)){ + intent = new Intent(CallsActivity.this, RemoveNodeActivity.class); + } else if (selectedCall.equals(TEST_BRAINKEY_DERIVATION)){ + intent = new Intent(CallsActivity.this, BrainkeyActivity.class); + } else if (selectedCall.equals(CREATE_HTLC) || selectedCall.equals(REDEEM_HTLC)){ + intent = new Intent(CallsActivity.this, HtlcActivity.class); + intent.putExtra(Constants.KEY_SELECTED_CALL, selectedCall); + } 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; + } + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + compositeDisposable.dispose(); + } +} 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..805f850 --- /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; +import cy.agorise.graphenej.network.NodeLatencyVerifier; + +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(); + Intent intent = new Intent(this, NetworkService.class); + // Binding to NetworkService + 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/HtlcActivity.java b/sample/src/main/java/cy/agorise/labs/sample/HtlcActivity.java new file mode 100644 index 0000000..d452898 --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/HtlcActivity.java @@ -0,0 +1,248 @@ +package cy.agorise.labs.sample; + +import android.content.ComponentName; +import android.os.Bundle; +import android.os.IBinder; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; + +import com.google.common.primitives.UnsignedLong; + +import org.bitcoinj.core.ECKey; + +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; + +import cy.agorise.graphenej.Asset; +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.BaseOperation; +import cy.agorise.graphenej.BlockData; +import cy.agorise.graphenej.BrainKey; +import cy.agorise.graphenej.Htlc; +import cy.agorise.graphenej.HtlcHash; +import cy.agorise.graphenej.HtlcHashType; +import cy.agorise.graphenej.Transaction; +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.Util; +import cy.agorise.graphenej.api.ConnectionStatusUpdate; +import cy.agorise.graphenej.api.android.RxBus; +import cy.agorise.graphenej.api.calls.BroadcastTransaction; +import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties; +import cy.agorise.graphenej.models.DynamicGlobalProperties; +import cy.agorise.graphenej.models.JsonRpcResponse; +import cy.agorise.graphenej.operations.CreateHtlcOperation; +import cy.agorise.graphenej.operations.RedeemHtlcOperation; +import cy.agorise.labs.sample.fragments.CreateHtlcFragment; +import cy.agorise.labs.sample.fragments.RedeemHtlcFragment; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; + +public class HtlcActivity extends ConnectedActivity implements + CreateHtlcFragment.CreateHtlcListener, + RedeemHtlcFragment.RedeemHtlcListener { + private String TAG = this.getClass().getName(); + private final short PREIMAGE_LENGTH = 32; + private byte[] mPreimage = new byte[PREIMAGE_LENGTH]; + + private CreateHtlcOperation createHtlcOperation; + private RedeemHtlcOperation redeemHtlcOperation; + private Disposable mDisposable; + private String mHtlcMode; + + private Fragment mActiveBottomFragment; + private CreateHtlcFragment mCreateHtlcFragment; + private RedeemHtlcFragment mRedeemHtlcFragment; + + private HashMap mResponseMap = new HashMap<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_htlc); + + mHtlcMode = getIntent().getStringExtra(Constants.KEY_SELECTED_CALL); + + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + if(mHtlcMode.equals(CallsActivity.CREATE_HTLC)){ + mCreateHtlcFragment = new CreateHtlcFragment(); + mActiveBottomFragment = mCreateHtlcFragment; + }else{ + mRedeemHtlcFragment = new RedeemHtlcFragment(); + mActiveBottomFragment = mRedeemHtlcFragment; + } + fragmentTransaction.add(R.id.fragment_root, mActiveBottomFragment, "active-fragment").commit(); + + Toolbar toolbar = findViewById(R.id.toolbar); + if(toolbar != null && mHtlcMode != null){ + toolbar.setTitle(mHtlcMode.replace("_", " ").toUpperCase()); + setSupportActionBar(toolbar); + } + + // While for the real world is best to use a random pre-image, for testing purposes it is more + // convenient to make use of a fixed one. +// SecureRandom secureRandom = new SecureRandom(); +// secureRandom.nextBytes(mPreimage); + mPreimage = Util.hexToBytes("efb79df9b0fd0d27b405e4decf9a2534efc1531f9e133915981fe27cd031ba32"); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.htlc_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + this.switchHtlcMode(); + return true; + } + + @Override + public void onAttachFragment(Fragment fragment) { + super.onAttachFragment(fragment); + if(fragment instanceof CreateHtlcFragment){ + mCreateHtlcFragment = (CreateHtlcFragment) fragment; + } + } + + private void switchHtlcMode(){ + if(mHtlcMode.equals(CallsActivity.CREATE_HTLC)){ + mHtlcMode = CallsActivity.REDEEM_HTLC; + if(mRedeemHtlcFragment == null) + mRedeemHtlcFragment = new RedeemHtlcFragment(); + mActiveBottomFragment = mRedeemHtlcFragment; + }else{ + mHtlcMode = CallsActivity.CREATE_HTLC; + if(mCreateHtlcFragment == null) + mCreateHtlcFragment = new CreateHtlcFragment(); + mActiveBottomFragment = mCreateHtlcFragment; + } + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.fragment_root, mActiveBottomFragment, "active-fragment") + .commit(); + + Toolbar toolbar = findViewById(R.id.toolbar); + if(toolbar != null) + toolbar.setTitle(mHtlcMode.replace("_", " ").toUpperCase()); + } + + @Override + protected void onResume() { + super.onResume(); + mDisposable = RxBus.getBusInstance() + .asFlowable() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + + @Override + public void accept(Object message) throws Exception { + if(message instanceof ConnectionStatusUpdate){ + // TODO: Update UI ? + }else if(message instanceof JsonRpcResponse){ + handleJsonRpcResponse((JsonRpcResponse) message); + } + } + }); + } + + @Override + protected void onPause() { + super.onPause(); + if(!mDisposable.isDisposed()) + mDisposable.dispose(); + + } + + @Override + public void onHtlcProposal(String from, String to, Double amount, Long timelock) { + UserAccount sourceAccount = new UserAccount(from); + UserAccount destinationAccount = new UserAccount(to); + AssetAmount fee = new AssetAmount(UnsignedLong.valueOf("86726"), new Asset("1.3.0")); + AssetAmount operationAmount = new AssetAmount(UnsignedLong.valueOf(Double.valueOf(amount * 100000).longValue()), new Asset("1.3.0")); + try { + byte[] hash = Util.htlcHash(mPreimage, HtlcHashType.RIPEMD160); + HtlcHash htlcHash = new HtlcHash(HtlcHashType.RIPEMD160, hash); + // Creating a HTLC operation, used later. + createHtlcOperation = new CreateHtlcOperation(fee, sourceAccount, destinationAccount, operationAmount, htlcHash, PREIMAGE_LENGTH, timelock.intValue()); + // Requesting dynamic network parameters + long id = mNetworkService.sendMessage(new GetDynamicGlobalProperties(), GetDynamicGlobalProperties.REQUIRED_API); + mResponseMap.put(id, CallsActivity.CREATE_HTLC); + Log.d(TAG,"sendMessage returned: " + id); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG,"NoSuchAlgorithmException while trying to create HTLC operation. Msg: " + e.getMessage()); + } + } + + @Override + public void onRedeemProposal(String userId, String htlcId) { + AssetAmount fee = new AssetAmount(UnsignedLong.valueOf("255128"), new Asset("1.3.0")); + UserAccount redeemer = new UserAccount(userId); + Htlc htlc = new Htlc(htlcId); + redeemHtlcOperation = new RedeemHtlcOperation(fee, redeemer, htlc, mPreimage); + long id = mNetworkService.sendMessage(new GetDynamicGlobalProperties(), GetDynamicGlobalProperties.REQUIRED_API); + mResponseMap.put(id, CallsActivity.REDEEM_HTLC); + Log.d(TAG,"sendMessage returned: " + id); + } + + private void handleJsonRpcResponse(JsonRpcResponse jsonRpcResponse){ + Log.d(TAG,"handleJsonRpcResponse"); + if(jsonRpcResponse.error == null && jsonRpcResponse.result instanceof DynamicGlobalProperties){ + DynamicGlobalProperties dynamicGlobalProperties = (DynamicGlobalProperties) jsonRpcResponse.result; + Transaction tx = buildHltcTransaction(dynamicGlobalProperties, jsonRpcResponse.id); + long id = mNetworkService.sendMessage(new BroadcastTransaction(tx), BroadcastTransaction.REQUIRED_API); + Log.d(TAG,"sendMessage returned: " + id); + } + } + + /** + * Private method used to build a transaction containing a specific HTLC operation. + * + * @param dynamicProperties The current dynamic properties. + * @param responseId The response id, used to decide whether to build a CREATE_HTLC or REDEEM_HTLC operation. + * @return A transaction that contains an HTLC operation. + */ + private Transaction buildHltcTransaction(DynamicGlobalProperties dynamicProperties, long responseId){ + // Private key, to be obtained differently below depending on which operation we'll be performing. + ECKey privKey = null; + + // Use the valid BlockData just obtained from the blockchain + long expirationTime = (dynamicProperties.time.getTime() / 1000) + Transaction.DEFAULT_EXPIRATION_TIME; + String headBlockId = dynamicProperties.head_block_id; + long headBlockNumber = dynamicProperties.head_block_number; + BlockData blockData = new BlockData(headBlockNumber, headBlockId, expirationTime); + + // Using the HTLC create operaton just obtained before + ArrayList operations = new ArrayList<>(); + + if(mResponseMap.get(responseId).equals(CallsActivity.CREATE_HTLC)){ + // Deriving private key + BrainKey brainKey = new BrainKey(">> Place brainkey of HTLC creator here <<", 0); + privKey = brainKey.getPrivateKey(); + + operations.add(this.createHtlcOperation); + }else if(mResponseMap.get(responseId).equals(CallsActivity.REDEEM_HTLC)){ + // Deriving private key + BrainKey brainKey = new BrainKey(">> Place brainkey of redeemer here <<", 0); + privKey = brainKey.getPrivateKey(); + + operations.add(this.redeemHtlcOperation); + } + + // Return a newly built transaction + return new Transaction(privKey, blockData, operations); + } + + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { } + + @Override + public void onServiceDisconnected(ComponentName componentName) { } +} 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..041b4e8 --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/PerformCallActivity.java @@ -0,0 +1,652 @@ +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.common.primitives.UnsignedLong; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import org.bitcoinj.core.ECKey; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import cy.agorise.graphenej.Asset; +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.BaseOperation; +import cy.agorise.graphenej.BlockData; +import cy.agorise.graphenej.BrainKey; +import cy.agorise.graphenej.Memo; +import cy.agorise.graphenej.OperationType; +import cy.agorise.graphenej.Price; +import cy.agorise.graphenej.RPC; +import cy.agorise.graphenej.Transaction; +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.Util; +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.BroadcastTransaction; +import cy.agorise.graphenej.api.calls.GetAccountBalances; +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.GetAssets; +import cy.agorise.graphenej.api.calls.GetBlock; +import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties; +import cy.agorise.graphenej.api.calls.GetFullAccounts; +import cy.agorise.graphenej.api.calls.GetKeyReferences; +import cy.agorise.graphenej.api.calls.GetLimitOrders; +import cy.agorise.graphenej.api.calls.GetObjects; +import cy.agorise.graphenej.api.calls.GetRequiredFees; +import cy.agorise.graphenej.api.calls.GetTransaction; +import cy.agorise.graphenej.api.calls.ListAssets; +import cy.agorise.graphenej.errors.MalformedAddressException; +import cy.agorise.graphenej.models.AssetFeed; +import cy.agorise.graphenej.models.BitAssetData; +import cy.agorise.graphenej.models.JsonRpcResponse; +import cy.agorise.graphenej.operations.TransferOperation; +import cy.agorise.graphenej.operations.TransferOperationBuilder; +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: + setupGetRequiredFees(); + break; + case RPC.CALL_LOOKUP_ASSET_SYMBOLS: + setupLookupAssetSymbols(); + break; + case RPC.CALL_LIST_ASSETS: + setupListAssets(); + break; + case RPC.CALL_GET_ASSETS: + setupGetAssets(); + 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(); + break; + case RPC.CALL_GET_FULL_ACCOUNTS: + setupGetFullAccounts(); + break; + case RPC.CALL_GET_DYNAMIC_GLOBAL_PROPERTIES: + setupGetDynamicGlobalProperties(); + break; + case RPC.CALL_GET_KEY_REFERENCES: + setupGetKeyReferences(); + break; + case RPC.CALL_GET_ACCOUNT_BALANCES: + setupGetAccountBalances(); + break; + case RPC.CALL_BROADCAST_TRANSACTION: + setupBroadcastTransaction(); + break; + case RPC.CALL_GET_TRANSACTION: + setupGetTransaction(); + default: + Log.d(TAG,"Default called"); + } + + mDisposable = RxBus.getBusInstance() + .asFlowable() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + + @Override + public void accept(Object message) throws Exception { + 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 setupGetRequiredFees(){ + requiredInput(1); + mParam1View.setHint(getString(R.string.get_required_fees_asset)); + } + + 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); + mParam1View.setHint(getString(R.string.list_assets_arg1)); + mParam2View.setHint(getString(R.string.list_assets_arg2)); + param2.setInputType(InputType.TYPE_CLASS_NUMBER); + } + + private void setupGetAssets(){ + requiredInput(1); + mParam1View.setHint(getString(R.string.get_assets_arg)); + } + + 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 setupGetDynamicGlobalProperties(){ + requiredInput(0); + } + + private void setupGetKeyReferences(){ + requiredInput(1); + // Test address + param1.setText("BTS8a7XJ94u1traaLGFHw6NgpvUaxmbG4MyCcZC1hBj9HCBuMEwXP"); + } + + private void setupGetAccountBalances(){ + requiredInput(2); + param1.setHint(R.string.get_account_balances_arg1); + param2.setHint(R.string.get_account_balances_arg2); + } + + private void setupBroadcastTransaction(){ + requiredInput(2); + param1.setText("1.2.116354"); + param2.setText("1"); + } + + private void setupGetTransaction(){ + requiredInput(2); + param1.setText("13282815"); + param2.setText("0"); + } + + private void requiredInput(int inputCount){ + if(inputCount == 0){ + mParam1View.setVisibility(View.GONE); + mParam2View.setVisibility(View.GONE); + mParam3View.setVisibility(View.GONE); + mParam4View.setVisibility(View.GONE); + }else 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: + sendGetRequiredFees(); + break; + case RPC.CALL_LOOKUP_ASSET_SYMBOLS: + break; + case RPC.CALL_LIST_ASSETS: + sendListAssets(); + break; + case RPC.CALL_GET_ASSETS: + sendGetAssets(); + 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(); + break; + case RPC.CALL_GET_DYNAMIC_GLOBAL_PROPERTIES: + getDynamicGlobalProperties(); + break; + case RPC.CALL_GET_KEY_REFERENCES: + getKeyReferences(); + break; + case RPC.CALL_GET_ACCOUNT_BALANCES: + getAccountBalances(); + break; + case RPC.CALL_BROADCAST_TRANSACTION: + broadcastTransaction(); + break; + case RPC.CALL_GET_TRANSACTION: + getTransaction(); + 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 sendGetRequiredFees(){ + String input = param1.getText().toString(); + ArrayList operations = new ArrayList<>(); + AssetAmount transfer = new AssetAmount(UnsignedLong.valueOf("1000"), new Asset("1.3.0")); + AssetAmount fee = new AssetAmount(UnsignedLong.valueOf("1000"), new Asset("1.3.0")); + operations.add(new TransferOperation(new UserAccount("1.2.12300"), new UserAccount("1.2.12301"), fee, transfer)); + long id = mNetworkService.sendMessage(new GetRequiredFees(operations, new Asset(input)), GetRequiredFees.REQUIRED_API); + responseMap.put(id, mRPC); + } + + 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 sendGetAssets(){ + String assetIds = param1.getText().toString(); + ArrayList assetList = new ArrayList<>(); + for(String id :assetIds.split(",")){ + assetList.add(new Asset(id)); + } + long id = mNetworkService.sendMessage(new GetAssets(assetList), GetAssets.REQUIRED_API); + responseMap.put(id, mRPC); + } + + 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); + } + + private void getDynamicGlobalProperties(){ + long id = mNetworkService.sendMessage(new GetDynamicGlobalProperties(), GetDynamicGlobalProperties.REQUIRED_API); + responseMap.put(id, mRPC); + } + + private void getKeyReferences(){ + String address = param1.getText().toString(); + long id = 0; + try { + id = mNetworkService.sendMessage(new GetKeyReferences(address), GetKeyReferences.REQUIRED_API); + responseMap.put(id, mRPC); + } catch (MalformedAddressException | IllegalArgumentException e) { + Log.e(TAG,"MalformedAddressException. Msg: "+e.getMessage()); + Toast.makeText(this, "Malformed address exception", Toast.LENGTH_SHORT).show(); + param1.setText(""); + } + } + + private void getAccountBalances(){ + String accountId = param1.getText().toString(); + UserAccount userAccount = new UserAccount(accountId); + String assets = param2.getText().toString(); + String[] assetArray = assets.split(","); + List assetList = new ArrayList(); + for(String id : assetArray) assetList.add(new Asset(id)); + long id = mNetworkService.sendMessage(new GetAccountBalances(userAccount, assetList), GetAccountBalances.REQUIRED_API); + responseMap.put(id, mRPC); + } + + private void getTransaction(){ + long blockNum = Long.parseLong(param1.getText().toString()); + long index = Long.parseLong(param2.getText().toString()); + long id = mNetworkService.sendMessage(new GetTransaction(blockNum, index), GetTransaction.REQUIRED_API); + responseMap.put(id, mRPC); + } + + private void broadcastTransaction(){ + String destinationId = param1.getText().toString(); + String amount = param2.getText().toString(); + UnsignedLong transferAmount = UnsignedLong.valueOf(amount).times(UnsignedLong.valueOf(100000)); + TransferOperation operation = new TransferOperationBuilder() + .setSource(new UserAccount("1.2.1029856")) + .setDestination(new UserAccount(destinationId)) + .setTransferAmount(new AssetAmount( transferAmount, new Asset("1.3.0"))) + .setFee(new AssetAmount(UnsignedLong.valueOf("10420"), new Asset("1.3.0"))) + .build(); + ArrayList ops = new ArrayList<>(); + ops.add(operation); + // >> Replace with your brainkey << + BrainKey brainKey = new BrainKey(">> Place your brainkey here <<", 0); + ECKey privKey = brainKey.getPrivateKey(); + // Use valid BlockData + BlockData blockData = new BlockData(44542, 3342959171L, 1544917202L); + Transaction tx = new Transaction(privKey, blockData, ops); + long id = mNetworkService.sendMessage(new BroadcastTransaction(tx), BroadcastTransaction.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_ACCOUNT_BALANCES: + List balances = (List) response.result; + StringBuilder builder = new StringBuilder(); + for(AssetAmount assetAmount : balances) builder.append(assetAmount).append("\n"); + mResponseView.setText(builder.toString()); + break; + case RPC.CALL_GET_TRANSACTION: + Transaction tx = (Transaction) response.result; + mResponseView.setText(mResponseView.getText() + String.format("[%s][%s]", tx.toString(), Util.bytesToHex(tx.getHash()))); + break; + case RPC.CALL_GET_OBJECTS: + List bitAssetDataArray = (List) response.result; + BitAssetData bitAssetData = bitAssetDataArray.get(0); + AssetFeed assetFeed = bitAssetData.getCurrentFeed(); + Price price = assetFeed.getSettlementPrice(); + 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_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: + case RPC.CALL_GET_DYNAMIC_GLOBAL_PROPERTIES: + case RPC.CALL_GET_KEY_REFERENCES: + mResponseView.setText(mResponseView.getText() + gson.toJson(response, JsonRpcResponse.class) + "\n"); + break; + default: + Log.w(TAG,"Case not handled"); + if(response.result != null) + mResponseView.setText(mResponseView.getText() + response.result.toString()); + else if(response.error != null) + mResponseView.setText(mResponseView.getText() + String.format("Error code: %d, Msg: %s", response.error.code, response.error.message)); + else + mResponseView.setText(mResponseView.getText() + "\nnull"); + } + // 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/RemoveNodeActivity.java b/sample/src/main/java/cy/agorise/labs/sample/RemoveNodeActivity.java new file mode 100644 index 0000000..db7f861 --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/RemoveNodeActivity.java @@ -0,0 +1,286 @@ +package cy.agorise.labs.sample; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.util.SortedList; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import cy.agorise.graphenej.api.android.NetworkService; +import cy.agorise.graphenej.network.FullNode; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.subjects.PublishSubject; + +public class RemoveNodeActivity extends AppCompatActivity implements ServiceConnection { + + private final String TAG = this.getClass().getName(); + + @BindView(R.id.rvNodes) + RecyclerView rvNodes; + + FullNodesAdapter nodesAdapter; + + // Comparator used to sort the nodes in ascending order + private final Comparator LATENCY_COMPARATOR = (a, b) -> + Double.compare(a.getLatencyValue(), b.getLatencyValue()); + + /* Network service connection */ + private NetworkService mNetworkService; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_remove_node); + + ButterKnife.bind(this); + + rvNodes.setLayoutManager(new LinearLayoutManager(this)); + nodesAdapter = new FullNodesAdapter(this, LATENCY_COMPARATOR); + rvNodes.setAdapter(nodesAdapter); + } + + @OnClick(R.id.btnReconnectNode) + public void removeCurrentNode() { + mNetworkService.reconnectNode(); + } + + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + // We've bound to LocalService, cast the IBinder and get LocalService instance + NetworkService.LocalBinder binder = (NetworkService.LocalBinder) iBinder; + mNetworkService = binder.getService(); + + if(mNetworkService != null){ + // PublishSubject used to announce full node latencies updates + PublishSubject fullNodePublishSubject = mNetworkService.getNodeLatencyObservable(); + if(fullNodePublishSubject != null) + fullNodePublishSubject.observeOn(AndroidSchedulers.mainThread()).subscribe(nodeLatencyObserver); + + List fullNodes = mNetworkService.getNodes(); + nodesAdapter.add(fullNodes); + } + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + mNetworkService = null; + } + + /** + * Observer used to be notified about node latency measurement updates. + */ + private Observer nodeLatencyObserver = new Observer() { + @Override + public void onSubscribe(Disposable d) { } + + @Override + public void onNext(FullNode fullNode) { + if (!fullNode.isRemoved()) + nodesAdapter.add(fullNode); + else + nodesAdapter.remove(fullNode); + } + + @Override + public void onError(Throwable e) { + Log.e(TAG,"nodeLatencyObserver.onError.Msg: "+e.getMessage()); + } + + @Override + public void onComplete() { } + }; + + @Override + protected void onStart() { + super.onStart(); + // Bind to LocalService + Intent intent = new Intent(this, NetworkService.class); + bindService(intent, this, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onPause() { + super.onPause(); + unbindService(this); + } + + class FullNodesAdapter extends RecyclerView.Adapter { + + class ViewHolder extends RecyclerView.ViewHolder { + ImageView ivNodeStatus; + TextView tvNodeName; + + ViewHolder(View itemView) { + super(itemView); + + ivNodeStatus = itemView.findViewById(R.id.ivNodeStatus); + tvNodeName = itemView.findViewById(R.id.tvNodeName); + } + } + + private final SortedList mSortedList = new SortedList<>(FullNode.class, new SortedList.Callback() { + @Override + public void onInserted(int position, int count) { + notifyItemRangeInserted(position, count); + } + + @Override + public void onRemoved(int position, int count) { + notifyItemRangeRemoved(position, count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + notifyItemMoved(fromPosition, toPosition); + } + + @Override + public void onChanged(int position, int count) { + notifyItemRangeChanged(position, count); + } + + @Override + public int compare(FullNode a, FullNode b) { + return mComparator.compare(a, b); + } + + @Override + public boolean areContentsTheSame(FullNode oldItem, FullNode newItem) { + return oldItem.getLatencyValue() == newItem.getLatencyValue(); + } + + @Override + public boolean areItemsTheSame(FullNode item1, FullNode item2) { + return item1.getUrl().equals(item2.getUrl()); + } + }); + + private final Comparator mComparator; + + private Context mContext; + + FullNodesAdapter(Context context, Comparator comparator) { + mContext = context; + mComparator = comparator; + } + + private Context getContext() { + return mContext; + } + + @NonNull + @Override + public FullNodesAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + Context context = parent.getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + + View transactionView = inflater.inflate(R.layout.item_node, parent, false); + + return new ViewHolder(transactionView); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) { + final FullNode fullNode = mSortedList.get(position); + + // Show the green check mark before the node name if that node is the one being used + if (fullNode.isConnected()) + viewHolder.ivNodeStatus.setImageResource(R.drawable.ic_connected); + else + viewHolder.ivNodeStatus.setImageDrawable(null); + + double latency = fullNode.getLatencyValue(); + + // Select correct color span according to the latency value + ForegroundColorSpan colorSpan; + + if (latency < 400) + colorSpan = new ForegroundColorSpan(ContextCompat.getColor(getContext(), R.color.colorPrimary)); + else if (latency < 800) + colorSpan = new ForegroundColorSpan(Color.rgb(255,136,0)); // Holo orange + else + colorSpan = new ForegroundColorSpan(Color.rgb(204,0,0)); // Holo red + + // Create a string with the latency number colored according to their amount + SpannableStringBuilder ssb = new SpannableStringBuilder(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ssb.append(fullNode.getUrl().replace("wss://", ""), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + ssb.append(" ("); + + // 2000 ms is the timeout of the websocket used to calculate the latency, therefore if the + // received latency is greater than such value we can assume the node was not reachable. + String ms = latency < 2000 ? String.format(Locale.US, "%.0f ms", latency) : "??"; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ssb.append(ms, colorSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + ssb.append(")"); + + viewHolder.tvNodeName.setText(ssb); + } + + /** + * Functions that adds/updates a FullNode to the SortedList + */ + public void add(FullNode fullNode) { + // Remove the old instance of the FullNode before adding a new one. My understanding is that + // the sorted list should be able to automatically find repeated elements and update them + // instead of adding duplicates but it wasn't working so I opted for manually removing old + // instances of FullNodes before adding the updated ones. + int removed = 0; + for (int i=0; i fullNodes) { + mSortedList.addAll(fullNodes); + } + + public void remove(FullNode fullNode) { + mSortedList.remove(fullNode); + } + + @Override + public int getItemCount() { + return mSortedList.size(); + } + } +} 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..4039e61 --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/SampleApplication.java @@ -0,0 +1,66 @@ +package cy.agorise.labs.sample; + +import android.app.Application; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import cy.agorise.graphenej.api.ApiAccess; +import cy.agorise.graphenej.api.android.NetworkServiceManager; + +/** + * Sample application class + */ + +public class SampleApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + // 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; + + String[] nodeURLs = new String[]{ + "wss://bitshares.openledger.info/ws", + "wss://us.nodes.bitshares.ws", + "wss://eu.nodes.bitshares.ws", + "wss://citadel.li/node", + "wss://api.bts.mobi/ws" + }; + List nodeList = Arrays.asList(nodeURLs); + String nodes = join(nodeList, ","); + + NetworkServiceManager networkManager = new NetworkServiceManager.Builder() + .setUserName("username") + .setPassword("secret") + .setRequestedApis(requestedApis) + .setCustomNodeUrls(nodes) + .setAutoConnect(true) + .setNodeLatencyVerification(true) + .setLatencyAverageAlpha(0.1f) + .build(this); + + // 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(networkManager); + } + + /** + * Private method used to join a sequence of Strings given a iterable representation + * and a delimiter. + * + * + * @param s Any collection of CharSequence that implements the Iterable interface. + * @param delimiter The delimiter which will be used to join the different strings together. + * @return A single string combining all the iterable pieces with the delimiter. + */ + private String join(Iterable s, String delimiter) { + Iterator iter = s.iterator(); + if (!iter.hasNext()) return ""; + StringBuilder buffer = new StringBuilder(iter.next()); + while (iter.hasNext()) buffer.append(delimiter).append(iter.next()); + return buffer.toString(); + } +} 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/java/cy/agorise/labs/sample/fragments/CreateHtlcFragment.java b/sample/src/main/java/cy/agorise/labs/sample/fragments/CreateHtlcFragment.java new file mode 100644 index 0000000..b78573e --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/fragments/CreateHtlcFragment.java @@ -0,0 +1,98 @@ +package cy.agorise.labs.sample.fragments; + + +import android.content.Context; +import android.os.Bundle; +import android.support.design.widget.TextInputEditText; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import cy.agorise.labs.sample.R; + +/** + * A simple {@link Fragment} subclass. + */ +public class CreateHtlcFragment extends Fragment { + private final String TAG = this.getClass().getName(); + + @BindView(R.id.from) + TextInputEditText fromField; + + @BindView(R.id.to) + TextInputEditText toField; + + @BindView(R.id.amount) + TextInputEditText amountField; + + @BindView(R.id.timelock) + TextInputEditText timelockField; + + // Parent activity, which must implement the CreateHtlcListener interface. + private CreateHtlcListener mListener; + + public CreateHtlcFragment() { + // Required empty public constructor + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_create_htlc, container, false); + ButterKnife.bind(this, view); + return view; + } + + @OnClick(R.id.button_create) + public void onSendClicked(View v){ + String from = fromField.getText().toString(); + String to = toField.getText().toString(); + Double amount = null; + Long timeLock = null; + try{ + amount = Double.parseDouble(amountField.getText().toString()); + }catch(NumberFormatException e){ + amountField.setError("Invalid amount"); + } + try{ + timeLock = Long.parseLong(timelockField.getText().toString()); + }catch(NumberFormatException e){ + timelockField.setError("Invalid value"); + } + if(amount != null && timeLock != null){ + Toast.makeText(getContext(), "Should be sending message up", Toast.LENGTH_SHORT).show(); + mListener.onHtlcProposal(from, to, amount, timeLock); + } + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if(context instanceof CreateHtlcListener){ + mListener = (CreateHtlcListener) context; + }else{ + throw new ClassCastException(context.toString() + " must implement the CreateHtlcListener interface!"); + } + } + + /** + * Interface to be implemented by the parent activity. + */ + public interface CreateHtlcListener { + /** + * Method used to notify the parent activity of the request to create an HTLC with the following parameters. + * + * @param from Source account id. + * @param to Destination account id. + * @param amount The amount of BTS to propose the HTLC. + * @param timelock The timelock in seconds. + */ + void onHtlcProposal(String from, String to, Double amount, Long timelock); + } +} diff --git a/sample/src/main/java/cy/agorise/labs/sample/fragments/PrintResponseFragment.java b/sample/src/main/java/cy/agorise/labs/sample/fragments/PrintResponseFragment.java new file mode 100644 index 0000000..282d50f --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/fragments/PrintResponseFragment.java @@ -0,0 +1,30 @@ +package cy.agorise.labs.sample.fragments; + + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import cy.agorise.labs.sample.R; + +/** + * A simple {@link Fragment} subclass. + */ +public class PrintResponseFragment extends Fragment { + + + public PrintResponseFragment() { + // Required empty public constructor + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_print_response, container, false); + } + +} diff --git a/sample/src/main/java/cy/agorise/labs/sample/fragments/RedeemHtlcFragment.java b/sample/src/main/java/cy/agorise/labs/sample/fragments/RedeemHtlcFragment.java new file mode 100644 index 0000000..621da5e --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/fragments/RedeemHtlcFragment.java @@ -0,0 +1,75 @@ +package cy.agorise.labs.sample.fragments; + + +import android.content.Context; +import android.os.Bundle; +import android.support.design.widget.TextInputEditText; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import cy.agorise.labs.sample.R; + +/** + * A simple {@link Fragment} subclass. + */ +public class RedeemHtlcFragment extends Fragment { + + @BindView(R.id.redeemer) + TextInputEditText mRedeemer; + + @BindView(R.id.htlc_id) + TextInputEditText mHtlcId; + + // Parent activity, which must implement the RedeemHtlcListener interface. + private RedeemHtlcListener mListener; + + + public RedeemHtlcFragment() { + // Required empty public constructor + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_redeem_htlc, container, false); + ButterKnife.bind(this, view); + return view; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if(context instanceof RedeemHtlcListener){ + mListener = (RedeemHtlcListener) context; + }else{ + throw new ClassCastException(context.toString() + " must implement the RedeemHtlcListener interface!"); + } + } + + @OnClick(R.id.button_create) + public void onSendClicked(View v){ + String redeemerId = mRedeemer.getText().toString(); + String htlcId = mHtlcId.getText().toString(); + Toast.makeText(getContext(), "Should be sending message up", Toast.LENGTH_SHORT).show(); + mListener.onRedeemProposal(redeemerId, htlcId); + } + + /** + * Interface to be implemented by the parent activity. + */ + public interface RedeemHtlcListener { + /** + * Method used to notify the parent activity about the creation of an HTLC redeem operation. + * + * @param userId The id of the user that wishes to redeem an HTLC. + * @param htlcId The HTLC id. + */ + void onRedeemProposal(String userId, String htlcId); + } +} 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_connected.xml b/sample/src/main/res/drawable/ic_connected.xml new file mode 100644 index 0000000..5b57096 --- /dev/null +++ b/sample/src/main/res/drawable/ic_connected.xml @@ -0,0 +1,5 @@ + + + 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/drawable/ic_switch_hltc_mode.xml b/sample/src/main/res/drawable/ic_switch_hltc_mode.xml new file mode 100644 index 0000000..9908c1e --- /dev/null +++ b/sample/src/main/res/drawable/ic_switch_hltc_mode.xml @@ -0,0 +1,5 @@ + + + diff --git a/sample/src/main/res/layout/activity_brainkey.xml b/sample/src/main/res/layout/activity_brainkey.xml new file mode 100644 index 0000000..1bc1860 --- /dev/null +++ b/sample/src/main/res/layout/activity_brainkey.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + \ No newline at end of file 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..10e0e6c --- /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_htlc.xml b/sample/src/main/res/layout/activity_htlc.xml new file mode 100644 index 0000000..34e4bdd --- /dev/null +++ b/sample/src/main/res/layout/activity_htlc.xml @@ -0,0 +1,28 @@ + + + + + + + \ 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..58c16ae --- /dev/null +++ b/sample/src/main/res/layout/activity_perform_call.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + +