diff --git a/graphenej/src/main/java/cy/agorise/graphenej/GrapheneObject.java b/graphenej/src/main/java/cy/agorise/graphenej/GrapheneObject.java
index 4da98a0..865df39 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/GrapheneObject.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/GrapheneObject.java
@@ -80,6 +80,8 @@ public class GrapheneObject {
return ObjectType.WORKER_OBJECT;
case 15:
return ObjectType.BALANCE_OBJECT;
+ case 16:
+ return ObjectType.HTLC_OBJECT;
}
case IMPLEMENTATION_SPACE:
switch(type){
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/Htlc.java b/graphenej/src/main/java/cy/agorise/graphenej/Htlc.java
new file mode 100644
index 0000000..6fc462d
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/Htlc.java
@@ -0,0 +1,43 @@
+package cy.agorise.graphenej;
+
+import com.google.gson.JsonElement;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import cy.agorise.graphenej.interfaces.ByteSerializable;
+import cy.agorise.graphenej.interfaces.JsonSerializable;
+
+/**
+ * Class used to represent an existing HTLC contract.
+ */
+public class Htlc extends GrapheneObject implements ByteSerializable, JsonSerializable {
+
+ public Htlc(String id) {
+ super(id);
+ }
+
+ @Override
+ public byte[] toBytes() {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ DataOutput out = new DataOutputStream(byteArrayOutputStream);
+ try {
+ Varint.writeUnsignedVarLong(this.instance, out);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ @Override
+ public String toJsonString() {
+ return this.getObjectId();
+ }
+
+ @Override
+ public JsonElement toJsonObject() {
+ return null;
+ }
+}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java
new file mode 100644
index 0000000..aa0b6d1
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java
@@ -0,0 +1,45 @@
+package cy.agorise.graphenej;
+
+import com.google.common.primitives.Bytes;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+
+import cy.agorise.graphenej.interfaces.ByteSerializable;
+import cy.agorise.graphenej.interfaces.JsonSerializable;
+
+/**
+ * Class used to represent a HTLC hash.
+ */
+public class HtlcHash implements ByteSerializable, JsonSerializable {
+ private HtlcHashType hashType;
+ private byte[] hash;
+
+ public HtlcHash(HtlcHashType hashType, byte[] hash) {
+ this.hashType = hashType;
+ this.hash = hash;
+ }
+
+ public HtlcHashType getType(){
+ return this.hashType;
+ }
+
+ @Override
+ public byte[] toBytes() {
+ byte[] hashTypeBytes = new byte[] { Util.revertInteger(hashType.ordinal())[3] };
+ return Bytes.concat(hashTypeBytes, hash);
+ }
+
+ @Override
+ public String toJsonString() {
+ JsonElement element = toJsonObject();
+ return element.toString();
+ }
+
+ @Override
+ public JsonElement toJsonObject() {
+ JsonArray array = new JsonArray();
+ array.add(hashType.ordinal());
+ array.add(Util.byteToString(hash));
+ return array;
+ }
+}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/HtlcHashType.java b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHashType.java
new file mode 100644
index 0000000..ba66fec
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHashType.java
@@ -0,0 +1,11 @@
+package cy.agorise.graphenej;
+
+/**
+ * Used to enumerate the possible hash algorithms used in HTLCs.
+ * @see htlc.hpp
+ */
+public enum HtlcHashType {
+ RIPEMD160,
+ SHA1,
+ SHA256
+}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/ObjectType.java b/graphenej/src/main/java/cy/agorise/graphenej/ObjectType.java
index ee1ce37..ef49a28 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/ObjectType.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/ObjectType.java
@@ -20,6 +20,7 @@ public enum ObjectType {
VESTING_BALANCE_OBJECT,
WORKER_OBJECT,
BALANCE_OBJECT,
+ HTLC_OBJECT,
GLOBAL_PROPERTY_OBJECT,
DYNAMIC_GLOBAL_PROPERTY_OBJECT,
ASSET_DYNAMIC_DATA,
@@ -53,6 +54,7 @@ public enum ObjectType {
case VESTING_BALANCE_OBJECT:
case WORKER_OBJECT:
case BALANCE_OBJECT:
+ case HTLC_OBJECT:
space = 1;
break;
case GLOBAL_PROPERTY_OBJECT:
@@ -123,6 +125,8 @@ public enum ObjectType {
case BALANCE_OBJECT:
type = 15;
break;
+ case HTLC_OBJECT:
+ type = 16;
case GLOBAL_PROPERTY_OBJECT:
type = 0;
break;
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/OperationType.java b/graphenej/src/main/java/cy/agorise/graphenej/OperationType.java
index 9ea093d..a4d4377 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/OperationType.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/OperationType.java
@@ -51,5 +51,15 @@ public enum OperationType {
BLIND_TRANSFER_OPERATION,
TRANSFER_FROM_BLIND_OPERATION,
ASSET_SETTLE_CANCEL_OPERATION, // VIRTUAL
- ASSET_CLAIM_FEES_OPERATION
+ ASSET_CLAIM_FEES_OPERATION,
+ FBA_DISTRIBUTE_OPERATION,
+ BID_COLLATERAL_OPERATION,
+ EXECUTE_BID_OPERATION, // VIRTUAL
+ ASSET_CLAIM_POOL_OPERATION,
+ ASSET_UPDATE_ISSUER_OPERATION,
+ HTLC_CREATE_OPERATION,
+ HTLC_REDEEM_OPERATION,
+ HTLC_REDEEMED_OPERATION, // VIRTUAL
+ HTLC_EXTEND_OPERATION,
+ HTLC_REFUND_OPERATION // VIRTUAL
}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/Transaction.java b/graphenej/src/main/java/cy/agorise/graphenej/Transaction.java
index 3ea51e3..ff37342 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/Transaction.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/Transaction.java
@@ -417,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;
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/Util.java b/graphenej/src/main/java/cy/agorise/graphenej/Util.java
index 52a743d..c360142 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/Util.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/Util.java
@@ -2,14 +2,25 @@ package cy.agorise.graphenej;
import com.google.common.primitives.Bytes;
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;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;
-import org.tukaani.xz.*;
+import org.tukaani.xz.CorruptedInputException;
+import org.tukaani.xz.FinishableOutputStream;
+import org.tukaani.xz.LZMA2Options;
+import org.tukaani.xz.LZMAInputStream;
+import org.tukaani.xz.LZMAOutputStream;
+import org.tukaani.xz.XZInputStream;
+import org.tukaani.xz.XZOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -378,4 +389,36 @@ public class Util {
public static long toBase(double value, int precision){
return (long) (value * Math.pow(10, precision));
}
+
+ /**
+ * Creates a hash for HTLC operations.
+ *
+ * @param preimage The data we want to operate on.
+ * @param hashType The type of hash.
+ * @return The hash.
+ * @throws NoSuchAlgorithmException
+ */
+ public static byte[] htlcHash(byte[] preimage, HtlcHashType hashType) throws NoSuchAlgorithmException {
+ byte[] out = null;
+ GeneralDigest digest = null;
+ switch(hashType){
+ case RIPEMD160:
+ digest = new RIPEMD160Digest();
+ out = new byte[20];
+ break;
+ case SHA1:
+ digest = new SHA1Digest();
+ out = new byte[20];
+ break;
+ case SHA256:
+ digest = new SHA256Digest();
+ out = new byte[32];
+ break;
+ default:
+ throw new IllegalArgumentException("Not supported hash function!");
+ }
+ digest.update(preimage, 0, preimage.length);
+ digest.doFinal(out, 0);
+ return out;
+ }
}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java b/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java
new file mode 100644
index 0000000..bbd8946
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java
@@ -0,0 +1,146 @@
+package cy.agorise.graphenej.operations;
+
+import com.google.common.primitives.Bytes;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import cy.agorise.graphenej.AssetAmount;
+import cy.agorise.graphenej.BaseOperation;
+import cy.agorise.graphenej.HtlcHash;
+import cy.agorise.graphenej.OperationType;
+import cy.agorise.graphenej.UserAccount;
+import cy.agorise.graphenej.Util;
+
+public class CreateHtlcOperation extends BaseOperation {
+ static final String KEY_FROM = "from";
+ static final String KEY_TO = "to";
+ static final String KEY_AMOUNT = "amount";
+ static final String KEY_PREIMAGE_HASH = "preimage_hash";
+ static final String KEY_PREIMAGE_SIZE = "preimage_size";
+ static final String KEY_CLAIM_PERIOD_SECONDS = "claim_period_seconds";
+
+ private AssetAmount fee;
+ private UserAccount from;
+ private UserAccount to;
+ private AssetAmount amount;
+ private HtlcHash preimageHash;
+ private short preimageSize;
+ private int claimPeriodSeconds;
+
+ /**
+ * Public constructor
+ *
+ * @param fee The operation fee.
+ * @param from The source account.
+ * @param to The destination account.
+ * @param amount The amount to be traded.
+ * @param hash The pre-image hash.
+ * @param preimageSize The pre-image size.
+ * @param claimPeriodSeconds The claim period, in seconds.
+ */
+ public CreateHtlcOperation(AssetAmount fee, UserAccount from, UserAccount to, AssetAmount amount, HtlcHash hash, short preimageSize, int claimPeriodSeconds) {
+ super(OperationType.HTLC_CREATE_OPERATION);
+ this.fee = fee;
+ this.from = from;
+ this.to = to;
+ this.amount = amount;
+ this.preimageHash = hash;
+ this.preimageSize = preimageSize;
+ this.claimPeriodSeconds = claimPeriodSeconds;
+ }
+
+ @Override
+ public void setFee(AssetAmount newFee){
+ this.fee = newFee;
+ }
+
+ public AssetAmount getFee() {
+ return fee;
+ }
+
+ public UserAccount getFrom() {
+ return from;
+ }
+
+ public void setFrom(UserAccount from) {
+ this.from = from;
+ }
+
+ public UserAccount getTo() {
+ return to;
+ }
+
+ public void setTo(UserAccount to) {
+ this.to = to;
+ }
+
+ public AssetAmount getAmount() {
+ return amount;
+ }
+
+ public void setAmount(AssetAmount amount) {
+ this.amount = amount;
+ }
+
+ public HtlcHash getPreimageHash() {
+ return preimageHash;
+ }
+
+ public void setPreimageHash(HtlcHash preimageHash) {
+ this.preimageHash = preimageHash;
+ }
+
+ public short getPreimageSize() {
+ return preimageSize;
+ }
+
+ public void setPreimageSize(short preimageSize) {
+ this.preimageSize = preimageSize;
+ }
+
+ public int getClaimPeriodSeconds() {
+ return claimPeriodSeconds;
+ }
+
+ public void setClaimPeriodSeconds(int claimPeriodSeconds) {
+ this.claimPeriodSeconds = claimPeriodSeconds;
+ }
+
+ @Override
+ public byte[] toBytes() {
+ byte[] feeBytes = fee.toBytes();
+ byte[] fromBytes = from.toBytes();
+ byte[] toBytes = to.toBytes();
+ byte[] amountBytes = amount.toBytes();
+ byte[] htlcHashBytes = preimageHash.toBytes();
+ byte[] preimageSizeBytes = Util.revertShort(preimageSize);
+ byte[] claimPeriodBytes = Util.revertInteger(claimPeriodSeconds);
+ byte[] extensionsBytes = extensions.toBytes();
+ return Bytes.concat(feeBytes, fromBytes, toBytes, amountBytes, htlcHashBytes, preimageSizeBytes, claimPeriodBytes, extensionsBytes);
+ }
+
+ @Override
+ public JsonElement toJsonObject() {
+ JsonArray array = new JsonArray();
+ array.add(this.getId());
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.add(KEY_FEE, fee.toJsonObject());
+ jsonObject.addProperty(KEY_FROM, from.getObjectId());
+ jsonObject.addProperty(KEY_TO, to.getObjectId());
+ jsonObject.add(KEY_AMOUNT, amount.toJsonObject());
+ jsonObject.add(KEY_PREIMAGE_HASH, preimageHash.toJsonObject());
+ jsonObject.addProperty(KEY_PREIMAGE_SIZE, preimageSize);
+ jsonObject.addProperty(KEY_CLAIM_PERIOD_SECONDS, claimPeriodSeconds);
+ jsonObject.add(KEY_EXTENSIONS, new JsonArray());
+ array.add(jsonObject);
+ return array;
+ }
+
+ @Override
+ public String toJsonString() {
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ return gsonBuilder.create().toJson(this);
+ }
+}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/operations/RedeemHtlcOperation.java b/graphenej/src/main/java/cy/agorise/graphenej/operations/RedeemHtlcOperation.java
new file mode 100644
index 0000000..0889cde
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/operations/RedeemHtlcOperation.java
@@ -0,0 +1,111 @@
+package cy.agorise.graphenej.operations;
+
+import com.google.common.primitives.Bytes;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import cy.agorise.graphenej.AssetAmount;
+import cy.agorise.graphenej.BaseOperation;
+import cy.agorise.graphenej.Htlc;
+import cy.agorise.graphenej.OperationType;
+import cy.agorise.graphenej.UserAccount;
+import cy.agorise.graphenej.Util;
+import cy.agorise.graphenej.Varint;
+
+/**
+ * Class used to encapsulate the redeem_htlc operation.
+ */
+public class RedeemHtlcOperation extends BaseOperation {
+ static final String KEY_REDEEMER = "redeemer";
+ static final String KEY_PREIMAGE = "preimage";
+ static final String KEY_HTLC_ID = "htlc_id";
+
+ private AssetAmount fee;
+ private UserAccount redeemer;
+ private Htlc htlc;
+ private byte[] preimage;
+
+ /**
+ * Public constructor
+ *
+ * @param fee The fee associated with this operation.
+ * @param redeemer The user account that will redeem the HTLC.
+ * @param htlc The existing HTLC operation.
+ */
+ public RedeemHtlcOperation(AssetAmount fee, UserAccount redeemer, Htlc htlc, byte[] preimage) {
+ super(OperationType.HTLC_REDEEM_OPERATION);
+ this.fee = fee;
+ this.redeemer = redeemer;
+ this.htlc = htlc;
+ this.preimage = preimage;
+ }
+
+ @Override
+ public void setFee(AssetAmount fee) {
+ this.fee = fee;
+ }
+
+ public AssetAmount getFee(){
+ return this.fee;
+ }
+
+ public UserAccount getRedeemer() {
+ return redeemer;
+ }
+
+ public void setRedeemer(UserAccount redeemer) {
+ this.redeemer = redeemer;
+ }
+
+ public Htlc getHtlc() {
+ return htlc;
+ }
+
+ public void setHtlc(Htlc htlc) {
+ this.htlc = htlc;
+ }
+
+ @Override
+ public byte[] toBytes() {
+ byte[] feeBytes = this.fee.toBytes();
+ byte[] htlcBytes = this.htlc.toBytes();
+ byte[] redeemerBytes = this.redeemer.toBytes();
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ DataOutput out = new DataOutputStream(byteArrayOutputStream);
+ try{
+ Varint.writeUnsignedVarLong(this.preimage.length, out);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ byte[] preimageLength = byteArrayOutputStream.toByteArray();
+ byte[] extensionsBytes = extensions.toBytes();
+ return Bytes.concat(feeBytes, htlcBytes, redeemerBytes, preimageLength, this.preimage, extensionsBytes);
+ }
+
+ @Override
+ public JsonElement toJsonObject() {
+ JsonArray array = new JsonArray();
+ array.add(this.getId());
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.add(KEY_FEE, fee.toJsonObject());
+ jsonObject.addProperty(KEY_REDEEMER, this.redeemer.getObjectId());
+ jsonObject.addProperty(KEY_PREIMAGE, Util.bytesToHex(this.preimage));
+ jsonObject.addProperty(KEY_HTLC_ID, this.htlc.getObjectId());
+ jsonObject.add(KEY_EXTENSIONS, new JsonArray());
+ array.add(jsonObject);
+ return array;
+ }
+
+ @Override
+ public String toJsonString() {
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ return gsonBuilder.create().toJson(this);
+ }
+}
diff --git a/graphenej/src/test/java/cy/agorise/graphenej/HtlcTest.java b/graphenej/src/test/java/cy/agorise/graphenej/HtlcTest.java
new file mode 100644
index 0000000..c5c02c7
--- /dev/null
+++ b/graphenej/src/test/java/cy/agorise/graphenej/HtlcTest.java
@@ -0,0 +1,26 @@
+package cy.agorise.graphenej;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class HtlcTest {
+ private final Htlc htlc = new Htlc("1.16.124");
+
+ @Test
+ public void testByteSerialization(){
+ Htlc htlc1 = new Htlc("1.16.1");
+ Htlc htlc2 = new Htlc("1.16.100");
+ Htlc htlc3 = new Htlc("1.16.500");
+ Htlc htlc4 = new Htlc("1.16.1000");
+
+ byte[] expected_1 = Util.hexToBytes("01");
+ byte[] expected_2 = Util.hexToBytes("64");
+ byte[] expected_3 = Util.hexToBytes("f403");
+ byte[] expected_4 = Util.hexToBytes("e807");
+
+ Assert.assertArrayEquals(expected_1, htlc1.toBytes());
+ Assert.assertArrayEquals(expected_2, htlc2.toBytes());
+ Assert.assertArrayEquals(expected_3, htlc3.toBytes());
+ Assert.assertArrayEquals(expected_4, htlc4.toBytes());
+ }
+}
diff --git a/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java
new file mode 100644
index 0000000..4c38531
--- /dev/null
+++ b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java
@@ -0,0 +1,114 @@
+package cy.agorise.graphenej.operations;
+
+import com.google.common.primitives.Bytes;
+import com.google.common.primitives.UnsignedLong;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.TimeZone;
+
+import cy.agorise.graphenej.Asset;
+import cy.agorise.graphenej.AssetAmount;
+import cy.agorise.graphenej.BaseOperation;
+import cy.agorise.graphenej.BlockData;
+import cy.agorise.graphenej.Chains;
+import cy.agorise.graphenej.HtlcHash;
+import cy.agorise.graphenej.HtlcHashType;
+import cy.agorise.graphenej.Transaction;
+import cy.agorise.graphenej.UserAccount;
+import cy.agorise.graphenej.Util;
+
+public class CreateHtlcOperationTest {
+ private final String SERIALIZED_OP = "0000000000000000007b7c80241100000000000000a06e327ea7388c18e4740e350ed4e60f2e04fc41c8007800000000";
+ private final String SERIALIZED_TX = "f68585abf4dce7c8045701310000000000000000007b7c80241100000000000000a06e327ea7388c18e4740e350ed4e60f2e04fc41c800780000000000";
+ private final String JSON_SERIALIZED_TX = "{\"expiration\":\"2016-04-06T08:29:27\",\"extensions\":[],\"operations\":[[49,{\"amount\":{\"amount\":1123456,\"asset_id\":\"1.3.0\"},\"claim_period_seconds\":120,\"extensions\":[],\"fee\":{\"amount\":0,\"asset_id\":\"1.3.0\"},\"from\":\"1.2.123\",\"preimage_hash\":[0,\"a06e327ea7388c18e4740e350ed4e60f2e04fc41\"],\"preimage_size\":200,\"to\":\"1.2.124\"}]],\"ref_block_num\":34294,\"ref_block_prefix\":3707022213,\"signatures\":[]}";
+ private final String PREIMAGE_HEX = "666f6f626172";
+ private final String HASH_RIPEMD160 = "a06e327ea7388c18e4740e350ed4e60f2e04fc41";
+ private final String HASH_SHA1 = "8843d7f92416211de9ebb963ff4ce28125932878";
+ private final String HASH_SHA256 = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";
+
+ private final Asset CORE = new Asset("1.3.0");
+
+ private CreateHtlcOperation buildCreateHtlcOperation() throws NoSuchAlgorithmException {
+ UserAccount from = new UserAccount("1.2.123");
+ UserAccount to = new UserAccount("1.2.124");
+ AssetAmount fee = new AssetAmount(UnsignedLong.valueOf(0), CORE);
+ AssetAmount amount = new AssetAmount(UnsignedLong.valueOf(1123456), CORE);
+ byte[] hashBytes = Util.htlcHash("foobar".getBytes(), HtlcHashType.RIPEMD160);
+ HtlcHash preimageHash = new HtlcHash(HtlcHashType.RIPEMD160, hashBytes);
+ return new CreateHtlcOperation(fee, from, to, amount, preimageHash, (short) 200, 120);
+ }
+
+ @Test
+ public void testRipemd160(){
+ try {
+ byte[] hashRipemd160 = Util.htlcHash(Util.hexToBytes(PREIMAGE_HEX), HtlcHashType.RIPEMD160);
+ String hexHash = Util.bytesToHex(hashRipemd160);
+ Assert.assertEquals(HASH_RIPEMD160, hexHash);
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Test
+ public void testSha1(){
+ try {
+ byte[] hashSha1 = Util.htlcHash(Util.hexToBytes(PREIMAGE_HEX), HtlcHashType.SHA1);
+ String hexHash = Util.bytesToHex(hashSha1);
+ Assert.assertEquals(HASH_SHA1, hexHash);
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Test
+ public void testSha256(){
+ try {
+ byte[] hashSha256 = Util.htlcHash(Util.hexToBytes(PREIMAGE_HEX), HtlcHashType.SHA256);
+ String hexHash = Util.bytesToHex(hashSha256);
+ Assert.assertEquals(HASH_SHA256, hexHash);
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Test
+ public void testOperationSerialization() throws NoSuchAlgorithmException {
+ CreateHtlcOperation operation = this.buildCreateHtlcOperation();
+ byte[] opBytes = operation.toBytes();
+ Assert.assertArrayEquals(Util.hexToBytes(SERIALIZED_OP), opBytes);
+ }
+
+ @Test
+ public void testTransactionSerialization() throws NoSuchAlgorithmException, ParseException {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
+ Date expirationDate = dateFormat.parse("2016-04-06T08:29:27");
+ BlockData blockData = new BlockData(34294, 3707022213L, (expirationDate.getTime() / 1000));
+ ArrayList operations = new ArrayList<>();
+ operations.add(buildCreateHtlcOperation());
+ Transaction transaction = new Transaction(blockData, operations);
+ // Checking byte serialization
+ byte[] txBytes = transaction.toBytes();
+ byte[] expected = Bytes.concat(Util.hexToBytes(Chains.BITSHARES.CHAIN_ID), Util.hexToBytes(SERIALIZED_TX));
+ Assert.assertArrayEquals(expected, txBytes);
+ // Checking JSON serialization
+ JsonObject jsonObject = transaction.toJsonObject();
+ JsonArray operationsArray = jsonObject.get("operations").getAsJsonArray().get(0).getAsJsonArray();
+ JsonArray hashArray = operationsArray.get(1).getAsJsonObject().get("preimage_hash").getAsJsonArray();
+ Assert.assertEquals("2016-04-06T08:29:27", jsonObject.get("expiration").getAsString());
+ Assert.assertEquals(49, operationsArray.get(0).getAsInt());
+ Assert.assertEquals("1.2.123", operationsArray.get(1).getAsJsonObject().get("from").getAsString());
+ Assert.assertEquals("1.2.124", operationsArray.get(1).getAsJsonObject().get("to").getAsString());
+ Assert.assertEquals(0, hashArray.get(0).getAsInt());
+ Assert.assertEquals(HASH_RIPEMD160, hashArray.get(1).getAsString());
+ }
+}
diff --git a/graphenej/src/test/java/cy/agorise/graphenej/operations/RedeemHtlcOperationTest.java b/graphenej/src/test/java/cy/agorise/graphenej/operations/RedeemHtlcOperationTest.java
new file mode 100644
index 0000000..0ba79db
--- /dev/null
+++ b/graphenej/src/test/java/cy/agorise/graphenej/operations/RedeemHtlcOperationTest.java
@@ -0,0 +1,72 @@
+package cy.agorise.graphenej.operations;
+
+import com.google.common.primitives.Bytes;
+import com.google.common.primitives.UnsignedLong;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.TimeZone;
+
+import cy.agorise.graphenej.Asset;
+import cy.agorise.graphenej.AssetAmount;
+import cy.agorise.graphenej.BaseOperation;
+import cy.agorise.graphenej.BlockData;
+import cy.agorise.graphenej.Chains;
+import cy.agorise.graphenej.Htlc;
+import cy.agorise.graphenej.Transaction;
+import cy.agorise.graphenej.UserAccount;
+import cy.agorise.graphenej.Util;
+
+public class RedeemHtlcOperationTest {
+ private final String SERIALIZED_OP = "00000000000000000084017c06666f6f62617200";
+ private final String SERIALIZED_TX = "f68585abf4dce7c80457013200000000000000000084017c06666f6f6261720000";
+ private final Asset CORE = new Asset("1.3.0");
+ private final String PREIMAGE_HEX = "666f6f626172";
+
+ private RedeemHtlcOperation buildRedeemdHtlcOperation(){
+ AssetAmount fee = new AssetAmount(UnsignedLong.valueOf("0"), CORE);
+ UserAccount redeemer = new UserAccount("1.2.124");
+ Htlc htlc = new Htlc("1.16.132");
+ byte[] preimage = Util.hexToBytes(PREIMAGE_HEX);
+ return new RedeemHtlcOperation(fee, redeemer, htlc, preimage);
+ }
+
+ @Test
+ public void testOperationSerialization(){
+ RedeemHtlcOperation redeemHtlcOperation = this.buildRedeemdHtlcOperation();
+ byte[] opBytes = redeemHtlcOperation.toBytes();
+ Assert.assertArrayEquals(Util.hexToBytes(SERIALIZED_OP), opBytes);
+ }
+
+ @Test
+ public void testTransactionSerialization() throws ParseException {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
+ Date expirationDate = dateFormat.parse("2016-04-06T08:29:27");
+ BlockData blockData = new BlockData(34294, 3707022213L, (expirationDate.getTime() / 1000));
+ ArrayList operations = new ArrayList<>();
+ operations.add(buildRedeemdHtlcOperation());
+ Transaction transaction = new Transaction(blockData, operations);
+ // Checking byte serialization
+ byte[] txBytes = transaction.toBytes();
+ byte[] expected = Bytes.concat(Util.hexToBytes(Chains.BITSHARES.CHAIN_ID), Util.hexToBytes(SERIALIZED_TX));
+ Assert.assertArrayEquals(expected, txBytes);
+ // Checking JSON serialization
+ JsonObject jsonObject = transaction.toJsonObject();
+ JsonArray operationsArray = jsonObject.get("operations").getAsJsonArray().get(0).getAsJsonArray();
+ int operationId = operationsArray.get(0).getAsInt();
+ JsonObject operationJson = operationsArray.get(1).getAsJsonObject();
+ Assert.assertEquals("2016-04-06T08:29:27", jsonObject.get("expiration").getAsString());
+ Assert.assertEquals(50, operationId);
+ Assert.assertEquals("1.16.132", operationJson.getAsJsonObject().get("htlc_id").getAsString());
+ Assert.assertEquals(PREIMAGE_HEX, operationJson.getAsJsonObject().get("preimage").getAsString());
+ Assert.assertEquals("1.2.124", operationJson.getAsJsonObject().get("redeemer").getAsString());
+ }
+}
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index 8ac3474..e4528e8 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -14,10 +14,12 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
+
+ android:theme="@style/AppTheme.NoActionBar" />
diff --git a/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java b/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java
index 00e679b..3e760e5 100644
--- a/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java
+++ b/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java
@@ -27,6 +27,8 @@ public class CallsActivity extends AppCompatActivity {
private static final String RECONNECT_NODE = "reconnect_node";
private static final String TEST_BRAINKEY_DERIVATION = "test_brainkey_derivation";
+ public static final String CREATE_HTLC = "create_htlc";
+ public static final String REDEEM_HTLC = "redeem_htlc";
@BindView(R.id.call_list)
RecyclerView mRecyclerView;
@@ -83,7 +85,9 @@ public class CallsActivity extends AppCompatActivity {
RPC.CALL_BROADCAST_TRANSACTION,
RPC.CALL_GET_TRANSACTION,
RECONNECT_NODE,
- TEST_BRAINKEY_DERIVATION
+ TEST_BRAINKEY_DERIVATION,
+ CREATE_HTLC,
+ REDEEM_HTLC
};
@NonNull
@@ -108,6 +112,9 @@ public class CallsActivity extends AppCompatActivity {
intent = new Intent(CallsActivity.this, RemoveNodeActivity.class);
} else if (selectedCall.equals(TEST_BRAINKEY_DERIVATION)){
intent = new Intent(CallsActivity.this, BrainkeyActivity.class);
+ } else if (selectedCall.equals(CREATE_HTLC) || selectedCall.equals(REDEEM_HTLC)){
+ intent = new Intent(CallsActivity.this, HtlcActivity.class);
+ intent.putExtra(Constants.KEY_SELECTED_CALL, selectedCall);
} else {
intent = new Intent(CallsActivity.this, PerformCallActivity.class);
intent.putExtra(Constants.KEY_SELECTED_CALL, selectedCall);
diff --git a/sample/src/main/java/cy/agorise/labs/sample/HtlcActivity.java b/sample/src/main/java/cy/agorise/labs/sample/HtlcActivity.java
new file mode 100644
index 0000000..d452898
--- /dev/null
+++ b/sample/src/main/java/cy/agorise/labs/sample/HtlcActivity.java
@@ -0,0 +1,248 @@
+package cy.agorise.labs.sample;
+
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.google.common.primitives.UnsignedLong;
+
+import org.bitcoinj.core.ECKey;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import cy.agorise.graphenej.Asset;
+import cy.agorise.graphenej.AssetAmount;
+import cy.agorise.graphenej.BaseOperation;
+import cy.agorise.graphenej.BlockData;
+import cy.agorise.graphenej.BrainKey;
+import cy.agorise.graphenej.Htlc;
+import cy.agorise.graphenej.HtlcHash;
+import cy.agorise.graphenej.HtlcHashType;
+import cy.agorise.graphenej.Transaction;
+import cy.agorise.graphenej.UserAccount;
+import cy.agorise.graphenej.Util;
+import cy.agorise.graphenej.api.ConnectionStatusUpdate;
+import cy.agorise.graphenej.api.android.RxBus;
+import cy.agorise.graphenej.api.calls.BroadcastTransaction;
+import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties;
+import cy.agorise.graphenej.models.DynamicGlobalProperties;
+import cy.agorise.graphenej.models.JsonRpcResponse;
+import cy.agorise.graphenej.operations.CreateHtlcOperation;
+import cy.agorise.graphenej.operations.RedeemHtlcOperation;
+import cy.agorise.labs.sample.fragments.CreateHtlcFragment;
+import cy.agorise.labs.sample.fragments.RedeemHtlcFragment;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.functions.Consumer;
+
+public class HtlcActivity extends ConnectedActivity implements
+ CreateHtlcFragment.CreateHtlcListener,
+ RedeemHtlcFragment.RedeemHtlcListener {
+ private String TAG = this.getClass().getName();
+ private final short PREIMAGE_LENGTH = 32;
+ private byte[] mPreimage = new byte[PREIMAGE_LENGTH];
+
+ private CreateHtlcOperation createHtlcOperation;
+ private RedeemHtlcOperation redeemHtlcOperation;
+ private Disposable mDisposable;
+ private String mHtlcMode;
+
+ private Fragment mActiveBottomFragment;
+ private CreateHtlcFragment mCreateHtlcFragment;
+ private RedeemHtlcFragment mRedeemHtlcFragment;
+
+ private HashMap mResponseMap = new HashMap<>();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_htlc);
+
+ mHtlcMode = getIntent().getStringExtra(Constants.KEY_SELECTED_CALL);
+
+ FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
+ if(mHtlcMode.equals(CallsActivity.CREATE_HTLC)){
+ mCreateHtlcFragment = new CreateHtlcFragment();
+ mActiveBottomFragment = mCreateHtlcFragment;
+ }else{
+ mRedeemHtlcFragment = new RedeemHtlcFragment();
+ mActiveBottomFragment = mRedeemHtlcFragment;
+ }
+ fragmentTransaction.add(R.id.fragment_root, mActiveBottomFragment, "active-fragment").commit();
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ if(toolbar != null && mHtlcMode != null){
+ toolbar.setTitle(mHtlcMode.replace("_", " ").toUpperCase());
+ setSupportActionBar(toolbar);
+ }
+
+ // While for the real world is best to use a random pre-image, for testing purposes it is more
+ // convenient to make use of a fixed one.
+// SecureRandom secureRandom = new SecureRandom();
+// secureRandom.nextBytes(mPreimage);
+ mPreimage = Util.hexToBytes("efb79df9b0fd0d27b405e4decf9a2534efc1531f9e133915981fe27cd031ba32");
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.htlc_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ this.switchHtlcMode();
+ return true;
+ }
+
+ @Override
+ public void onAttachFragment(Fragment fragment) {
+ super.onAttachFragment(fragment);
+ if(fragment instanceof CreateHtlcFragment){
+ mCreateHtlcFragment = (CreateHtlcFragment) fragment;
+ }
+ }
+
+ private void switchHtlcMode(){
+ if(mHtlcMode.equals(CallsActivity.CREATE_HTLC)){
+ mHtlcMode = CallsActivity.REDEEM_HTLC;
+ if(mRedeemHtlcFragment == null)
+ mRedeemHtlcFragment = new RedeemHtlcFragment();
+ mActiveBottomFragment = mRedeemHtlcFragment;
+ }else{
+ mHtlcMode = CallsActivity.CREATE_HTLC;
+ if(mCreateHtlcFragment == null)
+ mCreateHtlcFragment = new CreateHtlcFragment();
+ mActiveBottomFragment = mCreateHtlcFragment;
+ }
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_root, mActiveBottomFragment, "active-fragment")
+ .commit();
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ if(toolbar != null)
+ toolbar.setTitle(mHtlcMode.replace("_", " ").toUpperCase());
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mDisposable = RxBus.getBusInstance()
+ .asFlowable()
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(new Consumer