Merge branch 'develop'

master
Nelson R. Perez 2019-09-25 20:10:48 -05:00
commit 9b915b3d36
158 changed files with 8376 additions and 231 deletions

1
.gitignore vendored
View File

@ -103,3 +103,4 @@ release.properties
graphenej/build
local.properties

View File

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

View File

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

0
gradlew vendored Normal file → Executable file
View File

View File

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

View File

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

View File

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

View File

@ -33,17 +33,51 @@ public class AccountOptions implements GrapheneSerializable {
private Vote[] votes;
private Extensions extensions;
/**
* Constructor used to instantiate only the following attributes:
* <ul>
* <li>voting_account</li>
* <li>votes</li>
* <li>extensions</li>
* </ul>
*/
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:
* <ul>
* <li>voting_account</li>
* <li>votes</li>
* <li>memo_key</li>
* <li>extensions</li>
* </ul>
*/
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<AccountOptions> {
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();

View File

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

View File

@ -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.
*/

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,8 @@ package cy.agorise.graphenej;
import com.google.gson.annotations.Expose;
import java.util.Locale;
/**
* <p>
* 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){

View File

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

View File

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

View File

@ -0,0 +1,11 @@
package cy.agorise.graphenej;
/**
* Used to enumerate the possible hash algorithms used in HTLCs.
* @see <a href="https://github.com/bitshares/bitshares-core/blob/623aea265f2711adade982fc3248e6528dc8ac51/libraries/chain/include/graphene/chain/protocol/htlc.hpp">htlc.hpp</a>
*/
public enum HtlcHashType {
RIPEMD160,
SHA1,
SHA256
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,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";
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String, List<String>> headers) throws Exception {
ArrayList<Serializable> params = new ArrayList<>();
ArrayList<Serializable> subParams = new ArrayList<>();
for(String id : this.ids){
subParams.add(id);
}
subParams.addAll(this.ids);
params.add(subParams);
ApiCall apiCall = new ApiCall(0, RPC.GET_OBJECTS, params, RPC.VERSION, 0);
ApiCall apiCall = new ApiCall(0, RPC.CALL_GET_OBJECTS, params, RPC.VERSION, 0);
websocket.sendText(apiCall.toJsonString());
}
@ -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<WitnessResponse<List<BitAssetData>>>(){}.getType();
WitnessResponse<List<BitAssetData>> witnessResponse = gsonBuilder.create().fromJson(response, BitAssetDataType);
BitAssetData bitAssetData = witnessResponse.result.get(0);
BitAssetData bitAssetData = gson.fromJson(element, BitAssetData.class);
parsedResult.add(bitAssetData);
break;
}
}

View File

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

View File

@ -23,9 +23,10 @@ import cy.agorise.graphenej.interfaces.SubscriptionHub;
import cy.agorise.graphenej.interfaces.SubscriptionListener;
import cy.agorise.graphenej.models.ApiCall;
import cy.agorise.graphenej.models.DynamicGlobalProperties;
import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.models.SubscriptionResponse;
import cy.agorise.graphenej.models.WitnessResponse;
import cy.agorise.graphenej.objects.Memo;
import cy.agorise.graphenej.Memo;
import cy.agorise.graphenej.operations.CustomOperation;
import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
import cy.agorise.graphenej.operations.TransferOperation;
@ -61,6 +62,7 @@ public class SubscriptionMessagesHub extends BaseGrapheneHandler implements Subs
private int subscriptionCounter = 0;
private HashMap<Long, BaseGrapheneHandler> mHandlerMap = new HashMap<>();
private List<BaseGrapheneHandler> pendingHandlerList = new ArrayList<>();
private boolean printLogs = 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<Serializable> 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;
}
}

View File

@ -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<Class, Class> mClassMap = new HashMap<>();
private HashMap<Class, Gson> 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;
}
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* For example:
*
* wss://domain1.com/ws,wss://domain2.com/ws,wss://domain3.com/ws
* <p>
* 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<Integer, Integer> mApiIds = new HashMap<Integer, Integer>();
// 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<FullNode> 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<Long, Class> mRequestClassMap = new HashMap<>();
// This class is used to keep track of the mapping between request classes and response
// payload classes. It also provides a handy method that returns a Gson deserializer instance
// suited for every response type.
private DeserializationMap mDeserializationMap = new DeserializationMap();
/**
* 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<FullNode> 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<FullNode> nodeLatencyObserver = new Observer<FullNode>() {
@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<Serializable> loginParams = new ArrayList<>();
loginParams.add(mUsername);
loginParams.add(mPassword);
ApiCall loginCall = new ApiCall(1, RPC.CALL_LOGIN, loginParams, RPC.VERSION, ++mCurrentId);
mLastCall = RPC.CALL_LOGIN;
sendMessage(loginCall.toJsonString());
}
}
@Override
public 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<JsonRpcResponse<Integer>>() {}.getType();
JsonRpcResponse<Integer> apiIdResponse = gson.fromJson(text, IntegerJsonResponse);
// Storing the "database" api id
mApiIds.put(ApiAccess.API_DATABASE, apiIdResponse.result);
// Broadcasting result
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.API_UPDATE, ApiAccess.API_DATABASE));
checkNextRequestedApiAccess();
break;
}
case RPC.CALL_HISTORY: {
// Deserializing integer response
Type IntegerJsonResponse = new TypeToken<JsonRpcResponse<Integer>>() {}.getType();
JsonRpcResponse<Integer> apiIdResponse = gson.fromJson(text, IntegerJsonResponse);
// Broadcasting result
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.API_UPDATE, ApiAccess.API_HISTORY));
// Storing the "history" api id
mApiIds.put(ApiAccess.API_HISTORY, apiIdResponse.result);
checkNextRequestedApiAccess();
break;
}
case RPC.CALL_NETWORK_BROADCAST:
// Deserializing integer response
Type IntegerJsonResponse = new TypeToken<JsonRpcResponse<Integer>>() {}.getType();
JsonRpcResponse<Integer> apiIdResponse = gson.fromJson(text, IntegerJsonResponse);
// Broadcasting result
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.API_UPDATE, ApiAccess.API_NETWORK_BROADCAST));
// Storing the "network_broadcast" api access
mApiIds.put(ApiAccess.API_NETWORK_BROADCAST, apiIdResponse.result);
// All calls have been handled at this point
mLastCall = "";
break;
}
}
}
if(response.error != null && response.error.message != null){
// We could not make sense of this incoming message, just log a warning
Log.w(TAG,"Error.Msg: "+response.error.message);
}
// Properly de-serialize all other fields and broadcasts to the event bus
handleJsonRpcResponse(response, text);
}
}
/**
* Private method that will de-serialize all fields of every kind of JSON-RPC response
* and broadcast it to the event bus.
*
* @param response De-serialized response
* @param text Raw text, as received
*/
private void handleJsonRpcResponse(JsonRpcResponse response, String text){
JsonRpcResponse parsedResponse = null;
Class requestClass = mRequestClassMap.get(response.id);
if(requestClass != null){
// Removing the class entry in the map
mRequestClassMap.remove(response.id);
// Obtaining the response payload class
Class responsePayloadClass = mDeserializationMap.getReceivedClass(requestClass);
Gson gson = mDeserializationMap.getGson(requestClass);
if(responsePayloadClass == Block.class){
// If the response payload is a Block instance, we proceed to de-serialize it
Type GetBlockResponse = new TypeToken<JsonRpcResponse<Block>>() {}.getType();
parsedResponse = gson.fromJson(text, GetBlockResponse);
}else if(responsePayloadClass == BlockHeader.class){
// If the response payload is a BlockHeader instance, we proceed to de-serialize it
Type GetBlockHeaderResponse = new TypeToken<JsonRpcResponse<BlockHeader>>(){}.getType();
parsedResponse = gson.fromJson(text, GetBlockHeaderResponse);
} else if(responsePayloadClass == AccountProperties.class){
Type GetAccountByNameResponse = new TypeToken<JsonRpcResponse<AccountProperties>>(){}.getType();
parsedResponse = gson.fromJson(text, GetAccountByNameResponse);
} else if(responsePayloadClass == HistoryOperationDetail.class){
Type GetAccountHistoryByOperationsResponse = new TypeToken<JsonRpcResponse<HistoryOperationDetail>>(){}.getType();
parsedResponse = gson.fromJson(text, GetAccountHistoryByOperationsResponse);
}else if(responsePayloadClass == DynamicGlobalProperties.class){
Type GetDynamicGlobalPropertiesResponse = new TypeToken<JsonRpcResponse<DynamicGlobalProperties>>(){}.getType();
parsedResponse = gson.fromJson(text, GetDynamicGlobalPropertiesResponse);
}else if(responsePayloadClass == Transaction.class){
Type GetTransactionClass = new TypeToken<JsonRpcResponse<Transaction>>(){}.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<List<AccountProperties>>
// so we proceed with that
Type GetAccountsResponse = new TypeToken<JsonRpcResponse<List<AccountProperties>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetAccountsResponse);
}else if(requestClass == GetRequiredFees.class){
Type GetRequiredFeesResponse = new TypeToken<JsonRpcResponse<List<AssetAmount>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetRequiredFeesResponse);
}else if(requestClass == GetRelativeAccountHistory.class){
Type RelativeAccountHistoryResponse = new TypeToken<JsonRpcResponse<List<OperationHistory>>>(){}.getType();
parsedResponse = gson.fromJson(text, RelativeAccountHistoryResponse);
}else if(requestClass == GetMarketHistory.class){
Type GetMarketHistoryResponse = new TypeToken<JsonRpcResponse<List<BucketObject>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetMarketHistoryResponse);
}else if(requestClass == GetObjects.class){
parsedResponse = handleGetObject(text);
}else if(requestClass == ListAssets.class){
Type LisAssetsResponse = new TypeToken<JsonRpcResponse<List<Asset>>>(){}.getType();
parsedResponse = gson.fromJson(text, LisAssetsResponse);
}else if(requestClass == GetLimitOrders.class){
Type GetLimitOrdersResponse = new TypeToken<JsonRpcResponse<List<LimitOrder>>>() {}.getType();
parsedResponse = gson.fromJson(text, GetLimitOrdersResponse);
} else if (requestClass == GetFullAccounts.class) {
Type GetFullAccountsResponse = new TypeToken<JsonRpcResponse<List<FullAccountDetails>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetFullAccountsResponse);
} else if(requestClass == GetKeyReferences.class){
Type GetKeyReferencesResponse = new TypeToken<JsonRpcResponse<List<List<UserAccount>>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetKeyReferencesResponse);
} else if(requestClass == GetAccountBalances.class){
Type GetAccountBalancesResponse = new TypeToken<JsonRpcResponse<List<AssetAmount>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetAccountBalancesResponse);
} else if(requestClass == GetAssets.class){
Type GetAssetsResponse = new TypeToken<JsonRpcResponse<List<Asset>>>(){}.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<JsonRpcResponse<List<BitAssetData>>>(){}.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<FullNode> 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<FullNode> getNodeLatencyObservable(){
return fullNodePublishSubject;
}
public NodeLatencyVerifier getNodeLatencyVerifier(){ return nodeLatencyVerifier; }
}

View File

@ -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<Context> 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<String> 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<String> 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<String> getCustomNodeUrls() {
return mCustomNodeUrls;
}
public void setCustomNodeUrls(List<String> 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<String> 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<String> 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;
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,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<Serializable> transactions = new ArrayList<>();
transactions.add(mTransaction);
return new ApiCall(apiId, RPC.CALL_BROADCAST_TRANSACTION, transactions, RPC.VERSION, sequenceId);
}
}

View File

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

View File

@ -0,0 +1,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 <a href="https://goo.gl/faFdey">get_account_balances API doc</a>
*/
public class GetAccountBalances implements ApiCallable {
public static final int REQUIRED_API = ApiAccess.API_NONE;
private UserAccount mUserAccount;
private List<Asset> mAssetList;
public GetAccountBalances(UserAccount userAccount, List<Asset> assets){
mUserAccount = userAccount;
mAssetList = assets;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
ArrayList<Serializable> 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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<Asset> assetList = new ArrayList<>();
/**
* Constructor that will receive a List of Asset instances.
*
* @param assets List of Asset instances.
*/
public GetAssets(List<Asset> 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<Serializable> params = new ArrayList<>();
ArrayList<Serializable> 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);
}
}

View File

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

View File

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

View File

@ -0,0 +1,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<Serializable> params = new ArrayList<>();
return new ApiCall(apiId, RPC.CALL_GET_DYNAMIC_GLOBAL_PROPERTIES, params, RPC.VERSION, sequenceId);
}
}

View File

@ -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<String> 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<String> accounts, boolean subscribe){
this.mUserAccounts = accounts;
this.mSubscribe = subscribe;
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> params = new ArrayList<>();
ArrayList<Serializable> accounts = new ArrayList<Serializable>(mUserAccounts);
params.add(accounts);
params.add(mSubscribe);
return new ApiCall(apiId, RPC.CALL_GET_FULL_ACCOUNTS, params, RPC.VERSION, sequenceId);
}
}

View File

@ -0,0 +1,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<Address> addresses = new ArrayList<>();
public GetKeyReferences(String addr) throws MalformedAddressException, IllegalArgumentException {
this(new Address(addr));
}
public GetKeyReferences(Address address){
addresses.add(address);
}
public GetKeyReferences(List<Address> addressList){
addresses.addAll(addressList);
}
@Override
public ApiCall toApiCall(int apiId, long sequenceId) {
ArrayList<Serializable> inner = new ArrayList<Serializable>();
for(Address addr : addresses){
inner.add(addr.toString());
}
ArrayList<Serializable> params = new ArrayList<>();
params.add(inner);
return new ApiCall(apiId, RPC.CALL_GET_KEY_REFERENCES, params, RPC.VERSION, sequenceId);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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<Serializable> params = new ArrayList<>();
params.add(blockNumber);
params.add(txIndex);
return new ApiCall(apiId, RPC.CALL_GET_TRANSACTION, params, RPC.VERSION, sequenceId);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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<AssetFeed> {
@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;
}
}
}

View File

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

View File

@ -1,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<BitAssetData> {
@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<ReportedAssetFeed> 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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ import java.util.List;
import cy.agorise.graphenej.GrapheneObject;
import cy.agorise.graphenej.ObjectType;
import cy.agorise.graphenej.OperationType;
import cy.agorise.graphenej.Transaction;
import cy.agorise.graphenej.interfaces.SubscriptionListener;
@ -43,15 +44,12 @@ import cy.agorise.graphenej.interfaces.SubscriptionListener;
* To minimize CPU usage, we introduce a scheme of selective parsing, implemented by the static inner class
* SubscriptionResponseDeserializer.
*
* Created by nelson on 1/12/17.
*/
public class SubscriptionResponse {
private static final String TAG = "SubscriptionResponse";
public static final String KEY_ID = "id";
public static final String KEY_METHOD = "method";
public static final String KEY_PARAMS = "params";
public int id;
public String method;
public List<Serializable> params;
@ -182,6 +180,14 @@ public class SubscriptionResponse {
broadcastedTransaction.setTransactionId(jsonObject.get(BroadcastedTransaction.KEY_TRX_ID).getAsString());
objectMap.put(ObjectType.TRANSACTION_OBJECT, true);
secondArgument.add(broadcastedTransaction);
}else if(grapheneObject.getObjectType() == ObjectType.OPERATION_HISTORY_OBJECT){
if(jsonObject.get(OperationHistory.KEY_OP).getAsJsonArray().get(0).getAsLong() == OperationType.TRANSFER_OPERATION.ordinal()){
OperationHistory operationHistory = context.deserialize(jsonObject, OperationHistory.class);
objectMap.put(ObjectType.OPERATION_HISTORY_OBJECT, true);
secondArgument.add(operationHistory);
}else{
//TODO: Add support for other operations
}
}else{
//TODO: Add support for other types of objects
}

View File

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

View File

@ -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.
* <p>
* 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.
* <p>
* 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 2<sup>63</sup>-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();
}
}

View File

@ -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<FullNode> 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<FullNode> getSortedNodes() {
FullNode[] nodeArray = mFullNodeHeap.toArray(new FullNode[mFullNodeHeap.size()]);
ArrayList<FullNode> nodeList = new ArrayList<>();
for(FullNode fullNode : nodeArray){
if(fullNode != null){
nodeList.add(fullNode);
}
}
Collections.sort(nodeList);
return nodeList;
}
}

View File

@ -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<FullNode> mNodeList;
// Variable used to store the desired verification period
private long verificationPeriod;
// Subject used to publish the result to interested parties
private PublishSubject<FullNode> subject = PublishSubject.create();
private HashMap<HttpUrl, FullNode> 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<FullNode, Long> timestamps = new HashMap<>();
private HashMap<String, Request> requestMap = new HashMap<>();
private Handler mHandler = new Handler(Looper.getMainLooper());
private OkHttpClient client;
public NodeLatencyVerifier(List<FullNode> nodes){
this(nodes, DEFAULT_LATENCY_VERIFICATION_PERIOD);
}
public NodeLatencyVerifier(List<FullNode> nodes, long period){
mNodeList = nodes;
verificationPeriod = period;
}
/**
* Method used to start the latency verification task.
* <p>
* The returning object can be used for interested parties to receive constant updates
* regarding new latency measurements for every full node.
* </p>
* @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<FullNode> getNodeList(){
return mNodeList;
}
}

View File

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

View File

@ -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<AccountUpgradeOperation> {
@Override
public JsonElement serialize(AccountUpgradeOperation accountUpgrade, Type type, JsonSerializationContext jsonSerializationContext) {
return accountUpgrade.toJsonObject();
}
}
public static class AccountUpgradeDeserializer implements JsonDeserializer<AccountUpgradeOperation> {
@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;
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <a href="https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average">here</a>.
*/
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> ids = new ArrayList<>();
ids.add(ltmAccount.getObjectId());
mWebSocket.addListener(new GetObjects(ids, new WitnessResponseListener() {
@Override
public void onSuccess(WitnessResponse response) {
System.out.println("onSuccess");
List<GrapheneObject> result = (List<GrapheneObject>) response.result;
UserAccount userAccount = (UserAccount) result.get(0);
System.out.println("Account name.....: "+userAccount.getName());
System.out.println("Is LTM...........: "+userAccount.isLifeTime());
System.out.println("json string......: "+userAccount.toJsonString());
System.out.println("owner............: "+userAccount.getOwner().getKeyAuthList().get(0).getAddress());
System.out.println("active key.......: "+userAccount.getActive().getKeyAuthList().get(0).getAddress());
System.out.println("memo: "+userAccount.getOptions().getMemoKey().getAddress());
Assert.assertEquals("We expect this account to be LTM",true, userAccount.isLifeTime());
synchronized (GetObjectsTest.this){
GetObjectsTest.this.notifyAll();
}
}
@Override
public void onError(BaseResponse.Error error) {
System.out.println("onError");
synchronized (GetObjectsTest.this){
GetObjectsTest.this.notifyAll();
}
}
}));
try {
mWebSocket.connect();
synchronized (this){
wait();
}
}catch (WebSocketException e) {
System.out.println("WebSocketException. Msg: " + e.getMessage());
} catch (InterruptedException e) {
System.out.println("InterruptedException. Msg: "+e.getMessage());
}
}
@Test
public void testBitAssetData(){
try{
ArrayList<String> 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<GrapheneObject> list = (List<GrapheneObject>) response.result;
BitAssetData bitAssetData = (BitAssetData) list.get(0);
System.out.println("feed time: " + bitAssetData.current_feed_publication_time);
List<BitAssetData> list = (List<BitAssetData>) 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();
}
}
}));

View File

@ -8,7 +8,7 @@ import java.util.List;
import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.interfaces.WitnessResponseListener;
import cy.agorise.graphenej.models.BaseResponse;
import cy.agorise.graphenej.models.HistoricalTransfer;
import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.models.WitnessResponse;
import cy.agorise.graphenej.operations.TransferOperation;
@ -51,11 +51,11 @@ public class GetRelativeAccountHistoryTest extends BaseApiTest {
public void onSuccess(WitnessResponse response) {
System.out.println("mTransferHistoryListener.onSuccess");
historicalTransferCount++;
WitnessResponse<List<HistoricalTransfer>> resp = response;
for(HistoricalTransfer historicalTransfer : resp.result){
WitnessResponse<List<OperationHistory>> resp = response;
for(OperationHistory historicalTransfer : resp.result){
if(historicalTransfer.getOperation() != null){
System.out.println("Got transfer operation!");
TransferOperation transferOperation = historicalTransfer.getOperation();
TransferOperation transferOperation = (TransferOperation) historicalTransfer.getOperation();
System.out.println(String.format("%s - > %s, memo: %s",
transferOperation.getFrom().getObjectId(),
transferOperation.getTo().getObjectId(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More