diff --git a/src/main/java/com/luminiasoft/bitshares/Main.java b/src/main/java/com/luminiasoft/bitshares/Main.java index 01de501..369b6de 100644 --- a/src/main/java/com/luminiasoft/bitshares/Main.java +++ b/src/main/java/com/luminiasoft/bitshares/Main.java @@ -71,6 +71,9 @@ public class Main { // test.testBrainKeyOperations(false); // test.testBip39Opertion(); - test.testAccountNamebyAddress(); + +// test.testAccountNamebyAddress(); + + test.testRelativeAccountHistory(); } } diff --git a/src/main/java/com/luminiasoft/bitshares/RPC.java b/src/main/java/com/luminiasoft/bitshares/RPC.java index c146973..feb7d8e 100644 --- a/src/main/java/com/luminiasoft/bitshares/RPC.java +++ b/src/main/java/com/luminiasoft/bitshares/RPC.java @@ -4,12 +4,15 @@ package com.luminiasoft.bitshares; * Created by nelson on 11/16/16. */ public class RPC { + public static final String VERSION = "2.0"; public static final String CALL_LOGIN = "login"; public static final String CALL_NETWORK_BROADCAST = "network_broadcast"; + public static final String CALL_HISTORY = "history"; public static final String CALL_GET_ACCOUNT_BY_NAME = "get_account_by_name"; 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_ACCOUNTS = "get_accounts"; public static final String CALL_GET_KEY_REFERENCES = "get_key_references"; + public static final String CALL_GET_RELATIVE_ACCOUNT_HISTORY = "get_relative_account_history"; } diff --git a/src/main/java/com/luminiasoft/bitshares/Test.java b/src/main/java/com/luminiasoft/bitshares/Test.java index 7b9776f..43dbdee 100644 --- a/src/main/java/com/luminiasoft/bitshares/Test.java +++ b/src/main/java/com/luminiasoft/bitshares/Test.java @@ -7,10 +7,8 @@ import com.google.gson.reflect.TypeToken; import com.luminiasoft.bitshares.errors.MalformedTransactionException; import com.luminiasoft.bitshares.interfaces.WitnessResponseListener; import com.luminiasoft.bitshares.models.*; -import com.luminiasoft.bitshares.ws.GetAccountByName; -import com.luminiasoft.bitshares.ws.GetAccountsByAddress; -import com.luminiasoft.bitshares.ws.GetRequiredFees; -import com.luminiasoft.bitshares.ws.TransactionBroadcastSequence; +import com.luminiasoft.bitshares.test.NaiveSSLContext; +import com.luminiasoft.bitshares.ws.*; import com.neovisionaries.ws.client.*; import org.bitcoinj.core.*; import org.spongycastle.crypto.Digest; @@ -19,10 +17,14 @@ import org.spongycastle.crypto.digests.RIPEMD160Digest; import org.spongycastle.crypto.digests.SHA512Digest; import org.spongycastle.crypto.prng.DigestRandomGenerator; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocketFactory; import java.io.*; import java.lang.reflect.Type; import java.net.URL; import java.nio.file.Paths; +import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.*; @@ -32,6 +34,9 @@ import java.util.*; public class Test { public static final String WITNESS_URL = "ws://api.devling.xyz:8088"; + public static final String OPENLEDGER_WITNESS_URL = "wss://bitshares.openledger.info/ws"; +// public static final String WITNESS_URL = "wss://fr.blockpay.ch:8089"; + private Transaction transaction; public Transaction getTransaction() { @@ -573,10 +578,10 @@ public class Test { BrainKey brainKey = new BrainKey(Main.BRAIN_KEY, 0); Address address = new Address(brainKey.getPrivateKey()); try { - WebSocketFactory factory = new WebSocketFactory().setConnectionTimeout(5000); - WebSocket mWebSocket = factory.createSocket(WITNESS_URL); + WebSocket mWebSocket = new WebSocketFactory().createSocket(WITNESS_URL); byte[] key = brainKey.getPrivateKey().getPubKey(); mWebSocket.addListener(new GetAccountsByAddress(key, mListener)); + System.out.println("Before connecting"); mWebSocket.connect(); } catch (IOException e) { System.out.println("IOException. Msg: " + e.getMessage()); @@ -584,4 +589,27 @@ public class Test { System.out.println("WebSocketException. Msg: " + e.getMessage()); } } + + public void testRelativeAccountHistory(){ + GetRelativeAccountHistory relativeAccountHistory = new GetRelativeAccountHistory(new UserAccount("1.2.138632"), mListener); + try { + // Create a custom SSL context. + SSLContext context = null; + context = NaiveSSLContext.getInstance("TLS"); + WebSocketFactory factory = new WebSocketFactory(); + + // Set the custom SSL context. + factory.setSSLContext(context); + + WebSocket mWebSocket = factory.createSocket(OPENLEDGER_WITNESS_URL); + mWebSocket.addListener(relativeAccountHistory); + mWebSocket.connect(); + } catch (IOException e) { + System.out.println("IOException. Msg: "+e.getMessage()); + } catch (WebSocketException e) { + System.out.println("WebSocketException. Msg: "+e.getMessage()); + } catch (NoSuchAlgorithmException e) { + System.out.println("NoSuchAlgorithmException. Msg: "+e.getMessage()); + } + } } diff --git a/src/main/java/com/luminiasoft/bitshares/Transfer.java b/src/main/java/com/luminiasoft/bitshares/Transfer.java index 187cbaa..d2b2bc0 100644 --- a/src/main/java/com/luminiasoft/bitshares/Transfer.java +++ b/src/main/java/com/luminiasoft/bitshares/Transfer.java @@ -1,17 +1,14 @@ package com.luminiasoft.bitshares; 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 com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; +import com.google.common.primitives.UnsignedLong; +import com.google.gson.*; import java.lang.reflect.Type; /** * Class used to encapsulate the Transfer operation related functionalities. + * TODO: Add extensions support */ public class Transfer extends BaseOperation { public static final String KEY_FEE = "fee"; @@ -100,14 +97,72 @@ public class Transfer extends BaseOperation { return array; } - class TransferSerializer implements JsonSerializer { + public static class TransferSerializer implements JsonSerializer { @Override public JsonElement serialize(Transfer transfer, Type type, JsonSerializationContext jsonSerializationContext) { JsonArray arrayRep = new JsonArray(); arrayRep.add(transfer.getId()); - arrayRep.add(toJsonObject()); + arrayRep.add(transfer.toJsonObject()); return arrayRep; } } + + /** + * This deserializer will work on any transfer operation serialized in the 'array form' used a lot in + * the Graphene Blockchain API. + * + * An example of this serialized form is the following: + * + * [ + * 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": [] + * } + * ] + * + * It will convert this data into a nice Transfer object. + */ + public static class TransferDeserializer implements JsonDeserializer { + + @Override + public Transfer 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 serializedTransfer = json.getAsJsonArray(); + if(serializedTransfer.get(0).getAsInt() != OperationType.transfer_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(serializedTransfer.get(1), Transfer.class); + } + }else{ + // This block is called in the second recursion and takes care of deserializing the + // transfer data itself. + JsonObject jsonObject = json.getAsJsonObject(); + + // Deserializing AssetAmount objects + AssetAmount amount = context.deserialize(jsonObject.get("amount"), AssetAmount.class); + AssetAmount fee = context.deserialize(jsonObject.get("fee"), AssetAmount.class); + + // Deserializing UserAccount objects + UserAccount from = new UserAccount(jsonObject.get("from").getAsString()); + UserAccount to = new UserAccount(jsonObject.get("to").getAsString()); + Transfer transfer = new Transfer(from, to, amount, fee); + return transfer; + } + } + } } diff --git a/src/main/java/com/luminiasoft/bitshares/models/ApiCall.java b/src/main/java/com/luminiasoft/bitshares/models/ApiCall.java index 2b36736..c5e57a3 100644 --- a/src/main/java/com/luminiasoft/bitshares/models/ApiCall.java +++ b/src/main/java/com/luminiasoft/bitshares/models/ApiCall.java @@ -66,9 +66,12 @@ public class ApiCall implements JsonSerializable { JsonArray methodParams = new JsonArray(); for(int i = 0; i < this.params.size(); i++){ - if(this.params.get(i) instanceof JsonSerializable){ + 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)); diff --git a/src/main/java/com/luminiasoft/bitshares/models/HistoricalTransfer.java b/src/main/java/com/luminiasoft/bitshares/models/HistoricalTransfer.java new file mode 100644 index 0000000..763fbed --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/models/HistoricalTransfer.java @@ -0,0 +1,22 @@ +package com.luminiasoft.bitshares.models; + +import com.luminiasoft.bitshares.BaseOperation; +import com.luminiasoft.bitshares.GrapheneObject; +import com.luminiasoft.bitshares.Transfer; + +/** + * 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 { + public String id; + public Transfer op; + public Object[] result; + public long block_num; + public long trx_in_block; + public long op_in_trx; + public long virtual_op; +} diff --git a/src/main/java/com/luminiasoft/bitshares/test/NaiveSSLContext.java b/src/main/java/com/luminiasoft/bitshares/test/NaiveSSLContext.java new file mode 100644 index 0000000..a54d8ab --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/test/NaiveSSLContext.java @@ -0,0 +1,126 @@ +package com.luminiasoft.bitshares.test; + +/* + * Copyright (C) 2015 Neo Visionaries Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + + +/** + * A factory class which creates an {@link SSLContext} that + * naively accepts all certificates without verification. + * + *
+ * // Create an SSL context that naively accepts all certificates.
+ * SSLContext context = NaiveSSLContext.getInstance("TLS");
+ *
+ * // Create a socket factory from the SSL context.
+ * SSLSocketFactory factory = context.getSocketFactory();
+ *
+ * // Create a socket from the socket factory.
+ * SSLSocket socket = factory.createSocket("www.example.com", 443);
+ * 
+ * + * @author Takahiko Kawasaki + */ +public class NaiveSSLContext +{ + private NaiveSSLContext() + { + } + + + /** + * Get an SSLContext that implements the specified secure + * socket protocol and naively accepts all certificates + * without verification. + */ + public static SSLContext getInstance(String protocol) throws NoSuchAlgorithmException + { + return init(SSLContext.getInstance(protocol)); + } + + + /** + * Get an SSLContext that implements the specified secure + * socket protocol and naively accepts all certificates + * without verification. + */ + public static SSLContext getInstance(String protocol, Provider provider) throws NoSuchAlgorithmException + { + return init(SSLContext.getInstance(protocol, provider)); + } + + + /** + * Get an SSLContext that implements the specified secure + * socket protocol and naively accepts all certificates + * without verification. + */ + public static SSLContext getInstance(String protocol, String provider) throws NoSuchAlgorithmException, NoSuchProviderException + { + return init(SSLContext.getInstance(protocol, provider)); + } + + + /** + * Set NaiveTrustManager to the given context. + */ + private static SSLContext init(SSLContext context) + { + try + { + // Set NaiveTrustManager. + context.init(null, new TrustManager[] { new NaiveTrustManager() }, null); + } + catch (KeyManagementException e) + { + throw new RuntimeException("Failed to initialize an SSLContext.", e); + } + + return context; + } + + + /** + * A {@link TrustManager} which trusts all certificates naively. + */ + private static class NaiveTrustManager implements X509TrustManager + { + @Override + public X509Certificate[] getAcceptedIssuers() + { + return null; + } + + + public void checkClientTrusted(X509Certificate[] certs, String authType) + { + } + + + public void checkServerTrusted(X509Certificate[] certs, String authType) + { + } + } +} \ No newline at end of file diff --git a/src/main/java/com/luminiasoft/bitshares/ws/GetRelativeAccountHistory.java b/src/main/java/com/luminiasoft/bitshares/ws/GetRelativeAccountHistory.java new file mode 100644 index 0000000..08af08e --- /dev/null +++ b/src/main/java/com/luminiasoft/bitshares/ws/GetRelativeAccountHistory.java @@ -0,0 +1,146 @@ +package com.luminiasoft.bitshares.ws; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.luminiasoft.bitshares.AssetAmount; +import com.luminiasoft.bitshares.RPC; +import com.luminiasoft.bitshares.Transfer; +import com.luminiasoft.bitshares.UserAccount; +import com.luminiasoft.bitshares.interfaces.WitnessResponseListener; +import com.luminiasoft.bitshares.models.ApiCall; +import com.luminiasoft.bitshares.models.BaseResponse; +import com.luminiasoft.bitshares.models.HistoricalTransfer; +import com.luminiasoft.bitshares.models.WitnessResponse; +import com.neovisionaries.ws.client.WebSocket; +import com.neovisionaries.ws.client.WebSocketAdapter; +import com.neovisionaries.ws.client.WebSocketException; +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; + +/** + * Class used to encapsulate the communication sequence used to retrieve the transaction history of + * a given user. + */ +public class GetRelativeAccountHistory extends WebSocketAdapter { + // Sequence of message ids + private final static int LOGIN_ID = 1; + private final static int GET_HISTORY_ID = 2; + private final static int GET_HISTORY_DATA = 3; + + // Default value constants + public static final int DEFAULT_STOP = 0; + public static final int DEFAULT_START = 0; + public static final int MAX_LIMIT = 100; + + // API call parameters + private UserAccount mUserAccount; + private int stop; + private int limit; + private int start; + private WitnessResponseListener mListener; + + private int currentId = 1; + private int apiId = -1; + + /** + * Constructor that takes all possible parameters. + * @param userAccount The user account to be queried + * @param stop Sequence number of earliest operation + * @param limit Maximum number of operations to retrieve (must not exceed 100) + * @param start Sequence number of the most recent operation to retrieve + * @param listener Listener to be notified with the result of this query + */ + public GetRelativeAccountHistory(UserAccount userAccount, int stop, int limit, int start, WitnessResponseListener listener){ + if(limit > MAX_LIMIT) limit = MAX_LIMIT; + this.mUserAccount = userAccount; + this.stop = stop; + this.limit = limit; + this.start = start; + this.mListener = listener; + } + + /** + * Constructor that uses the default values, and sets the limit to its maximum possible value. + * @param userAccount The user account to be queried + * @param listener Listener to be notified with the result of this query + */ + public GetRelativeAccountHistory(UserAccount userAccount, WitnessResponseListener listener){ + this.mUserAccount = userAccount; + this.stop = DEFAULT_STOP; + this.limit = MAX_LIMIT; + this.start = DEFAULT_START; + this.mListener = listener; + } + + @Override + public void onConnected(WebSocket websocket, Map> headers) throws Exception { + ArrayList loginParams = new ArrayList<>(); + loginParams.add(null); + loginParams.add(null); + ApiCall loginCall = new ApiCall(1, RPC.CALL_LOGIN, loginParams, "2.0", currentId); + websocket.sendText(loginCall.toJsonString()); + } + + @Override + public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + String response = frame.getPayloadText(); + System.out.println("<<< "+response); + Gson gson = new Gson(); + BaseResponse baseResponse = gson.fromJson(response, BaseResponse.class); + if(baseResponse.error != null){ + mListener.onError(baseResponse.error); + websocket.disconnect(); + }else{ + currentId++; + ArrayList emptyParams = new ArrayList<>(); + if(baseResponse.id == LOGIN_ID){ + ApiCall getRelativeAccountHistoryId = new ApiCall(1, RPC.CALL_HISTORY, emptyParams, RPC.VERSION, currentId); + websocket.sendText(getRelativeAccountHistoryId.toJsonString()); + }else if(baseResponse.id == GET_HISTORY_ID){ + Type ApiIdResponse = new TypeToken>() {}.getType(); + WitnessResponse witnessResponse = gson.fromJson(response, ApiIdResponse); + apiId = witnessResponse.result.intValue(); + + ArrayList params = new ArrayList<>(); + params.add(mUserAccount.toJsonString()); + params.add(this.stop); + params.add(this.limit); + params.add(this.start); + + ApiCall getRelativeAccountHistoryCall = new ApiCall(apiId, RPC.CALL_GET_RELATIVE_ACCOUNT_HISTORY, params, RPC.VERSION, currentId); + websocket.sendText(getRelativeAccountHistoryCall.toJsonString()); + }else if(baseResponse.id == GET_HISTORY_DATA){ + System.out.println(frame.getPayloadText()); + Type RelativeAccountHistoryResponse = new TypeToken>>(){}.getType(); + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(Transfer.class, new Transfer.TransferDeserializer()); + gsonBuilder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetDeserializer()); + WitnessResponse> transfersResponse = gsonBuilder.create().fromJson(response, RelativeAccountHistoryResponse); + mListener.onSuccess(transfersResponse); + websocket.disconnect(); + } + } + } + + @Override + public void onFrameSent(WebSocket websocket, WebSocketFrame frame) throws Exception { + if(frame.isTextFrame()) + System.out.println(">>> "+frame.getPayloadText()); + } + + @Override + public void onError(WebSocket websocket, WebSocketException cause) throws Exception { + System.out.println("onError. Msg: "+cause.getMessage()); + } + + @Override + public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception { + System.out.println("handleCallbackError. Msg: "+cause.getMessage()); + } +}