Merge branch 'feat_htlc' into develop

This commit is contained in:
Nelson R. Perez 2019-09-12 13:59:47 -05:00
commit ff59f38ba7
27 changed files with 1288 additions and 4 deletions

View file

@ -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){

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,11 @@
package cy.agorise.graphenej;
/**
* Used to enumerate the possible hash algorithms used in HTLCs.
* @see <a href="https://github.com/bitshares/bitshares-core/blob/623aea265f2711adade982fc3248e6528dc8ac51/libraries/chain/include/graphene/chain/protocol/htlc.hpp">htlc.hpp</a>
*/
public enum HtlcHashType {
RIPEMD160,
SHA1,
SHA256
}

View file

@ -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;

View file

@ -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
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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<BaseOperation> 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());
}
}

View file

@ -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<BaseOperation> 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());
}
}

View file

@ -14,10 +14,12 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".HtlcActivity"
android:theme="@style/AppTheme.NoActionBar"/>
<activity
android:name=".BrainkeyActivity"
android:label="@string/title_activity_brainkey"
android:theme="@style/AppTheme.NoActionBar"></activity>
android:theme="@style/AppTheme.NoActionBar" />
<activity android:name=".SubscriptionActivity" />
<activity android:name=".CallsActivity">
<intent-filter>

View file

@ -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);

View file

@ -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<Long, String> 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<Object>() {
@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<BaseOperation> 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) { }
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#333333"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
</vector>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
tools:context=".HtlcActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
<fragment android:name="cy.agorise.labs.sample.fragments.PrintResponseFragment"
android:id="@+id/fragment_print_response"
android:layout_weight="0.4"
android:layout_height="0dp"
android:layout_width="match_parent"/>
<FrameLayout
android:id="@+id/fragment_root"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.6">
</FrameLayout>
</LinearLayout>

View file

@ -0,0 +1,72 @@