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() { + + @Override + public void accept(Object message) throws Exception { + if(message instanceof ConnectionStatusUpdate){ + // TODO: Update UI ? + }else if(message instanceof JsonRpcResponse){ + handleJsonRpcResponse((JsonRpcResponse) message); + } + } + }); + } + + @Override + protected void onPause() { + super.onPause(); + if(!mDisposable.isDisposed()) + mDisposable.dispose(); + + } + + @Override + public void onHtlcProposal(String from, String to, Double amount, Long timelock) { + UserAccount sourceAccount = new UserAccount(from); + UserAccount destinationAccount = new UserAccount(to); + AssetAmount fee = new AssetAmount(UnsignedLong.valueOf("86726"), new Asset("1.3.0")); + AssetAmount operationAmount = new AssetAmount(UnsignedLong.valueOf(Double.valueOf(amount * 100000).longValue()), new Asset("1.3.0")); + try { + byte[] hash = Util.htlcHash(mPreimage, HtlcHashType.RIPEMD160); + HtlcHash htlcHash = new HtlcHash(HtlcHashType.RIPEMD160, hash); + // Creating a HTLC operation, used later. + createHtlcOperation = new CreateHtlcOperation(fee, sourceAccount, destinationAccount, operationAmount, htlcHash, PREIMAGE_LENGTH, timelock.intValue()); + // Requesting dynamic network parameters + long id = mNetworkService.sendMessage(new GetDynamicGlobalProperties(), GetDynamicGlobalProperties.REQUIRED_API); + mResponseMap.put(id, CallsActivity.CREATE_HTLC); + Log.d(TAG,"sendMessage returned: " + id); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG,"NoSuchAlgorithmException while trying to create HTLC operation. Msg: " + e.getMessage()); + } + } + + @Override + public void onRedeemProposal(String userId, String htlcId) { + AssetAmount fee = new AssetAmount(UnsignedLong.valueOf("255128"), new Asset("1.3.0")); + UserAccount redeemer = new UserAccount(userId); + Htlc htlc = new Htlc(htlcId); + redeemHtlcOperation = new RedeemHtlcOperation(fee, redeemer, htlc, mPreimage); + long id = mNetworkService.sendMessage(new GetDynamicGlobalProperties(), GetDynamicGlobalProperties.REQUIRED_API); + mResponseMap.put(id, CallsActivity.REDEEM_HTLC); + Log.d(TAG,"sendMessage returned: " + id); + } + + private void handleJsonRpcResponse(JsonRpcResponse jsonRpcResponse){ + Log.d(TAG,"handleJsonRpcResponse"); + if(jsonRpcResponse.error == null && jsonRpcResponse.result instanceof DynamicGlobalProperties){ + DynamicGlobalProperties dynamicGlobalProperties = (DynamicGlobalProperties) jsonRpcResponse.result; + Transaction tx = buildHltcTransaction(dynamicGlobalProperties, jsonRpcResponse.id); + long id = mNetworkService.sendMessage(new BroadcastTransaction(tx), BroadcastTransaction.REQUIRED_API); + Log.d(TAG,"sendMessage returned: " + id); + } + } + + /** + * Private method used to build a transaction containing a specific HTLC operation. + * + * @param dynamicProperties The current dynamic properties. + * @param responseId The response id, used to decide whether to build a CREATE_HTLC or REDEEM_HTLC operation. + * @return A transaction that contains an HTLC operation. + */ + private Transaction buildHltcTransaction(DynamicGlobalProperties dynamicProperties, long responseId){ + // Private key, to be obtained differently below depending on which operation we'll be performing. + ECKey privKey = null; + + // Use the valid BlockData just obtained from the blockchain + long expirationTime = (dynamicProperties.time.getTime() / 1000) + Transaction.DEFAULT_EXPIRATION_TIME; + String headBlockId = dynamicProperties.head_block_id; + long headBlockNumber = dynamicProperties.head_block_number; + BlockData blockData = new BlockData(headBlockNumber, headBlockId, expirationTime); + + // Using the HTLC create operaton just obtained before + ArrayList operations = new ArrayList<>(); + + if(mResponseMap.get(responseId).equals(CallsActivity.CREATE_HTLC)){ + // Deriving private key + BrainKey brainKey = new BrainKey(">> Place brainkey of HTLC creator here <<", 0); + privKey = brainKey.getPrivateKey(); + + operations.add(this.createHtlcOperation); + }else if(mResponseMap.get(responseId).equals(CallsActivity.REDEEM_HTLC)){ + // Deriving private key + BrainKey brainKey = new BrainKey(">> Place brainkey of redeemer here <<", 0); + privKey = brainKey.getPrivateKey(); + + operations.add(this.redeemHtlcOperation); + } + + // Return a newly built transaction + return new Transaction(privKey, blockData, operations); + } + + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { } + + @Override + public void onServiceDisconnected(ComponentName componentName) { } +} diff --git a/sample/src/main/java/cy/agorise/labs/sample/fragments/CreateHtlcFragment.java b/sample/src/main/java/cy/agorise/labs/sample/fragments/CreateHtlcFragment.java new file mode 100644 index 0000000..b78573e --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/fragments/CreateHtlcFragment.java @@ -0,0 +1,98 @@ +package cy.agorise.labs.sample.fragments; + + +import android.content.Context; +import android.os.Bundle; +import android.support.design.widget.TextInputEditText; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import cy.agorise.labs.sample.R; + +/** + * A simple {@link Fragment} subclass. + */ +public class CreateHtlcFragment extends Fragment { + private final String TAG = this.getClass().getName(); + + @BindView(R.id.from) + TextInputEditText fromField; + + @BindView(R.id.to) + TextInputEditText toField; + + @BindView(R.id.amount) + TextInputEditText amountField; + + @BindView(R.id.timelock) + TextInputEditText timelockField; + + // Parent activity, which must implement the CreateHtlcListener interface. + private CreateHtlcListener mListener; + + public CreateHtlcFragment() { + // Required empty public constructor + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_create_htlc, container, false); + ButterKnife.bind(this, view); + return view; + } + + @OnClick(R.id.button_create) + public void onSendClicked(View v){ + String from = fromField.getText().toString(); + String to = toField.getText().toString(); + Double amount = null; + Long timeLock = null; + try{ + amount = Double.parseDouble(amountField.getText().toString()); + }catch(NumberFormatException e){ + amountField.setError("Invalid amount"); + } + try{ + timeLock = Long.parseLong(timelockField.getText().toString()); + }catch(NumberFormatException e){ + timelockField.setError("Invalid value"); + } + if(amount != null && timeLock != null){ + Toast.makeText(getContext(), "Should be sending message up", Toast.LENGTH_SHORT).show(); + mListener.onHtlcProposal(from, to, amount, timeLock); + } + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if(context instanceof CreateHtlcListener){ + mListener = (CreateHtlcListener) context; + }else{ + throw new ClassCastException(context.toString() + " must implement the CreateHtlcListener interface!"); + } + } + + /** + * Interface to be implemented by the parent activity. + */ + public interface CreateHtlcListener { + /** + * Method used to notify the parent activity of the request to create an HTLC with the following parameters. + * + * @param from Source account id. + * @param to Destination account id. + * @param amount The amount of BTS to propose the HTLC. + * @param timelock The timelock in seconds. + */ + void onHtlcProposal(String from, String to, Double amount, Long timelock); + } +} diff --git a/sample/src/main/java/cy/agorise/labs/sample/fragments/PrintResponseFragment.java b/sample/src/main/java/cy/agorise/labs/sample/fragments/PrintResponseFragment.java new file mode 100644 index 0000000..282d50f --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/fragments/PrintResponseFragment.java @@ -0,0 +1,30 @@ +package cy.agorise.labs.sample.fragments; + + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import cy.agorise.labs.sample.R; + +/** + * A simple {@link Fragment} subclass. + */ +public class PrintResponseFragment extends Fragment { + + + public PrintResponseFragment() { + // Required empty public constructor + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_print_response, container, false); + } + +} diff --git a/sample/src/main/java/cy/agorise/labs/sample/fragments/RedeemHtlcFragment.java b/sample/src/main/java/cy/agorise/labs/sample/fragments/RedeemHtlcFragment.java new file mode 100644 index 0000000..621da5e --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/fragments/RedeemHtlcFragment.java @@ -0,0 +1,75 @@ +package cy.agorise.labs.sample.fragments; + + +import android.content.Context; +import android.os.Bundle; +import android.support.design.widget.TextInputEditText; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import cy.agorise.labs.sample.R; + +/** + * A simple {@link Fragment} subclass. + */ +public class RedeemHtlcFragment extends Fragment { + + @BindView(R.id.redeemer) + TextInputEditText mRedeemer; + + @BindView(R.id.htlc_id) + TextInputEditText mHtlcId; + + // Parent activity, which must implement the RedeemHtlcListener interface. + private RedeemHtlcListener mListener; + + + public RedeemHtlcFragment() { + // Required empty public constructor + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_redeem_htlc, container, false); + ButterKnife.bind(this, view); + return view; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if(context instanceof RedeemHtlcListener){ + mListener = (RedeemHtlcListener) context; + }else{ + throw new ClassCastException(context.toString() + " must implement the RedeemHtlcListener interface!"); + } + } + + @OnClick(R.id.button_create) + public void onSendClicked(View v){ + String redeemerId = mRedeemer.getText().toString(); + String htlcId = mHtlcId.getText().toString(); + Toast.makeText(getContext(), "Should be sending message up", Toast.LENGTH_SHORT).show(); + mListener.onRedeemProposal(redeemerId, htlcId); + } + + /** + * Interface to be implemented by the parent activity. + */ + public interface RedeemHtlcListener { + /** + * Method used to notify the parent activity about the creation of an HTLC redeem operation. + * + * @param userId The id of the user that wishes to redeem an HTLC. + * @param htlcId The HTLC id. + */ + void onRedeemProposal(String userId, String htlcId); + } +} diff --git a/sample/src/main/res/drawable/ic_switch_hltc_mode.xml b/sample/src/main/res/drawable/ic_switch_hltc_mode.xml new file mode 100644 index 0000000..9908c1e --- /dev/null +++ b/sample/src/main/res/drawable/ic_switch_hltc_mode.xml @@ -0,0 +1,5 @@ + + + diff --git a/sample/src/main/res/layout/activity_htlc.xml b/sample/src/main/res/layout/activity_htlc.xml new file mode 100644 index 0000000..34e4bdd --- /dev/null +++ b/sample/src/main/res/layout/activity_htlc.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_create_htlc.xml b/sample/src/main/res/layout/fragment_create_htlc.xml new file mode 100644 index 0000000..266ddc6 --- /dev/null +++ b/sample/src/main/res/layout/fragment_create_htlc.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + +