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