From 12ce0ef27628af748acd5ec2af662e583ffa7d67 Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Tue, 9 Jul 2019 12:57:41 -0500 Subject: [PATCH 01/13] Added missing operation types --- .../cy/agorise/graphenej/OperationType.java | 12 ++++++++++- .../cy/agorise/graphenej/Transaction.java | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) 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; From d953dae81b2817c401571a2660b34f2fb0d892d5 Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Thu, 11 Jul 2019 13:21:30 -0500 Subject: [PATCH 02/13] Created a single function at the Util static class that exposes interfaces for SHA1, RIPEMD160 & SHA-256 hash functions --- .../main/java/cy/agorise/graphenej/Util.java | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) 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; + } } From bf346f25bf60f373f3f8dd39b0ebff179326c047 Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Thu, 11 Jul 2019 13:25:38 -0500 Subject: [PATCH 03/13] Introducing the CreateHtlcOperation & supporting classes - HtlcHashType class enumerates all supported HTLC hash functions. - The HtlcHash class is used to represent all possible HTLC hash results. - The CreateHtlcOperation class itself is used to represent the operation that creates an HTLC - A simple test class was introduced in order to test hash functions and subsequently also test the HTLC operation serialization --- .../java/cy/agorise/graphenej/HtlcHash.java | 24 ++++ .../cy/agorise/graphenej/HtlcHashType.java | 11 ++ .../operations/CreateHtlcOperation.java | 117 ++++++++++++++++++ .../operations/CreateHtlcOperationTest.java | 69 +++++++++++ 4 files changed, 221 insertions(+) create mode 100644 graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java create mode 100644 graphenej/src/main/java/cy/agorise/graphenej/HtlcHashType.java create mode 100644 graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java create mode 100644 graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java 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..feffe22 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java @@ -0,0 +1,24 @@ +package cy.agorise.graphenej; + +import com.google.common.primitives.Bytes; + +import cy.agorise.graphenej.interfaces.ByteSerializable; + +/** + * Class used to represent a HTLC hash. + */ +public class HtlcHash implements ByteSerializable { + private HtlcHashType hashType; + private byte[] hash; + + public HtlcHash(HtlcHashType hashType, byte[] hash) { + this.hashType = hashType; + this.hash = hash; + } + + @Override + public byte[] toBytes() { + byte[] hashTypeBytes = new byte[] { Util.revertInteger(hashType.ordinal())[3] }; + return Bytes.concat(hashTypeBytes, hash); + } +} 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/operations/CreateHtlcOperation.java b/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java new file mode 100644 index 0000000..0b41b2c --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java @@ -0,0 +1,117 @@ +package cy.agorise.graphenej.operations; + +import com.google.common.primitives.Bytes; + +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 { + 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 String toJsonString() { + return null; + } +} 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..888a498 --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java @@ -0,0 +1,69 @@ +package cy.agorise.graphenej.operations; + +import com.google.common.primitives.UnsignedLong; + +import org.junit.Assert; +import org.junit.Test; + +import java.security.NoSuchAlgorithmException; + +import cy.agorise.graphenej.Asset; +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.HtlcHash; +import cy.agorise.graphenej.HtlcHashType; +import cy.agorise.graphenej.UserAccount; +import cy.agorise.graphenej.Util; + +public class CreateHtlcOperationTest { + private final String SERIALIZED_OP = "f68585abf4dce7c8045701310000000000000000007b7c80241100000000000000a06e327ea7388c18e4740e350ed4e60f2e04fc41c80078000000000001"; + 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"); + + @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(); + } + } + + //TODO: Implement operation test + public void testOperationSerialization() 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); + CreateHtlcOperation operation = new CreateHtlcOperation(fee, from, to, amount, preimageHash, (short) 200, 120); + } +} From bb4fd5ce5da9b938e8bd4119b9422000310406f7 Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Thu, 11 Jul 2019 16:42:37 -0500 Subject: [PATCH 04/13] Implementing test of the CreateHtlcOperation class byte serialization --- .../graphenej/operations/CreateHtlcOperationTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java index 888a498..f33bc7b 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java @@ -15,7 +15,7 @@ import cy.agorise.graphenej.UserAccount; import cy.agorise.graphenej.Util; public class CreateHtlcOperationTest { - private final String SERIALIZED_OP = "f68585abf4dce7c8045701310000000000000000007b7c80241100000000000000a06e327ea7388c18e4740e350ed4e60f2e04fc41c80078000000000001"; + private final String SERIALIZED_OP = "0000000000000000007b7c80241100000000000000a06e327ea7388c18e4740e350ed4e60f2e04fc41c8007800000000"; private final String PREIMAGE_HEX = "666f6f626172"; private final String HASH_RIPEMD160 = "a06e327ea7388c18e4740e350ed4e60f2e04fc41"; private final String HASH_SHA1 = "8843d7f92416211de9ebb963ff4ce28125932878"; @@ -56,7 +56,7 @@ public class CreateHtlcOperationTest { } } - //TODO: Implement operation test + @Test public void testOperationSerialization() throws NoSuchAlgorithmException { UserAccount from = new UserAccount("1.2.123"); UserAccount to = new UserAccount("1.2.124"); @@ -65,5 +65,7 @@ public class CreateHtlcOperationTest { byte[] hashBytes = Util.htlcHash("foobar".getBytes(), HtlcHashType.RIPEMD160); HtlcHash preimageHash = new HtlcHash(HtlcHashType.RIPEMD160, hashBytes); CreateHtlcOperation operation = new CreateHtlcOperation(fee, from, to, amount, preimageHash, (short) 200, 120); + byte[] opBytes = operation.toBytes(); + Assert.assertArrayEquals(Util.hexToBytes(SERIALIZED_OP), opBytes); } } From ca30338af67674606e6bf89b2d6b02a7451bb80c Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Fri, 12 Jul 2019 00:00:46 -0500 Subject: [PATCH 05/13] Implemented a serialization test for a tx containing a CreateHtlcOperation --- .../operations/CreateHtlcOperationTest.java | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java index f33bc7b..3a322b5 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java @@ -1,21 +1,32 @@ package cy.agorise.graphenej.operations; +import com.google.common.primitives.Bytes; import com.google.common.primitives.UnsignedLong; 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 PREIMAGE_HEX = "666f6f626172"; private final String HASH_RIPEMD160 = "a06e327ea7388c18e4740e350ed4e60f2e04fc41"; private final String HASH_SHA1 = "8843d7f92416211de9ebb963ff4ce28125932878"; @@ -23,6 +34,16 @@ public class CreateHtlcOperationTest { 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 { @@ -58,14 +79,22 @@ public class CreateHtlcOperationTest { @Test public void testOperationSerialization() 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); - CreateHtlcOperation operation = new CreateHtlcOperation(fee, from, to, amount, preimageHash, (short) 200, 120); + 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); + byte[] txBytes = transaction.toBytes(); + byte[] expected = Bytes.concat(Util.hexToBytes(Chains.BITSHARES.CHAIN_ID), Util.hexToBytes(SERIALIZED_TX)); + Assert.assertArrayEquals(expected, txBytes); + } } From 931228089746605060f42ff9a8c4ac9b49833ec4 Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Fri, 26 Jul 2019 17:12:38 -0500 Subject: [PATCH 06/13] Making the HtlcHash class implement the JsonSerializable interface --- .../java/cy/agorise/graphenej/HtlcHash.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java index feffe22..aa0b6d1 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/HtlcHash.java @@ -1,13 +1,16 @@ 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 { +public class HtlcHash implements ByteSerializable, JsonSerializable { private HtlcHashType hashType; private byte[] hash; @@ -16,9 +19,27 @@ public class HtlcHash implements ByteSerializable { 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; + } } From 905712114698bffec3b4dd105607c066ffd60f18 Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Fri, 26 Jul 2019 17:16:16 -0500 Subject: [PATCH 07/13] Properly implemented the JsonSerializable interface methods of the CreateHtlcOperation class & included that in tests - This is required in order to communicate the operation to the full node. --- .../operations/CreateHtlcOperation.java | 31 ++++++++++++++++++- .../operations/CreateHtlcOperationTest.java | 14 +++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java b/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java index 0b41b2c..bbd8946 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/operations/CreateHtlcOperation.java @@ -1,6 +1,10 @@ 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; @@ -10,6 +14,13 @@ 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; @@ -110,8 +121,26 @@ public class CreateHtlcOperation extends BaseOperation { 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() { - return null; + GsonBuilder gsonBuilder = new GsonBuilder(); + return gsonBuilder.create().toJson(this); } } diff --git a/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java index 3a322b5..4c38531 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/operations/CreateHtlcOperationTest.java @@ -2,6 +2,8 @@ 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; @@ -27,6 +29,7 @@ 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"; @@ -93,8 +96,19 @@ public class CreateHtlcOperationTest { 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()); } } From d82bc6add1d43e4b1472e0d93ea29f9020a066e1 Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Fri, 26 Jul 2019 17:16:35 -0500 Subject: [PATCH 08/13] Extended the sample app's capabilities in order to be able to send a create_htlc operation - The brainkey must be specified in code before trying this feature out. --- sample/src/main/AndroidManifest.xml | 3 +- .../cy/agorise/labs/sample/CallsActivity.java | 8 +- .../cy/agorise/labs/sample/HtlcActivity.java | 137 ++++++++++++++++++ .../sample/fragments/CreateHtlcFragment.java | 101 +++++++++++++ .../fragments/PrintResponseFragment.java | 30 ++++ sample/src/main/res/layout/activity_htlc.xml | 18 +++ .../main/res/layout/fragment_create_htlc.xml | 71 +++++++++ .../res/layout/fragment_print_response.xml | 10 ++ sample/src/main/res/values/strings.xml | 6 + 9 files changed, 381 insertions(+), 3 deletions(-) create mode 100644 sample/src/main/java/cy/agorise/labs/sample/HtlcActivity.java create mode 100644 sample/src/main/java/cy/agorise/labs/sample/fragments/CreateHtlcFragment.java create mode 100644 sample/src/main/java/cy/agorise/labs/sample/fragments/PrintResponseFragment.java create mode 100644 sample/src/main/res/layout/activity_htlc.xml create mode 100644 sample/src/main/res/layout/fragment_create_htlc.xml create mode 100644 sample/src/main/res/layout/fragment_print_response.xml diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 8ac3474..1545559 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -14,10 +14,11 @@ 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..14bd95c 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,7 @@ public class CallsActivity extends AppCompatActivity { private static final String RECONNECT_NODE = "reconnect_node"; private static final String TEST_BRAINKEY_DERIVATION = "test_brainkey_derivation"; + private static final String CREATE_HTLC = "create_htlc"; @BindView(R.id.call_list) RecyclerView mRecyclerView; @@ -83,7 +84,8 @@ public class CallsActivity extends AppCompatActivity { RPC.CALL_BROADCAST_TRANSACTION, RPC.CALL_GET_TRANSACTION, RECONNECT_NODE, - TEST_BRAINKEY_DERIVATION + TEST_BRAINKEY_DERIVATION, + CREATE_HTLC }; @NonNull @@ -108,7 +110,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 { + } else if (selectedCall.equals(CREATE_HTLC)){ + intent = new Intent(CallsActivity.this, HtlcActivity.class); + }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..1c37ba4 --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/HtlcActivity.java @@ -0,0 +1,137 @@ +package cy.agorise.labs.sample; + +import android.content.ComponentName; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; + +import com.google.common.primitives.UnsignedLong; + +import org.bitcoinj.core.ECKey; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; + +import cy.agorise.graphenej.Address; +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.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.labs.sample.fragments.CreateHtlcFragment; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; + +public class HtlcActivity extends ConnectedActivity implements CreateHtlcFragment.HtlcListener { + private String TAG = this.getClass().getName(); + private final short PREIMAGE_LENGTH = 32; + + private CreateHtlcOperation createHtlcOperation; + private Disposable mDisposable; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_htlc); + } + + @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) { + Log.d(TAG,"onHtlcProposal"); + 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")); + SecureRandom secureRandom = new SecureRandom(); + byte[] preimage = new byte[PREIMAGE_LENGTH]; + secureRandom.nextBytes(preimage); + try { + byte[] hash = Util.htlcHash(preimage, 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); + Log.d(TAG,"sendMessage returned: " + id); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG,"NoSuchAlgorithmException while trying to create HTLC operation. Msg: " + e.getMessage()); + } + } + + 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); + long id = mNetworkService.sendMessage(new BroadcastTransaction(tx), BroadcastTransaction.REQUIRED_API); + Log.d(TAG,"sendMessage returned: " + id); + } + } + + private Transaction buildHltcTransaction(DynamicGlobalProperties dynamicProperties){ + // Deriving private key + BrainKey brainKey = new BrainKey(">> Enter your own test Brainkey here <<", 0); + ECKey privKey = brainKey.getPrivateKey(); + Address address = new Address(ECKey.fromPublicOnly(privKey.getPubKey())); + + // 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<>(); + operations.add(this.createHtlcOperation); + + // 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..f332365 --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/fragments/CreateHtlcFragment.java @@ -0,0 +1,101 @@ +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.util.Log; +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 HtlcListener interface. + private HtlcListener 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){ + Log.d(TAG,"onSendClicked"); + 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"); + } + Log.d(TAG,"amount: " + amount + ", timelock: " + timeLock); + 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 HtlcListener){ + mListener = (HtlcListener) context; + }else{ + throw new ClassCastException(context.toString() + " must implement the HtlcListener interface!"); + } + } + + /** + * Interface to be implemented by the parent activity. + */ + public interface HtlcListener { + /** + * 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/res/layout/activity_htlc.xml b/sample/src/main/res/layout/activity_htlc.xml new file mode 100644 index 0000000..9269ed0 --- /dev/null +++ b/sample/src/main/res/layout/activity_htlc.xml @@ -0,0 +1,18 @@ + + + + + \ 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..493dab7 --- /dev/null +++ b/sample/src/main/res/layout/fragment_create_htlc.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + +