Modified the sample app to make use of the HTLC redeem operation

- A very simple HTLC creation & redemption mechanism has been put in place for testing/demonstration purposes.
- The HtlcActivity now can both create and redeem an HTLC
This commit is contained in:
Nelson R. Perez 2019-08-02 17:31:48 -05:00
parent 2cd989eade
commit 91275f3cc0
12 changed files with 293 additions and 34 deletions

View file

@ -14,7 +14,8 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".HtlcActivity"></activity>
<activity android:name=".HtlcActivity"
android:theme="@style/AppTheme.NoActionBar"/>
<activity
android:name=".BrainkeyActivity"
android:label="@string/title_activity_brainkey"

View file

@ -27,7 +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";
private static final String CREATE_HTLC = "create_htlc";
public static final String CREATE_HTLC = "create_htlc";
public static final String REDEEM_HTLC = "redeem_htlc";
@BindView(R.id.call_list)
RecyclerView mRecyclerView;
@ -85,7 +86,8 @@ public class CallsActivity extends AppCompatActivity {
RPC.CALL_GET_TRANSACTION,
RECONNECT_NODE,
TEST_BRAINKEY_DERIVATION,
CREATE_HTLC
CREATE_HTLC,
REDEEM_HTLC
};
@NonNull
@ -110,9 +112,10 @@ 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)){
} else if (selectedCall.equals(CREATE_HTLC) || selectedCall.equals(REDEEM_HTLC)){
intent = new Intent(CallsActivity.this, HtlcActivity.class);
}else {
intent.putExtra(Constants.KEY_SELECTED_CALL, selectedCall);
} else {
intent = new Intent(CallsActivity.this, PerformCallActivity.class);
intent.putExtra(Constants.KEY_SELECTED_CALL, selectedCall);
}

View file

@ -3,22 +3,27 @@ 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.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
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.Htlc;
import cy.agorise.graphenej.HtlcHash;
import cy.agorise.graphenej.HtlcHashType;
import cy.agorise.graphenej.Transaction;
@ -31,22 +36,102 @@ 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.HtlcListener {
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
@ -78,42 +163,55 @@ public class HtlcActivity extends ConnectedActivity implements CreateHtlcFragmen
@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);
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);
Transaction tx = buildHltcTransaction(dynamicGlobalProperties, jsonRpcResponse.id);
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()));
/**
* 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;
@ -123,7 +221,20 @@ public class HtlcActivity extends ConnectedActivity implements CreateHtlcFragmen
// Using the HTLC create operaton just obtained before
ArrayList<BaseOperation> operations = new ArrayList<>();
operations.add(this.createHtlcOperation);
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);

View file

@ -5,7 +5,6 @@ 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;
@ -34,8 +33,8 @@ public class CreateHtlcFragment extends Fragment {
@BindView(R.id.timelock)
TextInputEditText timelockField;
// Parent activity, which must implement the HtlcListener interface.
private HtlcListener mListener;
// Parent activity, which must implement the CreateHtlcListener interface.
private CreateHtlcListener mListener;
public CreateHtlcFragment() {
// Required empty public constructor
@ -52,7 +51,6 @@ public class CreateHtlcFragment extends Fragment {
@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;
@ -67,7 +65,6 @@ public class CreateHtlcFragment extends Fragment {
}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);
@ -77,17 +74,17 @@ public class CreateHtlcFragment extends Fragment {
@Override
public void onAttach(Context context) {
super.onAttach(context);
if(context instanceof HtlcListener){
mListener = (HtlcListener) context;
if(context instanceof CreateHtlcListener){
mListener = (CreateHtlcListener) context;
}else{
throw new ClassCastException(context.toString() + " must implement the HtlcListener interface!");
throw new ClassCastException(context.toString() + " must implement the CreateHtlcListener interface!");
}
}
/**
* Interface to be implemented by the parent activity.
*/
public interface HtlcListener {
public interface CreateHtlcListener {
/**
* Method used to notify the parent activity of the request to create an HTLC with the following parameters.
*

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

@ -3,16 +3,26 @@
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"/>
<fragment android:name="cy.agorise.labs.sample.fragments.CreateHtlcFragment"
android:id="@+id/fragment_htlc_parameters"
android:layout_weight="0.6"
<FrameLayout
android:id="@+id/fragment_root"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_width="match_parent"/>
android:layout_weight="0.6">
</FrameLayout>
</LinearLayout>

View file

@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:clickable="false"
tools:context=".fragments.CreateHtlcFragment">
<android.support.design.widget.TextInputLayout
android:id="@+id/container_from"
@ -32,7 +33,7 @@
android:id="@+id/to"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1.2.153769" />
android:text="1.2.143563" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/container_amount"

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
android:layout_margin="16dp"
android:clickable="false"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragments.RedeemHtlcFragment">
<android.support.design.widget.TextInputLayout
android:id="@+id/container_redeemer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_redeemer"
app:layout_constraintBottom_toTopOf="@+id/container_htlc_id"
app:layout_constraintTop_toTopOf="parent">
<android.support.design.widget.TextInputEditText
android:id="@+id/redeemer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1.2.143563" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/container_htlc_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_htlc_id"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/container_redeemer">
<android.support.design.widget.TextInputEditText
android:id="@+id/htlc_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1.16.x" />
</android.support.design.widget.TextInputLayout>
<Button
android:id="@+id/button_create"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/action_send"
app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_selected_operation"
android:title="Create HTLC"
android:icon="@drawable/ic_switch_hltc_mode"
app:showAsAction="always"/>
</menu>

View file

@ -3,4 +3,6 @@
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="white">#FFFFFF</color>
</resources>

View file

@ -76,4 +76,8 @@
<string name="hint_to">To account</string>
<string name="hint_amount">Amount</string>
<string name="hint_timelock">Time to lock (in seconds)</string>
<!-- RedeemHtlcFragment strings -->
<string name="hint_redeemer">Redeemer user id</string>
<string name="hint_htlc_id">HTLC id</string>
</resources>