From aa642edce9188185a68695e196244291beb88dc5 Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Wed, 22 Mar 2017 16:25:55 -0500 Subject: [PATCH] Added support for the limit_order_cancel_operation --- .../de/bitsharesmunich/graphenej/Main.java | 4 +- .../de/bitsharesmunich/graphenej/Test.java | 10 +- .../graphenej/AssetAmount.java | 3 + .../graphenej/BaseOperation.java | 8 +- .../bitsharesmunich/graphenej/LimitOrder.java | 131 ++++++++++++- .../graphenej/api/GetLimitOrders.java | 1 + .../operations/LimitOrderCancelOperation.java | 61 +++++++ .../operations/LimitOrderCreateOperation.java | 3 +- .../graphenej/TransactionTest.java | 172 ++++++++++++++++-- .../LimitOrderCancelOperationTest.java | 32 ++++ 10 files changed, 395 insertions(+), 30 deletions(-) create mode 100644 graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCancelOperation.java create mode 100644 graphenej/src/test/java/de/bitsharesmunich/graphenej/operations/LimitOrderCancelOperationTest.java diff --git a/app/src/main/java/de/bitsharesmunich/graphenej/Main.java b/app/src/main/java/de/bitsharesmunich/graphenej/Main.java index 697ed2d..19bd618 100644 --- a/app/src/main/java/de/bitsharesmunich/graphenej/Main.java +++ b/app/src/main/java/de/bitsharesmunich/graphenej/Main.java @@ -88,8 +88,8 @@ public class Main { // test.testListAssets(); // test.testGetObjects(); // test.testGetBlockHeader(); -// test.testGetLimitOrders(); - test.testGetTradeHistory(); + test.testGetLimitOrders(); +// test.testGetTradeHistory(); // test.testAssetSerialization(); // test.testGetMarketHistory(); // test.testGetAccountBalances(); diff --git a/app/src/main/java/de/bitsharesmunich/graphenej/Test.java b/app/src/main/java/de/bitsharesmunich/graphenej/Test.java index f0decee..962d841 100644 --- a/app/src/main/java/de/bitsharesmunich/graphenej/Test.java +++ b/app/src/main/java/de/bitsharesmunich/graphenej/Test.java @@ -1137,7 +1137,7 @@ public class Test { WebSocket mWebSocket = factory.createSocket(BLOCK_PAY_DE); - Asset base = new Asset("1.3.120", "EUR", 4); + Asset base = new Asset("1.3.0", "BTS", 4); Asset quote = new Asset("1.3.121", "USD", 4); mWebSocket.addListener(new GetLimitOrders(base.getObjectId(), quote.getObjectId(), 100, new WitnessResponseListener() { @@ -1150,10 +1150,10 @@ public class Test { // System.out.println(String.format("base: %d, quote: %d", // order.sell_price.base.getAmount().longValue(), // order.sell_price.quote.getAmount().longValue())); - order.sell_price.base.getAsset().setPrecision(base.getPrecision()); - order.sell_price.quote.getAsset().setPrecision(quote.getPrecision()); - double baseToQuoteExchange = converter.getConversionRate(order.sell_price, Converter.BASE_TO_QUOTE); - double quoteToBaseExchange = converter.getConversionRate(order.sell_price, Converter.QUOTE_TO_BASE); + order.getSellPrice().base.getAsset().setPrecision(base.getPrecision()); + order.getSellPrice().quote.getAsset().setPrecision(quote.getPrecision()); + double baseToQuoteExchange = converter.getConversionRate(order.getSellPrice(), Converter.BASE_TO_QUOTE); + double quoteToBaseExchange = converter.getConversionRate(order.getSellPrice(), Converter.QUOTE_TO_BASE); System.out.println(String.format("base to quote: %.5f, quote to base: %.5f", baseToQuoteExchange, quoteToBaseExchange)); } } diff --git a/graphenej/src/main/java/de/bitsharesmunich/graphenej/AssetAmount.java b/graphenej/src/main/java/de/bitsharesmunich/graphenej/AssetAmount.java index 135d7d8..76abc69 100644 --- a/graphenej/src/main/java/de/bitsharesmunich/graphenej/AssetAmount.java +++ b/graphenej/src/main/java/de/bitsharesmunich/graphenej/AssetAmount.java @@ -129,6 +129,9 @@ public class AssetAmount implements ByteSerializable, JsonSerializable { } } + /** + * Custom deserializer used for this class + */ public static class AssetAmountDeserializer implements JsonDeserializer { @Override diff --git a/graphenej/src/main/java/de/bitsharesmunich/graphenej/BaseOperation.java b/graphenej/src/main/java/de/bitsharesmunich/graphenej/BaseOperation.java index 36bdf10..9a727cd 100644 --- a/graphenej/src/main/java/de/bitsharesmunich/graphenej/BaseOperation.java +++ b/graphenej/src/main/java/de/bitsharesmunich/graphenej/BaseOperation.java @@ -1,5 +1,7 @@ package de.bitsharesmunich.graphenej; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import de.bitsharesmunich.graphenej.interfaces.ByteSerializable; import de.bitsharesmunich.graphenej.interfaces.JsonSerializable; @@ -25,5 +27,9 @@ public abstract class BaseOperation implements ByteSerializable, JsonSerializabl public abstract void setFee(AssetAmount assetAmount); - public abstract byte[] toBytes(); + public JsonElement toJsonObject(){ + JsonArray array = new JsonArray(); + array.add(this.getId()); + return array; + } } diff --git a/graphenej/src/main/java/de/bitsharesmunich/graphenej/LimitOrder.java b/graphenej/src/main/java/de/bitsharesmunich/graphenej/LimitOrder.java index 010a5e3..af950c8 100644 --- a/graphenej/src/main/java/de/bitsharesmunich/graphenej/LimitOrder.java +++ b/graphenej/src/main/java/de/bitsharesmunich/graphenej/LimitOrder.java @@ -1,14 +1,131 @@ package de.bitsharesmunich.graphenej; +import com.google.gson.*; +import de.bitsharesmunich.graphenej.interfaces.ByteSerializable; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.Type; + /** * * @author henry */ -public class LimitOrder { - public String id; - public String expiration; - public UserAccount seller; - public long for_sale; - public long deferred_fee; - public Price sell_price; +public class LimitOrder extends GrapheneObject implements ByteSerializable { + + public static final String KEY_EXPIRATION = "expiration"; + public static final String KEY_SELLER = "seller"; + public static final String KEY_FOR_SALE = "for_sale"; + public static final String KEY_DEFERRED_FEE = "deferred_fee"; + public static final String KEY_PRICE = "key_price"; + + private String expiration; + private UserAccount seller; + private long forSale; + private long deferredFee; + private Price sellPrice; + + public LimitOrder(String id) { + super(id); + } + + public String getExpiration() { + return expiration; + } + + public void setExpiration(String expiration) { + this.expiration = expiration; + } + + public UserAccount getSeller() { + return seller; + } + + public void setSeller(UserAccount seller) { + this.seller = seller; + } + + public long getForSale() { + return forSale; + } + + public void setForSale(long forSale) { + this.forSale = forSale; + } + + public long getDeferredFee() { + return deferredFee; + } + + public void setDeferredFee(long deferredFee) { + this.deferredFee = deferredFee; + } + + public Price getSellPrice() { + return sellPrice; + } + + public void setSellPrice(Price sellPrice) { + this.sellPrice = sellPrice; + } + + @Override + public byte[] toBytes() { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DataOutput out = new DataOutputStream(byteArrayOutputStream); + byte[] serialized = null; + try { + Varint.writeUnsignedVarLong(this.instance, out); + serialized = byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } + return serialized; + } + + /** + * Custom deserializer for the LimitOrder class, used to deserialize a json-formatted string in + * the following format: + * + * { + * "id": "1.7.2389233", + * "expiration": "2017-04-21T15:40:04", + * "seller": "1.2.114363", + * "forSale": "10564959415", + * "sell_price": { + * "base": { + * "amount": "10565237932", + * "asset_id": "1.3.0" + * }, + * "quote": { + * "amount": 5803878, + * "asset_id": "1.3.121" + * } + * }, + * "deferredFee": 0 + * } + */ + public static class LimitOrderDeserializer implements JsonDeserializer { + + @Override + public LimitOrder deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject object = json.getAsJsonObject(); + String id = object.get(KEY_ID).getAsString(); + String expiration = object.get(KEY_EXPIRATION).getAsString(); + UserAccount seller = context.deserialize(object.get(KEY_SELLER), UserAccount.class); + String forSale = object.get(KEY_FOR_SALE).getAsString(); + Price price = context.deserialize(object.get(KEY_PRICE), Price.class); + long deferredFee = object.get(KEY_DEFERRED_FEE).getAsLong(); + + LimitOrder limitOrder = new LimitOrder(id); + limitOrder.setExpiration(expiration); + limitOrder.setSeller(seller); + limitOrder.setForSale(Long.parseLong(forSale)); + limitOrder.setSellPrice(price); + limitOrder.setDeferredFee(deferredFee); + return limitOrder; + } + } } diff --git a/graphenej/src/main/java/de/bitsharesmunich/graphenej/api/GetLimitOrders.java b/graphenej/src/main/java/de/bitsharesmunich/graphenej/api/GetLimitOrders.java index d018661..60c7f4d 100644 --- a/graphenej/src/main/java/de/bitsharesmunich/graphenej/api/GetLimitOrders.java +++ b/graphenej/src/main/java/de/bitsharesmunich/graphenej/api/GetLimitOrders.java @@ -59,6 +59,7 @@ public class GetLimitOrders extends WebSocketAdapter { Type GetLimitOrdersResponse = new TypeToken>>() {}.getType(); builder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer()); builder.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer()); + builder.registerTypeAdapter(LimitOrder.class, new LimitOrder.LimitOrderDeserializer()); WitnessResponse> witnessResponse = builder.create().fromJson(response, GetLimitOrdersResponse); if (witnessResponse.error != null) { this.mListener.onError(witnessResponse.error); diff --git a/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCancelOperation.java b/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCancelOperation.java new file mode 100644 index 0000000..0334255 --- /dev/null +++ b/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCancelOperation.java @@ -0,0 +1,61 @@ +package de.bitsharesmunich.graphenej.operations; + +import com.google.common.primitives.Bytes; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.bitsharesmunich.graphenej.*; + +/** + * Created by nelson on 3/21/17. + */ +public class LimitOrderCancelOperation extends BaseOperation { + + // Constants used in the JSON representation + public static final String KEY_FEE_PAYING_ACCOUNT = "fee_paying_account"; + public static final String KEY_ORDER_ID = "order"; + + + public LimitOrderCancelOperation(LimitOrder order, UserAccount feePayingAccount) { + super(OperationType.LIMIT_ORDER_CANCEL_OPERATION); + this.order = order; + this.feePayingAccount = feePayingAccount; + } + + // Inner fields of a limit order cancel operation + private AssetAmount fee; + private UserAccount feePayingAccount; + private LimitOrder order; + + @Override + public String toJsonString() { + return null; + } + + @Override + public JsonElement toJsonObject() { + JsonArray array = (JsonArray) super.toJsonObject(); + JsonObject jsonObject = new JsonObject(); + if(fee != null) + jsonObject.add(KEY_FEE, fee.toJsonObject()); + jsonObject.addProperty(KEY_FEE_PAYING_ACCOUNT, feePayingAccount.getObjectId()); + jsonObject.addProperty(KEY_ORDER_ID, order.getObjectId()); + jsonObject.add(KEY_EXTENSIONS, new JsonArray()); + array.add(jsonObject); + return array; + } + + @Override + public void setFee(AssetAmount assetAmount) { + this.fee = assetAmount; + } + + @Override + public byte[] toBytes() { + byte[] feeBytes = this.fee.toBytes(); + byte[] feePayingAccountBytes = this.feePayingAccount.toBytes(); + byte[] orderIdBytes = this.order.toBytes(); + byte[] extensions = this.extensions.toBytes(); + return Bytes.concat(feeBytes, feePayingAccountBytes, orderIdBytes, extensions); + } +} diff --git a/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCreateOperation.java b/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCreateOperation.java index 50a0ad9..7b60c70 100644 --- a/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCreateOperation.java +++ b/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCreateOperation.java @@ -65,8 +65,7 @@ public class LimitOrderCreateOperation extends BaseOperation { @Override public JsonElement toJsonObject() { - JsonArray array = new JsonArray(); - array.add(this.getId()); + JsonArray array = (JsonArray) super.toJsonObject(); JsonObject jsonObject = new JsonObject(); if(fee != null) jsonObject.add(KEY_FEE, fee.toJsonObject()); diff --git a/graphenej/src/test/java/de/bitsharesmunich/graphenej/TransactionTest.java b/graphenej/src/test/java/de/bitsharesmunich/graphenej/TransactionTest.java index 15aee31..b0c00af 100644 --- a/graphenej/src/test/java/de/bitsharesmunich/graphenej/TransactionTest.java +++ b/graphenej/src/test/java/de/bitsharesmunich/graphenej/TransactionTest.java @@ -4,15 +4,13 @@ import com.google.common.primitives.UnsignedLong; import com.neovisionaries.ws.client.WebSocket; import com.neovisionaries.ws.client.WebSocketException; import com.neovisionaries.ws.client.WebSocketFactory; +import de.bitsharesmunich.graphenej.api.GetLimitOrders; import de.bitsharesmunich.graphenej.api.TransactionBroadcastSequence; import de.bitsharesmunich.graphenej.interfaces.WitnessResponseListener; import de.bitsharesmunich.graphenej.models.BaseResponse; import de.bitsharesmunich.graphenej.models.WitnessResponse; import de.bitsharesmunich.graphenej.objects.Memo; -import de.bitsharesmunich.graphenej.operations.LimitOrderCreateOperation; -import de.bitsharesmunich.graphenej.operations.LimitOrderCreateOperationTest; -import de.bitsharesmunich.graphenej.operations.TransferOperation; -import de.bitsharesmunich.graphenej.operations.TransferOperationBuilder; +import de.bitsharesmunich.graphenej.operations.*; import de.bitsharesmunich.graphenej.test.NaiveSSLContext; import org.bitcoinj.core.ECKey; import org.junit.Assert; @@ -50,14 +48,22 @@ public class TransactionTest { private AssetAmount minToReceive = new AssetAmount(UnsignedLong.valueOf(520), BIT_USD); private long expiration; + private static final class Lock { } + private final Object lockObject = new Lock(); + + /** + * Generic witness response listener that will just release the lock created in + * main thread. + */ WitnessResponseListener listener = new WitnessResponseListener() { + @Override public void onSuccess(WitnessResponse response) { System.out.println("onSuccess"); WitnessResponse witnessResponse = response; Assert.assertNull(witnessResponse.result); synchronized (this){ - notifyAll(); + this.notifyAll(); } } @@ -74,7 +80,6 @@ public class TransactionTest { @Before public void setup(){ - } /** @@ -82,8 +87,9 @@ public class TransactionTest { * @param privateKey: The private key used to sign the transaction. * @param operationList: The list of operations to include * @param responseListener: The response listener. + * @param lockObject: Optional object to use as a lock */ - private void broadcastTransaction(ECKey privateKey, List operationList, WitnessResponseListener responseListener) { + private void broadcastTransaction(ECKey privateKey, List operationList, WitnessResponseListener responseListener, Object lockObject) { try{ Transaction transaction = new Transaction(privateKey, null, operationList); @@ -96,11 +102,19 @@ public class TransactionTest { WebSocket mWebSocket = factory.createSocket(BLOCK_PAY_DE); - mWebSocket.addListener(new TransactionBroadcastSequence(transaction, CORE_ASSET, listener)); + mWebSocket.addListener(new TransactionBroadcastSequence(transaction, CORE_ASSET, responseListener)); mWebSocket.connect(); - synchronized (responseListener){ - responseListener.wait(); - System.out.println("Wait released"); + + // If a lock object is specified, we use it + if(lockObject != null){ + synchronized (lockObject){ + lockObject.wait(); + } + }else{ + // Otherwise we just use this listener as the lock + synchronized (this){ + this.wait(); + } } }catch(NoSuchAlgorithmException e){ System.out.println("NoSuchAlgoritmException. Msg: " + e.getMessage()); @@ -147,7 +161,7 @@ public class TransactionTest { operationList.add(transferOperation2); // Broadcasting transaction - broadcastTransaction(sourcePrivateKey, operationList, listener); + broadcastTransaction(sourcePrivateKey, operationList, listener, null); } @Test @@ -163,6 +177,138 @@ public class TransactionTest { operationList.add(operation); // Broadcasting transaction - broadcastTransaction(privateKey, operationList, listener); + broadcastTransaction(privateKey, operationList, listener, null); + } + + /** + * Since tests should be independent of each other, in order to be able to test the cancellation of an + * existing order we must first proceed to create one. And after creating one, we must also retrieve + * its id in a separate call. + * + * All of this just makes this test a bit more complex, since we have 3 clearly defined tasks that require + * network communication + * + * 1- Create order + * 2- Retrieve order id + * 3- Send order cancellation tx + * + * Only the last one is what we actually want to test + * + * @throws NoSuchAlgorithmException + * @throws IOException + * @throws WebSocketException + */ + @Test + public void testLimitOrderCancelTransaction() throws NoSuchAlgorithmException, IOException, WebSocketException { + + // We first must create a limit order for this test + ECKey privateKey = new BrainKey(BILTHON_15_BRAIN_KEY, 0).getPrivateKey(); + expiration = (System.currentTimeMillis() / 1000) + 60 * 5; + + // Creating limit order creation operation + LimitOrderCreateOperation operation = new LimitOrderCreateOperation(seller, amountToSell, minToReceive, (int) expiration, false); + operation.setFee(new AssetAmount(UnsignedLong.valueOf(2), CORE_ASSET)); + + ArrayList operationList = new ArrayList<>(); + operationList.add(operation); + + // Broadcasting transaction (Task 1) + broadcastTransaction(privateKey, operationList, new WitnessResponseListener() { + + @Override + public void onSuccess(WitnessResponse response) { + + System.out.println("onSuccess.0"); + try{ + // Setting up the assets + Asset base = amountToSell.getAsset(); + Asset quote = minToReceive.getAsset(); + + SSLContext context = NaiveSSLContext.getInstance("TLS"); + WebSocketFactory factory = new WebSocketFactory(); + + // Set the custom SSL context. + factory.setSSLContext(context); + WebSocket mWebSocket = factory.createSocket(BLOCK_PAY_DE); + + // Requesting limit order to cancel (Task 2) + mWebSocket.addListener(new GetLimitOrders(base.getObjectId(), quote.getObjectId(), 100, new WitnessResponseListener() { + + @Override + public void onSuccess(WitnessResponse response) { + System.out.println("onSuccess.1"); + List orders = (List) response.result; + for(LimitOrder order : orders){ + if(order.getSeller().getObjectId().equals(bilthon_15.getObjectId())){ + + // Instantiating a private key for bilthon-15 + ECKey privateKey = new BrainKey(BILTHON_15_BRAIN_KEY, 0).getPrivateKey(); + + // Creating limit order cancellation operation + LimitOrderCancelOperation operation = new LimitOrderCancelOperation(order, bilthon_15); + ArrayList operationList = new ArrayList<>(); + operationList.add(operation); + + // Broadcasting order cancellation tx (Task 3) + broadcastTransaction(privateKey, operationList, new WitnessResponseListener() { + + @Override + public void onSuccess(WitnessResponse response) { + System.out.println("onSuccess.2"); + Assert.assertNull(response.result); + synchronized (this){ + notifyAll(); + } + synchronized (lockObject){ + lockObject.notifyAll(); + } + } + + @Override + public void onError(BaseResponse.Error error) { + System.out.println("onError.2"); + Assert.assertNull(error); + synchronized (this){ + notifyAll(); + } + synchronized (lockObject){ + lockObject.notifyAll(); + } + } + }, null); + } + } + } + + @Override + public void onError(BaseResponse.Error error) { + System.out.println("onError.1"); + System.out.println(error.data.message); + Assert.assertNull(error); + synchronized (lockObject){ + lockObject.notifyAll(); + } + } + })); + + mWebSocket.connect(); + + }catch(NoSuchAlgorithmException e){ + System.out.println("NoSuchAlgorithmException. Msg: "+e.getMessage()); + } catch (WebSocketException e) { + System.out.println("WebSocketException. Msg: "+e.getMessage()); + } catch (IOException e) { + System.out.println("IOException. Msg: "+e.getMessage()); + } + } + + @Override + public void onError(BaseResponse.Error error) { + System.out.println("OnError. Msg: "+error.message); + synchronized (this){ + notifyAll(); + } + } + }, lockObject); } } \ No newline at end of file diff --git a/graphenej/src/test/java/de/bitsharesmunich/graphenej/operations/LimitOrderCancelOperationTest.java b/graphenej/src/test/java/de/bitsharesmunich/graphenej/operations/LimitOrderCancelOperationTest.java new file mode 100644 index 0000000..a065d45 --- /dev/null +++ b/graphenej/src/test/java/de/bitsharesmunich/graphenej/operations/LimitOrderCancelOperationTest.java @@ -0,0 +1,32 @@ +package de.bitsharesmunich.graphenej.operations; + +import com.google.common.primitives.UnsignedLong; +import de.bitsharesmunich.graphenej.*; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + +/** + * Created by nelson on 3/21/17. + */ +public class LimitOrderCancelOperationTest { + private static final Asset CORE_ASSET = new Asset("1.3.0"); + private UserAccount feePayingAccount; + private LimitOrder limitOrder; + + @Before + public void setup(){ + feePayingAccount = new UserAccount("1.2.143563"); + limitOrder = new LimitOrder("1.7.2360289"); + } + + @Test + public void toBytes() throws Exception { + LimitOrderCancelOperation operation = new LimitOrderCancelOperation(limitOrder, feePayingAccount); + operation.setFee(new AssetAmount(UnsignedLong.valueOf(2), CORE_ASSET)); + byte[] serialized = operation.toBytes(); + assertArrayEquals("Correct serialization", Util.hexToBytes("020000000000000000cbe108e187900100"), serialized); + } +} \ No newline at end of file