diff --git a/app/build.gradle b/app/build.gradle
index b4ee422..c9d588c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -101,4 +101,11 @@ dependencies {
implementation 'id.zelory:compressor:2.1.0'
implementation 'com.vincent.filepicker:MultiTypeFilePicker:1.0.7'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
+ implementation 'commons-codec:commons-codec:1.11'
+
+ implementation ('io.socket:socket.io-client:0.8.3') {
+ // excluding org.json which is provided by Android
+ exclude group: 'org.json', module: 'json'
+ }
+
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a81d557..d77e761 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -55,6 +55,21 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/cy/agorise/crystalwallet/activities/IntroActivity.java b/app/src/main/java/cy/agorise/crystalwallet/activities/IntroActivity.java
index 75513a3..6fa59d2 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/activities/IntroActivity.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/activities/IntroActivity.java
@@ -108,6 +108,7 @@ public class IntroActivity extends CustomActivity {
} else {
//Intent intent = new Intent(this, CreateSeedActivity.class);
Intent intent = new Intent(this, BoardActivity.class);
+ //Intent intent = new Intent(this, PocketRequestActivity.class);
startActivity(intent);
}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/activities/PatternRequestActivity.java b/app/src/main/java/cy/agorise/crystalwallet/activities/PatternRequestActivity.java
index 283578e..0ecb4f8 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/activities/PatternRequestActivity.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/activities/PatternRequestActivity.java
@@ -18,6 +18,7 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnTextChanged;
import cy.agorise.crystalwallet.R;
+import cy.agorise.crystalwallet.application.CrystalSecurityMonitor;
import cy.agorise.crystalwallet.models.GeneralSetting;
import cy.agorise.crystalwallet.util.PasswordManager;
import cy.agorise.crystalwallet.viewmodels.GeneralSettingListViewModel;
@@ -69,7 +70,11 @@ public class PatternRequestActivity extends AppCompatActivity {
@Override
public void onComplete(List pattern) {
if (PasswordManager.checkPassword(patternEncrypted,patternToString(pattern))){
- thisActivity.finish();
+ if (CrystalSecurityMonitor.getInstance(null).is2ndFactorSet()) {
+ CrystalSecurityMonitor.getInstance(null).call2ndFactor(thisActivity);
+ } else {
+ thisActivity.finish();
+ }
} else {
patternLockView.clearPattern();
patternLockView.requestFocus();
diff --git a/app/src/main/java/cy/agorise/crystalwallet/activities/PinRequestActivity.java b/app/src/main/java/cy/agorise/crystalwallet/activities/PinRequestActivity.java
index df9e430..289d899 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/activities/PinRequestActivity.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/activities/PinRequestActivity.java
@@ -15,6 +15,8 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnTextChanged;
import cy.agorise.crystalwallet.R;
+import cy.agorise.crystalwallet.application.CrystalSecurityMonitor;
+import cy.agorise.crystalwallet.models.AccountSeed;
import cy.agorise.crystalwallet.models.GeneralSetting;
import cy.agorise.crystalwallet.util.PasswordManager;
import cy.agorise.crystalwallet.viewmodels.GeneralSettingListViewModel;
@@ -61,7 +63,11 @@ public class PinRequestActivity extends AppCompatActivity {
callback = OnTextChanged.Callback.AFTER_TEXT_CHANGED)
void afterPasswordChanged(Editable editable) {
if (PasswordManager.checkPassword(passwordEncrypted, etPassword.getText().toString())) {
- this.finish();
+ if (CrystalSecurityMonitor.getInstance(null).is2ndFactorSet()) {
+ CrystalSecurityMonitor.getInstance(null).call2ndFactor(this);
+ } else {
+ this.finish();
+ }
}
}
}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/activities/PocketRequestActivity.java b/app/src/main/java/cy/agorise/crystalwallet/activities/PocketRequestActivity.java
new file mode 100644
index 0000000..687f810
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/activities/PocketRequestActivity.java
@@ -0,0 +1,144 @@
+package cy.agorise.crystalwallet.activities;
+
+import android.app.PendingIntent;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.Observer;
+import android.arch.lifecycle.ViewModelProviders;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.nfc.tech.IsoDep;
+import android.nfc.tech.MifareClassic;
+import android.nfc.tech.NdefFormatable;
+import android.nfc.tech.NfcA;
+import android.nfc.tech.NfcF;
+import android.nfc.tech.NfcV;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.andrognito.patternlockview.PatternLockView;
+import com.andrognito.patternlockview.listener.PatternLockViewListener;
+
+import org.apache.commons.codec.binary.Base32;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import cy.agorise.crystalwallet.R;
+import cy.agorise.crystalwallet.application.CrystalSecurityMonitor;
+import cy.agorise.crystalwallet.models.GeneralSetting;
+import cy.agorise.crystalwallet.util.PasswordManager;
+import cy.agorise.crystalwallet.util.yubikey.Algorithm;
+import cy.agorise.crystalwallet.util.yubikey.OathType;
+import cy.agorise.crystalwallet.util.yubikey.TOTP;
+import cy.agorise.crystalwallet.util.yubikey.YkOathApi;
+import cy.agorise.crystalwallet.viewmodels.GeneralSettingListViewModel;
+
+public class PocketRequestActivity extends AppCompatActivity {
+
+ private NfcAdapter mNfcAdapter;
+ private PendingIntent pendingIntent;
+ private IntentFilter ndef;
+ IntentFilter[] intentFiltersArray;
+ String[][] techList;
+
+ @Override
+ public void onBackPressed() {
+ //Do nothing to prevent the user to use the back button
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_pocket_request);
+ ButterKnife.bind(this);
+
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
+
+ this.configureForegroundDispatch();
+ }
+
+ public void configureForegroundDispatch(){
+ if (mNfcAdapter != null) {
+ pendingIntent = PendingIntent.getActivity(
+ this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
+
+ ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
+ try {
+ ndef.addDataType("*/*"); /* Handles all MIME based dispatches.
+ You should specify only the ones that you need. */
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ throw new RuntimeException("fail", e);
+ }
+ intentFiltersArray = new IntentFilter[]{ndef,};
+ techList = new String[][]{ new String[] {IsoDep.class.getName(), NfcA.class.getName(), MifareClassic.class.getName(), NdefFormatable.class.getName()} };
+
+ } else {
+ Toast.makeText(this, "This device doesn't support NFC.", Toast.LENGTH_LONG).show();
+ }
+ }
+
+ public void onPause() {
+ super.onPause();
+ if (mNfcAdapter != null) {
+ mNfcAdapter.disableForegroundDispatch(this);
+ }
+ }
+
+ public void onResume() {
+ super.onResume();
+ if (mNfcAdapter != null) {
+ mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techList);
+ }
+ }
+
+ public void onNewIntent(Intent intent) {
+ Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+ IsoDep tagIsoDep = IsoDep.get(tagFromIntent);
+ Log.i("Tag from nfc","New Intent");
+ String yubikeySecret = CrystalSecurityMonitor.getInstance(null).get2ndFactorValue();
+
+ try {
+ tagIsoDep.connect();
+ YkOathApi ykOathApi = new YkOathApi(tagIsoDep);
+
+ long unixTime = System.currentTimeMillis() / 1000L;
+ byte[] timeStep = ByteBuffer.allocate(8).putLong(unixTime / 30L).array();
+ byte[] response;
+ response = ykOathApi.calculate("cy.agorise.crystalwallet",timeStep,true);
+ ByteBuffer responseBB = ByteBuffer.wrap(response);
+ int digits = (int)responseBB.get();
+ String challengeString = ""+(responseBB.getInt());
+ String challenge = challengeString.substring(challengeString.length()-digits);
+ while (challenge.length() < digits){
+ challenge = '0'+challenge;
+ }
+
+ String storedChallenge = PasswordManager.totpd(yubikeySecret, unixTime, ykOathApi.getDeviceSalt());
+
+ Toast.makeText(this, "Secret:"+yubikeySecret+" StoredChallenge:"+storedChallenge+" Yubikey:"+challenge , Toast.LENGTH_LONG).show();
+
+ Log.i("TOTP","Secret: "+yubikeySecret);
+ Log.i("TOTP", "Unixtime: "+unixTime);
+ Log.i("TOTP", "Step: "+unixTime/30L);
+ Log.i("TOTP", "StoredChallenge: "+storedChallenge);
+ Log.i("TOTP", "Yubikey: "+challenge);
+
+ tagIsoDep.close();
+ //ykOathApi.
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
+
+
diff --git a/app/src/main/java/cy/agorise/crystalwallet/activities/SettingsActivity.java b/app/src/main/java/cy/agorise/crystalwallet/activities/SettingsActivity.java
index 24b6dae..a953b05 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/activities/SettingsActivity.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/activities/SettingsActivity.java
@@ -1,5 +1,6 @@
package cy.agorise.crystalwallet.activities;
+import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.support.v4.app.Fragment;
@@ -47,6 +48,8 @@ public class SettingsActivity extends AppCompatActivity{
@BindView(R.id.tvBuildVersion)
public TextView tvBuildVersion;
+ private SecuritySettingsFragment securitySettingsFragment;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -97,7 +100,8 @@ public class SettingsActivity extends AppCompatActivity{
case 0:
return new GeneralSettingsFragment();
case 1:
- return new SecuritySettingsFragment();
+ securitySettingsFragment = new SecuritySettingsFragment();
+ return securitySettingsFragment;
case 2:
return new BackupsSettingsFragment();
//case 3:
@@ -123,4 +127,12 @@ public class SettingsActivity extends AppCompatActivity{
public void goBack(){
onBackPressed();
}
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ if (this.securitySettingsFragment != null){
+ this.securitySettingsFragment.onNewIntent(intent);
+ }
+ }
}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/BitsharesFaucetApiGenerator.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/BitsharesFaucetApiGenerator.java
index 8aa4b56..10c2465 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/BitsharesFaucetApiGenerator.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/BitsharesFaucetApiGenerator.java
@@ -7,6 +7,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
+import io.reactivex.Observable;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
@@ -217,8 +218,9 @@ public abstract class BitsharesFaucetApiGenerator {
public interface IWebService {
@Headers({"Content-Type: application/json"})
- @POST("/api/v1/accounts")
+ @POST("/faucet/api/v1/accounts")
Call getReg(@Body Map params);
+
}
public class RegisterAccountResponse {
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/GrapheneApiGenerator.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/GrapheneApiGenerator.java
index 3c2dcca..9d781fd 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/GrapheneApiGenerator.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/GrapheneApiGenerator.java
@@ -610,7 +610,7 @@ public abstract class GrapheneApiGenerator {
public void onError(BaseResponse.Error error) {
request.getListener().fail(request.getId());
}
- }), BitsharesConstant.EQUIVALENT_URL); //todo change equivalent url for current server url
+ }), CryptoNetManager.getURL(CryptoNet.BITSHARES)); //todo change equivalent url for current server url
thread.start();
}
@@ -626,7 +626,7 @@ public abstract class GrapheneApiGenerator {
for (BitsharesAsset quoteAsset : quoteAssets) {
WebSocketThread thread = new WebSocketThread(new GetLimitOrders(baseAsset.getBitsharesId(),
quoteAsset.getBitsharesId(), 10, new EquivalentValueListener(baseAsset,
- quoteAsset, context)), BitsharesConstant.EQUIVALENT_URL); //todo change equivalent url for current server url
+ quoteAsset, context)), CryptoNetManager.getURL(CryptoNet.BITSHARES)); //todo change equivalent url for current server url
thread.start();
}
}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/AccountActivityWatcher.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/AccountActivityWatcher.java
new file mode 100644
index 0000000..aa8624e
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/AccountActivityWatcher.java
@@ -0,0 +1,160 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi;
+
+import android.content.Context;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import cy.agorise.crystalwallet.models.GeneralCoinAccount;
+import io.socket.client.IO;
+import io.socket.client.Socket;
+import io.socket.emitter.Emitter;
+
+/**
+ * Handles all the calls for the Socket.IO of the insight api
+ *
+ * Only gets new transaction in real time for each address of an Account
+ *
+ */
+
+public class AccountActivityWatcher {
+
+ /**
+ * The mAccount to be monitor
+ */
+ private final GeneralCoinAccount mAccount;
+ /**
+ * The list of address to monitor
+ */
+ private List mWatchAddress = new ArrayList<>();
+ /**
+ * the Socket.IO
+ */
+ private Socket mSocket;
+ /**
+ * This app mContext, used to save on the DB
+ */
+ private final Context mContext;
+
+ /**
+ * Handles the address/transaction notification.
+ * Then calls the GetTransactionData to get the info of the new transaction
+ */
+ private final Emitter.Listener onAddressTransaction = new Emitter.Listener() {
+ @Override
+ public void call(Object... os) {
+ try {
+ System.out.println("Receive accountActivtyWatcher " + os[0].toString() );
+ String txid = ((JSONObject) os[0]).getString(InsightApiConstants.sTxTag);
+ new GetTransactionData(txid, mAccount, mContext).start();
+ } catch (JSONException ex) {
+ Logger.getLogger(AccountActivityWatcher.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+ };
+
+ /**
+ * Handles the connect of the Socket.IO
+ */
+ private final Emitter.Listener onConnect = new Emitter.Listener() {
+ @Override
+ public void call(Object... os) {
+ System.out.println("Connected to accountActivityWatcher");
+ JSONArray array = new JSONArray();
+ for(String addr : mWatchAddress) {
+ array.put(addr);
+ }
+ mSocket.emit(InsightApiConstants.sSubscribeEmmit, InsightApiConstants.sChangeAddressRoom, array);
+ }
+ };
+
+ /**
+ * Handles the disconnect of the Socket.Io
+ * Reconcects the mSocket
+ */
+ private final Emitter.Listener onDisconnect = new Emitter.Listener() {
+ @Override
+ public void call(Object... os) {
+ System.out.println("Disconnected to accountActivityWatcher");
+ mSocket.connect();
+ }
+ };
+
+ /**
+ * Error handler, doesn't need reconnect, the mSocket.io do that by default
+ */
+ private final Emitter.Listener onError = new Emitter.Listener() {
+ @Override
+ public void call(Object... os) {
+ System.out.println("Error to accountActivityWatcher ");
+ for(Object ob : os) {
+ System.out.println("accountActivityWatcher " + ob.toString());
+ }
+ }
+ };
+
+ /**
+ * Basic constructor
+ *
+ * @param mAccount The mAccount to be monitor
+ * @param mContext This app mContext
+ */
+ public AccountActivityWatcher(GeneralCoinAccount mAccount, Context mContext) {
+ //String serverUrl = InsightApiConstants.protocol + "://" + InsightApiConstants.getAddress(mAccount.getCoin()) + ":" + InsightApiConstants.getPort(mAccount.getCoin()) + "/"+InsightApiConstants.getRawPath(mAccount.getCoin())+"/mSocket.io/";
+ String serverUrl = InsightApiConstants.sProtocolSocketIO + "://" + InsightApiConstants.getAddress(mAccount.getCryptoCoin()) + ":" + InsightApiConstants.getPort(mAccount.getCryptoCoin()) + "/";
+ this.mAccount = mAccount;
+ this.mContext = mContext;
+ System.out.println("accountActivityWatcher " + serverUrl);
+ try {
+ IO.Options opts = new IO.Options();
+ System.out.println("accountActivityWatcher default path " + opts.path);
+ this.mSocket = IO.socket(serverUrl);
+ this.mSocket.on(Socket.EVENT_CONNECT, onConnect);
+ this.mSocket.on(Socket.EVENT_DISCONNECT, onDisconnect);
+ this.mSocket.on(Socket.EVENT_ERROR, onError);
+ this.mSocket.on(Socket.EVENT_CONNECT_ERROR, onError);
+ this.mSocket.on(Socket.EVENT_CONNECT_TIMEOUT, onError);
+ this.mSocket.on(InsightApiConstants.sChangeAddressRoom, onAddressTransaction);
+ } catch (URISyntaxException e) {
+ //TODO change exception handler
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Add an address to be monitored, it can be used after the connect
+ * @param address The String address to monitor
+ */
+ public void addAddress(String address) {
+ mWatchAddress.add(address);
+ if (this.mSocket.connected()) {
+ mSocket.emit(InsightApiConstants.sSubscribeEmmit, InsightApiConstants.sChangeAddressRoom, new String[]{address});
+ }
+ }
+
+ /**
+ * Connects the Socket
+ */
+ public void connect() {
+ //TODO change to use log
+ System.out.println("accountActivityWatcher connecting");
+ try{
+ this.mSocket.connect();
+ }catch(Exception e){
+ //TODO change exception handler
+ System.out.println("accountActivityWatcher exception " + e.getMessage());
+ }
+ }
+
+ /**
+ * Disconnects the Socket
+ */
+ public void disconnect() {this.mSocket.disconnect();}
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/BroadcastTransaction.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/BroadcastTransaction.java
new file mode 100644
index 0000000..ae91afb
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/BroadcastTransaction.java
@@ -0,0 +1,83 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi;
+
+import android.content.Context;
+
+import cy.agorise.crystalwallet.apigenerator.insightapi.models.Txi;
+import cy.agorise.crystalwallet.models.GeneralCoinAccount;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+/**
+ * Broadcast a transaction, using the InsightApi
+ *
+ */
+
+public class BroadcastTransaction extends Thread implements Callback {
+ /**
+ * The rawTX as Hex String
+ */
+ private String mRawTx;
+ /**
+ * The serviceGenerator to call
+ */
+ private InsightApiServiceGenerator mServiceGenerator;
+ /**
+ * This app context, used to save on the DB
+ */
+ private Context mContext;
+ /**
+ * The account who sign the transaction
+ */
+ private GeneralCoinAccount mAccount;
+
+ /**
+ * Basic Consturctor
+ * @param RawTx The RawTX in Hex String
+ * @param account The account who signs the transaction
+ * @param context This app context
+ */
+ public BroadcastTransaction(String RawTx, GeneralCoinAccount account, Context context){
+ String serverUrl = InsightApiConstants.sProtocol + "://" + InsightApiConstants.getAddress(account.getCryptoCoin()) +"/";
+ this.mServiceGenerator = new InsightApiServiceGenerator(serverUrl);
+ this.mContext = context;
+ this.mRawTx = RawTx;
+ this.mAccount = account;
+ }
+
+ /**
+ * Handles the response of the call
+ *
+ */
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ //TODO invalidated send
+ //TODO call getTransactionData
+ GetTransactionData trData = new GetTransactionData(response.body().txid,this.mAccount,this.mContext);
+ trData.start();
+ } else {
+ System.out.println("SENDTEST: not succesful " + response.message());
+ //TODO change how to handle invalid transaction
+ }
+ }
+
+ /**
+ * Handles the failures of the call
+ */
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ //TODO change how to handle invalid transaction
+ System.out.println("SENDTEST: sendError " + t.getMessage() );
+ }
+
+ /**
+ * Starts the call of the service
+ */
+ @Override
+ public void run() {
+ InsightApiService service = this.mServiceGenerator.getService(InsightApiService.class);
+ Call broadcastTransaction = service.broadcastTransaction(InsightApiConstants.getPath(this.mAccount.getCryptoCoin()),this.mRawTx);
+ broadcastTransaction.enqueue(this);
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetEstimateFee.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetEstimateFee.java
new file mode 100644
index 0000000..933d2b4
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetEstimateFee.java
@@ -0,0 +1,74 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi;
+
+import com.google.gson.JsonObject;
+
+import java.io.IOException;
+
+import cy.agorise.crystalwallet.enums.CryptoCoin;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+/**
+ * Get the estimete fee amount from an insight api server.
+ * This class gets the rate of the fee for a giving coin in about to block for a transaction to be
+ * confirmated.
+ *
+ * This ammount is giving as amount of currency / kbytes, as example btc / kbytes
+ *
+ */
+
+public abstract class GetEstimateFee {
+
+ //TODO add a funciton to get the rate of a specific port
+
+ /**
+ * The funciton to get the rate for the transaction be included in the next 2 blocks
+ * @param coin The coin to get the rate
+ * @return The rate number (coin/kbytes)
+ * @throws IOException If the server answer null, or the rate couldn't be calculated
+ */
+ public static long getEstimateFee(final CryptoCoin coin) throws IOException {
+ String serverUrl = InsightApiConstants.sProtocol + "://"
+ + InsightApiConstants.getAddress(coin) + "/";
+ InsightApiServiceGenerator serviceGenerator = new InsightApiServiceGenerator(serverUrl);
+ InsightApiService service = serviceGenerator.getService(InsightApiService.class);
+ Call call = service.estimateFee(InsightApiConstants.getPath(coin));
+ final Object SYNC = new Object();
+ final JsonObject answer = new JsonObject();
+ call.enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ synchronized (SYNC) {
+ answer.addProperty("answer",
+ (long) (response.body().get("2").getAsDouble()* Math.pow(10, coin.getPrecision())));
+ SYNC.notifyAll();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ synchronized (SYNC) {
+ SYNC.notifyAll();
+ }
+ }
+ });
+ synchronized (SYNC){
+ for(int i = 0; i < 6; i++) {
+ try {
+ SYNC.wait(5000);
+ } catch (InterruptedException e) {
+ // this interruption never rises
+ }
+ if(answer.get("answer")!=null){
+ break;
+ }
+ }
+ }
+ if(answer.get("answer")==null){
+ throw new IOException("");
+ }
+ return (long) (answer.get("answer").getAsDouble());
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetTransactionByAddress.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetTransactionByAddress.java
new file mode 100644
index 0000000..d8052ff
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetTransactionByAddress.java
@@ -0,0 +1,207 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import cy.agorise.crystalwallet.apigenerator.insightapi.models.AddressTxi;
+import cy.agorise.crystalwallet.apigenerator.insightapi.models.Txi;
+import cy.agorise.crystalwallet.apigenerator.insightapi.models.Vin;
+import cy.agorise.crystalwallet.apigenerator.insightapi.models.Vout;
+import cy.agorise.crystalwallet.models.GTxIO;
+import cy.agorise.crystalwallet.models.GeneralCoinAccount;
+import cy.agorise.crystalwallet.models.GeneralCoinAddress;
+import cy.agorise.crystalwallet.models.GeneralTransaction;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+/**
+ * Get all the transaction data of the addresses of an account
+ *
+ */
+
+public class GetTransactionByAddress extends Thread implements Callback {
+ /**
+ * The account to be query
+ */
+ private GeneralCoinAccount mAccount;
+ /**
+ * The list of address to query
+ */
+ private List mAddresses = new ArrayList<>();
+ /**
+ * The serviceGenerator to call
+ */
+ private InsightApiServiceGenerator mServiceGenerator;
+ /**
+ * This app context, used to save on the DB
+ */
+ private Context mContext;
+
+
+ /**
+ * Basic consturcotr
+ * @param account The account to be query
+ * @param context This app context
+ */
+ public GetTransactionByAddress(GeneralCoinAccount account, Context context) {
+ String serverUrl = InsightApiConstants.sProtocol + "://" + InsightApiConstants.getAddress(account.getCryptoCoin()) +"/";
+ this.mAccount = account;
+ this.mServiceGenerator = new InsightApiServiceGenerator(serverUrl);
+ this.mContext = context;
+ }
+
+ /**
+ * add an address to be query
+ * @param address the address to be query
+ */
+ public void addAddress(GeneralCoinAddress address) {
+ this.mAddresses.add(address);
+ }
+
+
+ /**
+ * Handle the response
+ * @param call The call with the addresTxi object
+ * @param response the response status object
+ */
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ boolean changed = false;
+ AddressTxi addressTxi = response.body();
+
+ for (Txi txi : addressTxi.items) {
+ GeneralCoinAccount tempAccount = null;
+ GeneralTransaction transaction = new GeneralTransaction();
+ transaction.setAccount(this.mAccount);
+ transaction.setTxid(txi.txid);
+ transaction.setBlock(txi.blockheight);
+ transaction.setDate(new Date(txi.time * 1000));
+ transaction.setFee((long) (txi.fee * Math.pow(10,this.mAccount.getCryptoCoin().getPrecision())));
+ transaction.setConfirm(txi.confirmations);
+ transaction.setType(this.mAccount.getCryptoCoin());
+ transaction.setBlockHeight(txi.blockheight);
+
+ for (Vin vin : txi.vin) {
+ GTxIO input = new GTxIO();
+ input.setAmount((long) (vin.value * Math.pow(10,this.mAccount.getCryptoCoin().getPrecision())));
+ input.setTransaction(transaction);
+ input.setOut(true);
+ input.setType(this.mAccount.getCryptoCoin());
+ String addr = vin.addr;
+ input.setAddressString(addr);
+ input.setIndex(vin.n);
+ input.setScriptHex(vin.scriptSig.hex);
+ input.setOriginalTxid(vin.txid);
+ for (GeneralCoinAddress address : this.mAddresses) {
+ if (address.getAddressString(this.mAccount.getNetworkParam()).equals(addr)) {
+ input.setAddress(address);
+ tempAccount = address.getAccount();
+
+ if (!address.hasTransactionOutput(input, this.mAccount.getNetworkParam())) {
+ address.getTransactionOutput().add(input);
+ }
+ changed = true;
+ }
+ }
+ transaction.getTxInputs().add(input);
+ }
+
+ for (Vout vout : txi.vout) {
+ if(vout.scriptPubKey.addresses == null || vout.scriptPubKey.addresses.length <= 0){
+ // The address is null, this must be a memo
+ String hex = vout.scriptPubKey.hex;
+ int opReturnIndex = hex.indexOf("6a");
+ if(opReturnIndex >= 0) {
+ byte[] memoBytes = new byte[Integer.parseInt(hex.substring(opReturnIndex+2,opReturnIndex+4),16)];
+ for(int i = 0; i < memoBytes.length;i++){
+ memoBytes[i] = Byte.parseByte(hex.substring(opReturnIndex+4+(i*2),opReturnIndex+6+(i*2)),16);
+ }
+ transaction.setMemo(new String(memoBytes));
+ }
+ }else {
+ GTxIO output = new GTxIO();
+ output.setAmount((long) (vout.value * Math.pow(10, this.mAccount.getCryptoCoin().getPrecision())));
+ output.setTransaction(transaction);
+ output.setOut(false);
+ output.setType(this.mAccount.getCryptoCoin());
+ String addr = vout.scriptPubKey.addresses[0];
+ output.setAddressString(addr);
+ output.setIndex(vout.n);
+ output.setScriptHex(vout.scriptPubKey.hex);
+ for (GeneralCoinAddress address : this.mAddresses) {
+ if (address.getAddressString(this.mAccount.getNetworkParam()).equals(addr)) {
+ output.setAddress(address);
+ tempAccount = address.getAccount();
+
+ if (!address.hasTransactionInput(output, this.mAccount.getNetworkParam())) {
+ address.getTransactionInput().add(output);
+ }
+ changed = true;
+ }
+ }
+
+ transaction.getTxOutputs().add(output);
+ }
+ }
+ if(txi.txlock && txi.confirmations< this.mAccount.getCryptoNet().getConfirmationsNeeded()){
+ transaction.setConfirm(this.mAccount.getCryptoNet().getConfirmationsNeeded());
+ }
+ //TODO database
+ /*SCWallDatabase db = new SCWallDatabase(this.mContext);
+ long idTransaction = db.getGeneralTransactionId(transaction);
+ if (idTransaction == -1) {
+ db.putGeneralTransaction(transaction);
+ } else {
+ transaction.setId(idTransaction);
+ db.updateGeneralTransaction(transaction);
+ }*/
+
+ if (tempAccount != null && transaction.getConfirm() < this.mAccount.getCryptoNet().getConfirmationsNeeded()) {
+ new GetTransactionData(transaction.getTxid(), tempAccount, this.mContext, true).start();
+ }
+ for (GeneralCoinAddress address : this.mAddresses) {
+ if (address.updateTransaction(transaction)) {
+ break;
+ }
+ }
+ }
+
+ if(changed) {
+ this.mAccount.balanceChange();
+ }
+ }
+ }
+
+ /**
+ * Failure of the call
+ * @param call The call object
+ * @param t The reason for the failure
+ */
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e("GetTransactionByAddress", "Error in json format");
+ }
+
+ /**
+ * Function to start the insight api call
+ */
+ @Override
+ public void run() {
+ if (this.mAddresses.size() > 0) {
+ StringBuilder addressToQuery = new StringBuilder();
+ for (GeneralCoinAddress address : this.mAddresses) {
+ addressToQuery.append(address.getAddressString(this.mAccount.getNetworkParam())).append(",");
+ }
+ addressToQuery.deleteCharAt(addressToQuery.length() - 1);
+ InsightApiService service = this.mServiceGenerator.getService(InsightApiService.class);
+ Call addressTxiCall = service.getTransactionByAddress(InsightApiConstants.getPath(this.mAccount.getCryptoCoin()),addressToQuery.toString());
+ addressTxiCall.enqueue(this);
+ }
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetTransactionData.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetTransactionData.java
new file mode 100644
index 0000000..a9f0e88
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetTransactionData.java
@@ -0,0 +1,196 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi;
+
+import android.content.Context;
+
+import java.util.Date;
+
+import cy.agorise.crystalwallet.apigenerator.insightapi.models.Txi;
+import cy.agorise.crystalwallet.apigenerator.insightapi.models.Vin;
+import cy.agorise.crystalwallet.apigenerator.insightapi.models.Vout;
+import cy.agorise.crystalwallet.models.GTxIO;
+import cy.agorise.crystalwallet.models.GeneralCoinAccount;
+import cy.agorise.crystalwallet.models.GeneralCoinAddress;
+import cy.agorise.crystalwallet.models.GeneralTransaction;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+/**
+ * CThis class retrieve the data of a single transaction
+ */
+
+public class GetTransactionData extends Thread implements Callback {
+ /**
+ * The account to be query
+ */
+ private final GeneralCoinAccount mAccount;
+ /**
+ * The transaction txid to be query
+ */
+ private String mTxId;
+ /**
+ * The serviceGenerator to call
+ */
+ private InsightApiServiceGenerator mServiceGenerator;
+ /**
+ * This app context, used to save on the DB
+ */
+ private Context mContext;
+ /**
+ * If has to wait for another confirmation
+ */
+ private boolean mMustWait = false;
+
+ /**
+ * Constructor used to query for a transaction with unknown confirmations
+ * @param txid The txid of the transaciton to be query
+ * @param account The account to be query
+ * @param context This app Context
+ */
+ public GetTransactionData(String txid, GeneralCoinAccount account, Context context) {
+ this(txid, account, context, false);
+ }
+
+ /**
+ * Consturctor to be used qhen the confirmations of the transaction are known
+ * @param txid The txid of the transaciton to be query
+ * @param account The account to be query
+ * @param context This app Context
+ * @param mustWait If there is less confirmation that needed
+ */
+ public GetTransactionData(String txid, GeneralCoinAccount account, Context context, boolean mustWait) {
+ String serverUrl = InsightApiConstants.sProtocol + "://" + InsightApiConstants.getAddress(account.getCryptoCoin()) +"/";
+ this.mAccount = account;
+ this.mTxId= txid;
+ this.mServiceGenerator = new InsightApiServiceGenerator(serverUrl);
+ this.mContext = context;
+ this.mMustWait = mustWait;
+ }
+
+ /**
+ * Function to start the insight api call
+ */
+ @Override
+ public void run() {
+ if (this.mMustWait) {
+ //We are waiting for confirmation
+ try {
+ Thread.sleep(InsightApiConstants.sWaitTime);
+ } catch (InterruptedException ignored) {
+ //TODO this exception never rises
+ }
+ }
+
+ InsightApiService service = this.mServiceGenerator.getService(InsightApiService.class);
+ Call txiCall = service.getTransaction(InsightApiConstants.getPath(this.mAccount.getCryptoCoin()),this.mTxId);
+ txiCall.enqueue(this);
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Txi txi = response.body();
+
+ GeneralTransaction transaction = new GeneralTransaction();
+ transaction.setAccount(this.mAccount);
+ transaction.setTxid(txi.txid);
+ transaction.setBlock(txi.blockheight);
+ transaction.setDate(new Date(txi.time * 1000));
+ transaction.setFee((long) (txi.fee * Math.pow(10,this.mAccount.getCryptoCoin().getPrecision())));
+ transaction.setConfirm(txi.confirmations);
+ transaction.setType(this.mAccount.getCryptoCoin());
+ transaction.setBlockHeight(txi.blockheight);
+
+ for (Vin vin : txi.vin) {
+ GTxIO input = new GTxIO();
+ input.setAmount((long) (vin.value * Math.pow(10,this.mAccount.getCryptoCoin().getPrecision())));
+ input.setTransaction(transaction);
+ input.setOut(true);
+ input.setType(this.mAccount.getCryptoCoin());
+ String addr = vin.addr;
+ input.setAddressString(addr);
+ input.setIndex(vin.n);
+ input.setScriptHex(vin.scriptSig.hex);
+ input.setOriginalTxid(vin.txid);
+ for (GeneralCoinAddress address : this.mAccount.getAddresses()) {
+ if (address.getAddressString(this.mAccount.getNetworkParam()).equals(addr)) {
+ input.setAddress(address);
+ if (!address.hasTransactionOutput(input, this.mAccount.getNetworkParam())) {
+ address.getTransactionOutput().add(input);
+ }
+ }
+ }
+ transaction.getTxInputs().add(input);
+ }
+
+ for (Vout vout : txi.vout) {
+ if(vout.scriptPubKey.addresses == null || vout.scriptPubKey.addresses.length <= 0){
+ // The address is null, this must be a memo
+ String hex = vout.scriptPubKey.hex;
+ int opReturnIndex = hex.indexOf("6a");
+ if(opReturnIndex >= 0) {
+ byte[] memoBytes = new byte[Integer.parseInt(hex.substring(opReturnIndex+2,opReturnIndex+4),16)];
+ for(int i = 0; i < memoBytes.length;i++){
+ memoBytes[i] = Byte.parseByte(hex.substring(opReturnIndex+4+(i*2),opReturnIndex+6+(i*2)),16);
+ }
+ transaction.setMemo(new String(memoBytes));
+ System.out.println("Memo read : " + transaction.getMemo()); //TODO log this line
+ }
+
+ }else {
+ GTxIO output = new GTxIO();
+ output.setAmount((long) (vout.value * Math.pow(10, this.mAccount.getCryptoCoin().getPrecision())));
+ output.setTransaction(transaction);
+ output.setOut(false);
+ output.setType(this.mAccount.getCryptoCoin());
+ String addr = vout.scriptPubKey.addresses[0];
+ output.setAddressString(addr);
+ output.setIndex(vout.n);
+ output.setScriptHex(vout.scriptPubKey.hex);
+ for (GeneralCoinAddress address : this.mAccount.getAddresses()) {
+ if (address.getAddressString(this.mAccount.getNetworkParam()).equals(addr)) {
+ output.setAddress(address);
+ if (!address.hasTransactionInput(output, this.mAccount.getNetworkParam())) {
+ address.getTransactionInput().add(output);
+ }
+ }
+ }
+ transaction.getTxOutputs().add(output);
+ }
+ }
+
+ // This is for features like dash instantSend
+ if(txi.txlock && txi.confirmations< this.mAccount.getCryptoNet().getConfirmationsNeeded()){
+ transaction.setConfirm(this.mAccount.getCryptoNet().getConfirmationsNeeded());
+ }
+
+ //TODO database
+ /*SCWallDatabase db = new SCWallDatabase(this.mContext);
+ long idTransaction = db.getGeneralTransactionId(transaction);
+ if (idTransaction == -1) {
+ db.putGeneralTransaction(transaction);
+ } else {
+ transaction.setId(idTransaction);
+ db.updateGeneralTransaction(transaction);
+ }*/
+
+ this.mAccount.updateTransaction(transaction);
+ this.mAccount.balanceChange();
+
+ if (transaction.getConfirm() < this.mAccount.getCryptoNet().getConfirmationsNeeded()) {
+ //If transaction weren't confirmed, add the transaction to watch for change on the confirmations
+ new GetTransactionData(this.mTxId, this.mAccount, this.mContext, true).start();
+ }
+ }
+ }
+
+ /**
+ * TODO handle the failure response
+ * @param call the Call object
+ * @param t the reason of the failure
+ */
+ @Override
+ public void onFailure(Call call, Throwable t) {
+
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiConstants.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiConstants.java
new file mode 100644
index 0000000..b433f1b
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiConstants.java
@@ -0,0 +1,116 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi;
+
+import java.util.HashMap;
+
+import cy.agorise.crystalwallet.enums.CryptoCoin;
+
+/**
+ * Class holds all constant related to the Insight Api
+ *
+ */
+
+abstract class InsightApiConstants {
+ /**
+ * Protocol of the insight api calls
+ */
+ static final String sProtocol = "https";
+ /**
+ * Protocol of the insigiht api Socket.IO connection
+ */
+ static final String sProtocolSocketIO = "http";
+ /**
+ * Contains each url information for each coin
+ */
+ private static final HashMap sServerAddressPort = new HashMap<>();
+ /**
+ * Insight api Socket.IO new transaction by address notification
+ */
+ static final String sChangeAddressRoom = "bitcoind/addresstxid";
+ /**
+ * Socket.io subscribe command
+ */
+ static final String sSubscribeEmmit = "subscribe";
+ /**
+ * Tag used in the response of the address transaction notification
+ */
+ static final String sTxTag = "txid";
+
+ /**
+ * Wait time to check for confirmations
+ */
+ static long sWaitTime = (30 * 1000); //wait 1 minute
+
+ //Filled the serverAddressPort maps with static data
+ static{
+ //serverAddressPort.put(Coin.BITCOIN,new AddressPort("fr.blockpay.ch",3002,"node/btc/testnet","insight-api"));
+ sServerAddressPort.put(CryptoCoin.BITCOIN,new AddressPort("fr.blockpay.ch",3003,"node/btc/testnet","insight-api"));
+ //serverAddressPort.put(Coin.BITCOIN_TEST,new AddressPort("fr.blockpay.ch",3003,"node/btc/testnet","insight-api"));
+ sServerAddressPort.put(CryptoCoin.LITECOIN,new AddressPort("fr.blockpay.ch",3009,"node/ltc","insight-lite-api"));
+ sServerAddressPort.put(CryptoCoin.DASH,new AddressPort("fr.blockpay.ch",3005,"node/dash","insight-api-dash"));
+ sServerAddressPort.put(CryptoCoin.DOGECOIN,new AddressPort("fr.blockpay.ch",3006,"node/dogecoin","insight-api"));
+ }
+
+ /**
+ * Get the insight api server address
+ * @param coin The coin of the API to find
+ * @return The String address of the server, can be a name or the IP
+ */
+ static String getAddress(CryptoCoin coin){
+ return sServerAddressPort.get(coin).mServerAddress;
+ }
+
+ /**
+ * Get the port of the server Insight API
+ * @param coin The coin of the API to find
+ * @return The server number port
+ */
+ static int getPort(CryptoCoin coin){
+ return sServerAddressPort.get(coin).mPort;
+ }
+
+ /**
+ * Get the url path of the server Insight API
+ * @param coin The coin of the API to find
+ * @return The path of the Insight API
+ */
+ static String getPath(CryptoCoin coin){
+ return sServerAddressPort.get(coin).mPath + "/" + sServerAddressPort.get(coin).mInsightPath;
+ }
+
+ /**
+ * Contains all the url info neccessary to connects to the insight api
+ */
+ private static class AddressPort{
+ /**
+ * The server address
+ */
+ final String mServerAddress;
+ /**
+ * The port used in the Socket.io
+ */
+ final int mPort;
+ /**
+ * The path of the coin server
+ */
+ final String mPath;
+ /**
+ * The path of the insight api
+ */
+ final String mInsightPath;
+
+
+ /**
+ * Constructor
+ * @param serverAddress The server address of the Insight API
+ * @param port the port number of the Insight API
+ * @param path the path to the Insight API before the last /
+ * @param insightPath the path after the last / of the Insight API
+ */
+ AddressPort(String serverAddress, int port, String path, String insightPath) {
+ this.mServerAddress = serverAddress;
+ this.mPort = port;
+ this.mPath = path;
+ this.mInsightPath = insightPath;
+ }
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiService.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiService.java
new file mode 100644
index 0000000..5e36ca1
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiService.java
@@ -0,0 +1,52 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi;
+
+import com.google.gson.JsonObject;
+
+import cy.agorise.crystalwallet.apigenerator.insightapi.models.AddressTxi;
+import cy.agorise.crystalwallet.apigenerator.insightapi.models.Txi;
+import retrofit2.Call;
+import retrofit2.http.Field;
+import retrofit2.http.FormUrlEncoded;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.Path;
+
+/**
+ * Holds each call to the insigh api server
+ */
+
+interface InsightApiService {
+
+ /**
+ * The query for the info of a single transaction
+ * @param path The path of the insight api without the server address
+ * @param txid the transasction to be query
+ */
+ @GET("{path}/tx/{txid}")
+ Call getTransaction(@Path(value = "path", encoded = true) String path, @Path(value = "txid", encoded = true) String txid);
+
+ /**
+ * The query for the transasctions of multiples addresses
+ * @param path The path of the insight api without the server address
+ * @param addrs the addresses to be query each separated with a ","
+ */
+ @GET("{path}/addrs/{addrs}/txs")
+ Call getTransactionByAddress(@Path(value = "path", encoded = true) String path, @Path(value = "addrs", encoded = true) String addrs);
+
+ /**
+ * Broadcast Transaction
+ * @param path The path of the insight api without the server address
+ * @param rawtx the rawtx to send in Hex String
+ */
+ @FormUrlEncoded
+ @POST("{path}/tx/send")
+ Call broadcastTransaction(@Path(value = "path", encoded = true) String path, @Field("rawtx") String rawtx);
+
+ /**
+ * Get the estimate rate fee for a coin in the Insight API
+ * @param path The path of the insight api without the server address
+ */
+ @GET("{path}/utils/estimatefee?nbBlocks=2")
+ Call estimateFee(@Path(value = "path", encoded = true) String path);
+
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiServiceGenerator.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiServiceGenerator.java
new file mode 100644
index 0000000..7eea5c6
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiServiceGenerator.java
@@ -0,0 +1,130 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import okhttp3.logging.HttpLoggingInterceptor;
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+/**
+ * Generatir fir tge okhttp connection of the Insight API
+ * TODO finish documentation
+ */
+
+class InsightApiServiceGenerator {
+ /**
+ * Tag used for logging
+ */
+ public static String TAG = "InsightApiServiceGenerator";
+ /**
+ * The complete uri to connect to the insight api, this change from coin to coin
+ */
+ private static String sApiBaseUrl;
+ /**
+ * Loggin interceptor
+ */
+ private static HttpLoggingInterceptor sLogging;
+ /**
+ * Http builder
+ */
+ private static OkHttpClient.Builder sClientBuilder;
+ /**
+ * Builder for the retrofit class
+ */
+ private static Retrofit.Builder sBuilder;
+ /**
+ *
+ */
+ private static HashMap, Object> sServices;
+
+ /**
+ * Constructor, using the url of a insigth api coin
+ * @param apiBaseUrl The complete url to the server of the insight api
+ */
+ InsightApiServiceGenerator(String apiBaseUrl) {
+ sApiBaseUrl= apiBaseUrl;
+ sLogging = new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY);
+ sClientBuilder = new OkHttpClient.Builder().addInterceptor(sLogging);
+ sBuilder = new Retrofit.Builder().baseUrl(sApiBaseUrl).addConverterFactory(GsonConverterFactory.create());
+ sServices = new HashMap<>();
+ }
+
+ /**
+ *
+ * @param klass
+ * @param thing
+ * @param
+ */
+ private static void setService(Class klass, T thing) {
+ sServices.put(klass, thing);
+ }
+
+ /**
+ *
+ * @param serviceClass
+ * @param
+ * @return
+ */
+ public T getService(Class serviceClass) {
+
+ T service = serviceClass.cast(sServices.get(serviceClass));
+ if (service == null) {
+ service = createService(serviceClass);
+ setService(serviceClass, service);
+ }
+ return service;
+ }
+
+ /**
+ *
+ * @param serviceClass
+ * @param
+ * @return
+ */
+ private static S createService(Class serviceClass) {
+
+ sClientBuilder.interceptors().add(new Interceptor() {
+ @Override
+ public okhttp3.Response intercept(Chain chain) throws IOException {
+ okhttp3.Request original = chain.request();
+ okhttp3.Request.Builder requestBuilder = original.newBuilder().method(original.method(), original.body());
+
+ okhttp3.Request request = requestBuilder.build();
+ return chain.proceed(request);
+ }
+ });
+ sClientBuilder.readTimeout(5, TimeUnit.MINUTES);
+ sClientBuilder.connectTimeout(5, TimeUnit.MINUTES);
+ OkHttpClient client = sClientBuilder.build();
+ Retrofit retrofit = sBuilder.client(client).build();
+ return retrofit.create(serviceClass);
+
+ }
+
+ /**
+ *
+ * @return
+ */
+ public static InsightApiService Create() {
+ OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
+ httpClient.interceptors().add(new Interceptor() {
+ @Override
+ public okhttp3.Response intercept(Chain chain) throws IOException {
+ okhttp3.Request original = chain.request();
+
+ // Customize the request
+ okhttp3.Request request = original.newBuilder().method(original.method(), original.body()).build();
+
+ return chain.proceed(request);
+ }
+ });
+
+ OkHttpClient client = httpClient.build();
+ Retrofit retrofit = new Retrofit.Builder().baseUrl(sApiBaseUrl).client(client).build();
+ return retrofit.create(InsightApiService.class);
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/AddressTxi.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/AddressTxi.java
new file mode 100644
index 0000000..f12ac69
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/AddressTxi.java
@@ -0,0 +1,24 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi.models;
+
+/**
+ * Represents the address txi of a insishgt api response
+ */
+public class AddressTxi {
+ /**
+ * The total number of items
+ */
+ public int totalItems;
+ /**
+ * The start index of the current txi
+ */
+ public int from;
+ /**
+ * the last index of the current txi
+ */
+ public int to;
+ /**
+ * The arrays of txi of this response
+ */
+ public Txi[] items;
+
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/ScriptPubKey.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/ScriptPubKey.java
new file mode 100644
index 0000000..0c157f9
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/ScriptPubKey.java
@@ -0,0 +1,24 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi.models;
+
+/**
+ * The transasction Script public keym is used to validate
+ */
+
+public class ScriptPubKey {
+ /**
+ * The code to validate in hex
+ */
+ public String hex;
+ /**
+ * the code to validate this transaction
+ */
+ public String asm;
+ /**
+ * the acoin address involved
+ */
+ public String[] addresses;
+ /**
+ * The type of the hash
+ */
+ public String type;
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/ScriptSig.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/ScriptSig.java
new file mode 100644
index 0000000..bfa5c89
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/ScriptSig.java
@@ -0,0 +1,16 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi.models;
+
+/**
+ * Reprensents the Script signature of an trnasaction Input
+ */
+
+public class ScriptSig {
+ /**
+ * The hex
+ */
+ public String hex;
+ /**
+ * the asm
+ */
+ public String asm;
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Txi.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Txi.java
new file mode 100644
index 0000000..6060be6
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Txi.java
@@ -0,0 +1,69 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi.models;
+
+/**
+ * Represents one transaction input of the insight API
+ */
+
+public class Txi {
+ /**
+ * The id of this transaction
+ */
+ public String txid;
+ /**
+ * the version number of this transaction
+ */
+ public int version;
+ /**
+ * Time to hold this transaction
+ */
+ public long locktime;
+ /**
+ * The array of the transaction inputs
+ */
+ public Vin[] vin;
+ /**
+ * the array of the transactions outputs
+ */
+ public Vout[] vout;
+ /**
+ * this block hash
+ */
+ public String blockhash;
+ /**
+ * The blockheight where this transaction belongs, if 0 this transactions hasn't be included in any block yet
+ */
+ public int blockheight;
+ /**
+ * Number of confirmations
+ */
+ public int confirmations;
+ /**
+ * The time of the first broadcast fo this transaction
+ */
+ public long time;
+ /**
+ * The time which this transaction was included
+ */
+ public long blocktime;
+ /**
+ * Total value to transactions outputs
+ */
+ public double valueOut;
+ /**
+ * The size in bytes
+ */
+ public int size;
+ /**
+ * Total value of transactions inputs
+ */
+ public double valueIn;
+ /**
+ * Fee of this transaction has to be valueIn - valueOut
+ */
+ public double fee;
+ /**
+ * This is only for dash, is the instantsend state
+ */
+ public boolean txlock=false;
+
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Vin.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Vin.java
new file mode 100644
index 0000000..e369550
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Vin.java
@@ -0,0 +1,44 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi.models;
+
+/**
+ * This represents a transaction input
+ */
+
+public class Vin {
+ /**
+ * The original transaction id where this transaction is an output
+ */
+ public String txid;
+ /**
+ *
+ */
+ public int vout;
+ /**
+ * Sequence fo the transaction
+ */
+ public long sequence;
+ /**
+ * Order of the transasction input on the transasction
+ */
+ public int n;
+ /**
+ * The script signature
+ */
+ public ScriptSig scriptSig;
+ /**
+ * The addr of this transaction
+ */
+ public String addr;
+ /**
+ * Value in satoshi
+ */
+ public long valueSat;
+ /**
+ * Calue of this transaction
+ */
+ public double value;
+ /**
+ *
+ */
+ public String doubleSpentTxID;
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Vout.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Vout.java
new file mode 100644
index 0000000..347ded9
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Vout.java
@@ -0,0 +1,32 @@
+package cy.agorise.crystalwallet.apigenerator.insightapi.models;
+
+/**
+ * Represents a Transasction output
+ */
+
+public class Vout {
+ /**
+ * The amount of coin
+ */
+ public double value;
+ /**
+ * the order of this transaciton output on the transaction
+ */
+ public int n;
+ /**
+ * The script public key
+ */
+ public ScriptPubKey scriptPubKey;
+ /**
+ * If this transaciton output was spent what txid it belongs
+ */
+ public String spentTxId;
+ /**
+ * The index on the transaction that this transaction was spent
+ */
+ public String spentIndex;
+ /**
+ * The block height of the transaction this output was spent
+ */
+ public String spentHeight;
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/application/CrystalApplication.java b/app/src/main/java/cy/agorise/crystalwallet/application/CrystalApplication.java
index 2b2a7db..d6dad49 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/application/CrystalApplication.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/application/CrystalApplication.java
@@ -93,7 +93,7 @@ public class CrystalApplication extends Application {
CryptoNetEvents.getInstance().addListener(crystalWalletNotifier);
//Next line is for use the bitshares main net
- //CryptoNetManager.addCryptoNetURL(CryptoNet.BITSHARES,BITSHARES_URL);
+ CryptoNetManager.addCryptoNetURL(CryptoNet.BITSHARES,BITSHARES_URL);
GeneralSetting generalSettingPreferredLanguage = db.generalSettingDao().getSettingByName(GeneralSetting.SETTING_NAME_PREFERRED_LANGUAGE);
diff --git a/app/src/main/java/cy/agorise/crystalwallet/application/CrystalSecurityMonitor.java b/app/src/main/java/cy/agorise/crystalwallet/application/CrystalSecurityMonitor.java
index c3ca8bb..e027107 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/application/CrystalSecurityMonitor.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/application/CrystalSecurityMonitor.java
@@ -15,6 +15,7 @@ import java.util.List;
import cy.agorise.crystalwallet.activities.PatternRequestActivity;
import cy.agorise.crystalwallet.activities.PinRequestActivity;
+import cy.agorise.crystalwallet.activities.PocketRequestActivity;
import cy.agorise.crystalwallet.models.GeneralSetting;
import cy.agorise.crystalwallet.notifiers.CrystalWalletNotifier;
import cy.agorise.crystalwallet.viewmodels.GeneralSettingListViewModel;
@@ -27,6 +28,7 @@ public class CrystalSecurityMonitor implements Application.ActivityLifecycleCall
private int numStarted = 0;
private String passwordEncrypted;
private String patternEncrypted;
+ private String yubikeyOathTotpPasswordEncrypted;
private static CrystalSecurityMonitor instance;
private GeneralSettingListViewModel generalSettingListViewModel;
@@ -38,8 +40,10 @@ public class CrystalSecurityMonitor implements Application.ActivityLifecycleCall
this.passwordEncrypted = "";
this.patternEncrypted = "";
+ this.yubikeyOathTotpPasswordEncrypted = "";
GeneralSetting passwordGeneralSetting = generalSettingListViewModel.getGeneralSettingByName(GeneralSetting.SETTING_PASSWORD);;
GeneralSetting patternGeneralSetting = generalSettingListViewModel.getGeneralSettingByName(GeneralSetting.SETTING_PATTERN);;
+ GeneralSetting yubikeyOathTotpPasswordSetting = generalSettingListViewModel.getGeneralSettingByName(GeneralSetting.SETTING_YUBIKEY_OATH_TOTP_PASSWORD);;
if (passwordGeneralSetting != null){
this.passwordEncrypted = passwordGeneralSetting.getValue();
@@ -47,6 +51,9 @@ public class CrystalSecurityMonitor implements Application.ActivityLifecycleCall
if (patternGeneralSetting != null){
this.patternEncrypted = patternGeneralSetting.getValue();
}
+ if (yubikeyOathTotpPasswordSetting != null){
+ this.yubikeyOathTotpPasswordEncrypted = yubikeyOathTotpPasswordSetting.getValue();
+ }
}
public static CrystalSecurityMonitor getInstance(final FragmentActivity fragmentActivity){
@@ -57,6 +64,10 @@ public class CrystalSecurityMonitor implements Application.ActivityLifecycleCall
return instance;
}
+ public static String getServiceName(){
+ return "cy.agorise.crystalwallet";
+ }
+
public void clearSecurity(){
this.patternEncrypted = "";
this.passwordEncrypted = "";
@@ -65,6 +76,12 @@ public class CrystalSecurityMonitor implements Application.ActivityLifecycleCall
generalSettingListViewModel.deleteGeneralSettingByName(GeneralSetting.SETTING_PATTERN);
}
+ public void clearSecurity2ndFactor(){
+ this.yubikeyOathTotpPasswordEncrypted = "";
+
+ generalSettingListViewModel.deleteGeneralSettingByName(GeneralSetting.SETTING_YUBIKEY_OATH_TOTP_PASSWORD);
+ }
+
public void setPasswordSecurity(String password){
clearSecurity();
this.passwordEncrypted = password;
@@ -95,6 +112,19 @@ public class CrystalSecurityMonitor implements Application.ActivityLifecycleCall
return "";
}
+ public boolean is2ndFactorSet(){
+ return !this.yubikeyOathTotpPasswordEncrypted.equals("");
+ }
+
+ public void setYubikeyOathTotpSecurity(String name, String password){
+ this.yubikeyOathTotpPasswordEncrypted = password;
+ GeneralSetting yubikeyOathTotpSetting = new GeneralSetting();
+ yubikeyOathTotpSetting.setName(GeneralSetting.SETTING_YUBIKEY_OATH_TOTP_PASSWORD);
+ yubikeyOathTotpSetting.setValue(password);
+
+ generalSettingListViewModel.saveGeneralSetting(yubikeyOathTotpSetting);
+ }
+
@Override
public void onActivityStarted(Activity activity) {
if (numStarted == 0) {
@@ -130,6 +160,19 @@ public class CrystalSecurityMonitor implements Application.ActivityLifecycleCall
}
}
+ public void call2ndFactor(Activity activity){
+ Intent intent = null;
+ if ((this.yubikeyOathTotpPasswordEncrypted != null) && (!this.yubikeyOathTotpPasswordEncrypted.equals(""))) {
+ intent = new Intent(activity, PocketRequestActivity.class);
+ //intent.putExtra("ACTIVITY_TYPE", "PASSWORD_REQUEST");
+ activity.startActivity(intent);
+ }
+ }
+
+ public String get2ndFactorValue(){
+ return this.yubikeyOathTotpPasswordEncrypted;
+ }
+
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
//
diff --git a/app/src/main/java/cy/agorise/crystalwallet/application/constant/BitsharesConstant.java b/app/src/main/java/cy/agorise/crystalwallet/application/constant/BitsharesConstant.java
index 9021c2f..0b807fa 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/application/constant/BitsharesConstant.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/application/constant/BitsharesConstant.java
@@ -30,7 +30,9 @@ public abstract class BitsharesConstant {
"http://185.208.208.147:11012", // Openledger node
};
- public final static String FAUCET_URL = "http://185.208.208.147:5010";
+ //testnet faucet
+ //public final static String FAUCET_URL = "http://185.208.208.147:5010";
+ public final static String FAUCET_URL = "https://de.palmpay.io";
public final static String EQUIVALENT_URL = "wss://bitshares.openledger.info/ws";
public final static BitsharesAsset[] SMARTCOINS = new BitsharesAsset[]{
diff --git a/app/src/main/java/cy/agorise/crystalwallet/fragments/SecuritySettingsFragment.java b/app/src/main/java/cy/agorise/crystalwallet/fragments/SecuritySettingsFragment.java
index bd7447a..86b201b 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/fragments/SecuritySettingsFragment.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/fragments/SecuritySettingsFragment.java
@@ -1,20 +1,42 @@
package cy.agorise.crystalwallet.fragments;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.nfc.tech.IsoDep;
+import android.nfc.tech.MifareClassic;
+import android.nfc.tech.NdefFormatable;
+import android.nfc.tech.NfcA;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.Switch;
+import android.widget.Toast;
+
+import org.apache.commons.codec.binary.Base32;
+
+import java.io.IOException;
import butterknife.BindView;
import butterknife.ButterKnife;
+import butterknife.OnCheckedChanged;
import cy.agorise.crystalwallet.R;
import cy.agorise.crystalwallet.application.CrystalSecurityMonitor;
import cy.agorise.crystalwallet.models.GeneralSetting;
import cy.agorise.crystalwallet.util.ChildViewPager;
+import cy.agorise.crystalwallet.util.yubikey.Algorithm;
+import cy.agorise.crystalwallet.util.yubikey.OathType;
+import cy.agorise.crystalwallet.util.yubikey.YkOathApi;
/**
* Created by xd on 1/17/18.
@@ -34,11 +56,23 @@ public class SecuritySettingsFragment extends Fragment {
return fragment;
}
+ private NfcAdapter mNfcAdapter;
+ private PendingIntent pendingIntent;
+ private IntentFilter ndef;
+ IntentFilter[] intentFiltersArray;
+ String[][] techList;
+
+
@BindView(R.id.pager)
public ChildViewPager mPager;
public SecurityPagerAdapter securityPagerAdapter;
+ @BindView(R.id.sPocketSecurity)
+ Switch sPocketSecurity;
+ @BindView(R.id.etPocketPassword)
+ EditText etPocketPassword;
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -66,6 +100,9 @@ public class SecuritySettingsFragment extends Fragment {
mPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
tabLayout.addOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(mPager));
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(this.getActivity());
+ this.configureForegroundDispatch();
+
return v;
}
@@ -101,4 +138,87 @@ public class SecuritySettingsFragment extends Fragment {
return 3;
}
}
+
+ @OnCheckedChanged(R.id.sPocketSecurity)
+ public void onPocketSecurityActivated(CompoundButton button, boolean checked){
+ if (checked) {
+ enableNfc();
+ } else {
+ disableNfc();
+ }
+ }
+
+ public void configureForegroundDispatch(){
+ if (mNfcAdapter != null) {
+ pendingIntent = PendingIntent.getActivity(
+ this.getActivity(), 0, new Intent(this.getActivity(), getActivity().getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
+
+ ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
+ try {
+ ndef.addDataType("*/*"); /* Handles all MIME based dispatches.
+ You should specify only the ones that you need. */
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ throw new RuntimeException("fail", e);
+ }
+ intentFiltersArray = new IntentFilter[]{ndef,};
+ techList = new String[][]{ new String[] {IsoDep.class.getName(), NfcA.class.getName(), MifareClassic.class.getName(), NdefFormatable.class.getName()} };
+
+ } else {
+ Toast.makeText(this.getContext(), "This device doesn't support NFC.", Toast.LENGTH_LONG).show();
+ }
+ }
+
+ public void enableNfc(){
+ mNfcAdapter.enableForegroundDispatch(this.getActivity(), pendingIntent, intentFiltersArray, techList);
+ Toast.makeText(this.getContext(), "Tap with your yubikey", Toast.LENGTH_LONG).show();
+ }
+
+ public void disableNfc(){
+ mNfcAdapter.disableForegroundDispatch(this.getActivity());
+ }
+
+ public void onPause() {
+ super.onPause();
+ disableNfc();
+ }
+
+ public void onResume() {
+ super.onResume();
+ if (mNfcAdapter != null) {
+ enableNfc();
+ }
+ }
+
+ public void onNewIntent(Intent intent) {
+ Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+ IsoDep tagIsoDep = IsoDep.get(tagFromIntent);
+ Log.i("Tag from nfc","New Intent");
+ try {
+
+ String serviceName = "cy.agorise.crystalwallet";
+ String encodedSecret = etPocketPassword.getText().toString();
+ Base32 decoder = new Base32();
+
+ if ((encodedSecret != null) && (!encodedSecret.equals("")) && decoder.isInAlphabet(encodedSecret)) {
+ byte[] secret = decoder.decode(encodedSecret);
+ tagIsoDep.connect();
+ tagIsoDep.setTimeout(15000);
+ YkOathApi ykOathApi = new YkOathApi(tagIsoDep);
+
+ try {
+ ykOathApi.putCode(serviceName, secret, OathType.TOTP, Algorithm.SHA256, (byte) 6, 0, false);
+ CrystalSecurityMonitor.getInstance(null).setYubikeyOathTotpSecurity(CrystalSecurityMonitor.getServiceName(),encodedSecret);
+ } catch(IOException e) {
+ Toast.makeText(this.getContext(), "There's no space for new credentials!", Toast.LENGTH_LONG).show();
+ }
+
+ tagIsoDep.close();
+ Toast.makeText(this.getContext(), "Credential saved!", Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(this.getContext(), "Invalid password for credential", Toast.LENGTH_LONG).show();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/models/GTxIO.java b/app/src/main/java/cy/agorise/crystalwallet/models/GTxIO.java
new file mode 100644
index 0000000..2d14499
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/models/GTxIO.java
@@ -0,0 +1,166 @@
+package cy.agorise.crystalwallet.models;
+
+import cy.agorise.crystalwallet.enums.CryptoCoin;
+
+/**
+ * General Coin Transaction Input/Output
+ *
+ * This class represent each Input or Output Transaction of a General Coin Transaction
+ *
+ * Created by henry on 06/02/2017.
+ */
+
+public class GTxIO {
+ /**
+ * The id on the database
+ */
+ private long mId = -1;
+ /**
+ * The Coin type of this transaction
+ */
+ private CryptoCoin mType;
+ /**
+ * The index on the transaction Input/Output
+ */
+ private int mIndex;
+ /**
+ * The address that this transaction Input/Output belongs
+ */
+ private GeneralCoinAddress mAddress;
+ /**
+ * The transaction that this Input/Output belongs
+ */
+ private GeneralTransaction mTransaction;
+ /**
+ * The amount
+ */
+ private long mAmount;
+ /**
+ * If this transaction is output or input
+ */
+ private boolean mIsOut;
+ /**
+ * The address of this transaction as String
+ */
+ private String mAddressString;
+ /**
+ * The Script as Hex
+ */
+ private String mScriptHex;
+ /**
+ * If this is a transaction output, the original transaction where this is input
+ */
+ private String mOriginalTxId;
+
+ /**
+ * Empty Constructor
+ */
+ public GTxIO() {
+
+ }
+
+ /**
+ * General Constructor, used by the DB.
+ *
+ * @param id The id in the dataabase
+ * @param type The coin mType
+ * @param address The addres fo an account on the wallet, or null if the address is external
+ * @param transaction The transaction where this belongs
+ * @param amount The amount with the lowest precision
+ * @param isOut if this is an output
+ * @param addressString The string of the General Coin address, this can't be null
+ * @param index The index on the transaction
+ * @param scriptHex The script in hex String
+ */
+ public GTxIO(long id, CryptoCoin type, GeneralCoinAddress address, GeneralTransaction transaction, long amount, boolean isOut, String addressString, int index, String scriptHex) {
+ this.mId = id;
+ this.mType = type;
+ this.mAddress = address;
+ this.mTransaction = transaction;
+ this.mAmount = amount;
+ this.mIsOut = isOut;
+ this.mAddressString = addressString;
+ this.mIndex = index;
+ this.mScriptHex = scriptHex;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public void setId(long id) {
+ this.mId = id;
+ }
+
+ public CryptoCoin getType() {
+ return mType;
+ }
+
+ public void setType(CryptoCoin type) {
+ this.mType = type;
+ }
+
+ public int getIndex() {
+ return mIndex;
+ }
+
+ public void setIndex(int index) {
+ this.mIndex = index;
+ }
+
+ public GeneralCoinAddress getAddress() {
+ return mAddress;
+ }
+
+ public void setAddress(GeneralCoinAddress address) {
+ this.mAddress = address;
+ }
+
+ public GeneralTransaction getTransaction() {
+ return mTransaction;
+ }
+
+ public void setTransaction(GeneralTransaction transaction) {
+ this.mTransaction = transaction;
+ }
+
+ public long getAmount() {
+ return mAmount;
+ }
+
+ public void setAmount(long amount) {
+ this.mAmount = amount;
+ }
+
+ public boolean isOut() {
+ return mIsOut;
+ }
+
+ public void setOut(boolean out) {
+ mIsOut = out;
+ }
+
+ public String getAddressString() {
+ return mAddressString;
+ }
+
+ public void setAddressString(String addressString) {
+ this.mAddressString = addressString;
+ }
+
+ public String getScriptHex() {
+ return mScriptHex;
+ }
+
+ public void setScriptHex(String scriptHex) {
+ this.mScriptHex = scriptHex;
+ }
+
+ public String getOriginalTxid() {
+ return mOriginalTxId;
+ }
+
+ public void setOriginalTxid(String originalTxid) {
+ this.mOriginalTxId = originalTxid;
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAccount.java b/app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAccount.java
new file mode 100644
index 0000000..68f44f2
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAccount.java
@@ -0,0 +1,392 @@
+package cy.agorise.crystalwallet.models;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.google.gson.JsonObject;
+
+import org.bitcoinj.core.NetworkParameters;
+import org.bitcoinj.crypto.ChildNumber;
+import org.bitcoinj.crypto.DeterministicKey;
+import org.bitcoinj.crypto.HDKeyDerivation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+import cy.agorise.crystalwallet.enums.CryptoCoin;
+import cy.agorise.crystalwallet.enums.CryptoNet;
+
+/**
+ * Created by henry on 2/8/2018.
+ */
+
+public abstract class GeneralCoinAccount extends CryptoNetAccount {
+ /**
+ * The account number of the BIP-44
+ */
+ protected int mAccountNumber;
+ /**
+ * The index of the last used external address
+ */
+ protected int mLastExternalIndex;
+ /**
+ * The indes of the last used change address
+ */
+ protected int mLastChangeIndex;
+ /**
+ * The account key, this is calculated as a cache
+ */
+ protected DeterministicKey mAccountKey;
+ /**
+ * With this key we can calculate the external addresses
+ */
+ protected DeterministicKey mExternalKey;
+ /**
+ * With this key we can calculate the change address
+ */
+ protected DeterministicKey mChangeKey;
+ /**
+ * The keys for externals addresses
+ */
+ protected HashMap mExternalKeys = new HashMap();
+ /**
+ * The keys for the change addresses
+ */
+ protected HashMap mChangeKeys = new HashMap();
+
+ /**
+ * The list of transaction that involves this account
+ */
+ protected List mTransactions = new ArrayList();
+
+ /**
+ * The Limit gap define in the BIP-44
+ */
+ private final static int sAddressGap = 20;
+
+ /**
+ * is the coin number defined by the SLIP-44
+ */
+ private final int mCoinNumber;
+
+ public GeneralCoinAccount(long mId, AccountSeed seed, int mAccountIndex, CryptoNet mCryptoNet, int mAccountNumber, int mLastExternalIndex, int mLastChangeIndex, int mCoinNumber) {
+ super(mId, seed.getId(), mAccountIndex, mCryptoNet);
+ this.mAccountNumber = mAccountNumber;
+ this.mLastExternalIndex = mLastExternalIndex;
+ this.mLastChangeIndex = mLastChangeIndex;
+ this.mCoinNumber = mCoinNumber;
+ calculateAddresses((DeterministicKey) seed.getPrivateKey());
+ }
+
+ /**
+ * Setter for the transactions of this account, this is used from the database
+ */
+ public void setTransactions(List transactions) {
+ this.mTransactions = transactions;
+ }
+
+ /**
+ * Calculates each basic key, not the addresses keys using the BIP-44
+ */
+ private void calculateAddresses(DeterministicKey masterKey) {
+ DeterministicKey purposeKey = HDKeyDerivation.deriveChildKey(masterKey,
+ new ChildNumber(44, true));
+ DeterministicKey coinKey = HDKeyDerivation.deriveChildKey(purposeKey,
+ new ChildNumber(this.mCoinNumber, true));
+ this.mAccountKey = HDKeyDerivation.deriveChildKey(coinKey,
+ new ChildNumber(this.mAccountNumber, true));
+ this.mExternalKey = HDKeyDerivation.deriveChildKey(this.mAccountKey,
+ new ChildNumber(0, false));
+ this.mChangeKey = HDKeyDerivation.deriveChildKey(this.mAccountKey,
+ new ChildNumber(1, false));
+ }
+
+ /**
+ * Calculate the external address keys until the index + gap
+ */
+ public void calculateGapExternal() {
+ for (int i = 0; i < this.mLastExternalIndex + this.sAddressGap; i++) {
+ if (!this.mExternalKeys.containsKey(i)) {
+ this.mExternalKeys.put(i, new GeneralCoinAddress(this, false, i,
+ HDKeyDerivation.deriveChildKey(this.mExternalKey,
+ new ChildNumber(i, false))));
+ }
+ }
+ }
+
+ /**
+ * Calculate the change address keys until the index + gap
+ */
+ public void calculateGapChange() {
+ for (int i = 0; i < this.mLastChangeIndex + this.sAddressGap; i++) {
+ if (!this.mChangeKeys.containsKey(i)) {
+ this.mChangeKeys.put(i, new GeneralCoinAddress(this, true, i,
+ HDKeyDerivation.deriveChildKey(this.mChangeKey,
+ new ChildNumber(i, false))));
+ }
+ }
+ }
+
+ //TODO check init address
+ /*public List getAddresses(SCWallDatabase db) {
+ //TODO check for used address
+ this.getNextReceiveAddress();
+ this.getNextChangeAddress();
+ this.calculateGapExternal();
+ this.calculateGapChange();
+
+ List addresses = new ArrayList();
+ addresses.addAll(this.mChangeKeys.values());
+ addresses.addAll(this.mExternalKeys.values());
+ this.saveAddresses(db);
+ return addresses;
+ }*/
+
+ /**
+ * Get the list of all the address, external and change addresses
+ * @return a list with all the addresses of this account
+ */
+ public List getAddresses() {
+ List addresses = new ArrayList();
+ addresses.addAll(this.mChangeKeys.values());
+ addresses.addAll(this.mExternalKeys.values());
+ return addresses;
+ }
+
+ /**
+ * Charges the list of addresse of this account, this is used from the database
+ */
+ public void loadAddresses(List addresses) {
+ for (GeneralCoinAddress address : addresses) {
+ if (address.isIsChange()) {
+ this.mChangeKeys.put(address.getIndex(), address);
+ } else {
+ this.mExternalKeys.put(address.getIndex(), address);
+ }
+ }
+ }
+
+ //TODO save address
+ /*public void saveAddresses(SCWallDatabase db) {
+ for (GeneralCoinAddress externalAddress : this.mExternalKeys.values()) {
+ if (externalAddress.getId() == -1) {
+ long id = db.putGeneralCoinAddress(externalAddress);
+ if(id != -1)
+ externalAddress.setId(id);
+ } else {
+ db.updateGeneralCoinAddress(externalAddress);
+ }
+ }
+
+ for (GeneralCoinAddress changeAddress : this.mChangeKeys.values()) {
+ if (changeAddress.getId() == -1) {
+ Log.i("SCW","change address id " + changeAddress.getId());
+ long id = db.putGeneralCoinAddress(changeAddress);
+ if(id != -1)
+ changeAddress.setId(id);
+ } else {
+ db.updateGeneralCoinAddress(changeAddress);
+ }
+ }
+
+ db.updateGeneralCoinAccount(this);
+ }*/
+
+ /**
+ * Getter of the account number
+ */
+ public int getAccountNumber() {
+ return this.mAccountNumber;
+ }
+
+ /**
+ * Getter of the last external address used index
+ */
+ public int getLastExternalIndex() {
+ return this.mLastExternalIndex;
+ }
+
+ /**
+ * Getter of the last change address used index
+ */
+ public int getLastChangeIndex() {
+ return this.mLastChangeIndex;
+ }
+
+ /**
+ * Getter of the next receive address
+ * @return The next unused receive address to be used
+ */
+ public abstract String getNextReceiveAddress();
+
+ /**
+ * Getter of the next change address
+ * @return The next unused change address to be used
+ */
+ public abstract String getNextChangeAddress();
+
+ /**
+ * Transfer coin amount to another address
+ *
+ * @param toAddress The destination address
+ * @param coin the coin
+ * @param amount the amount to send in satoshi
+ * @param memo the memo, this can be empty
+ * @param context the android context
+ */
+ public abstract void send(String toAddress, CryptoCoin coin, long amount, String memo,
+ Context context);
+
+ /**
+ * Transform this account into json object to be saved in the bin file, or any other file
+ */
+ public JsonObject toJson() {
+ JsonObject answer = new JsonObject();
+ answer.addProperty("type", this.getCryptoNet().name());
+ answer.addProperty("name", this.getName());
+ answer.addProperty("accountNumber", this.mAccountNumber);
+ answer.addProperty("changeIndex", this.mLastChangeIndex);
+ answer.addProperty("externalIndex", this.mLastExternalIndex);
+ return answer;
+ }
+
+ /**
+ * Getter of the list of transactions
+ */
+ public List getTransactions() {
+ List transactions = new ArrayList();
+ for (GeneralCoinAddress address : this.mExternalKeys.values()) {
+ for (GTxIO giotx : address.getTransactionInput()) {
+ if (!transactions.contains(giotx.getTransaction())) {
+ transactions.add(giotx.getTransaction());
+ }
+ }
+ for (GTxIO giotx : address.getTransactionOutput()) {
+ if (!transactions.contains(giotx.getTransaction())) {
+ transactions.add(giotx.getTransaction());
+ }
+ }
+ }
+
+ for (GeneralCoinAddress address : this.mChangeKeys.values()) {
+ for (GTxIO giotx : address.getTransactionInput()) {
+ if (!transactions.contains(giotx.getTransaction())) {
+ transactions.add(giotx.getTransaction());
+ }
+ }
+ for (GTxIO giotx : address.getTransactionOutput()) {
+ if (!transactions.contains(giotx.getTransaction())) {
+ transactions.add(giotx.getTransaction());
+ }
+ }
+ ;
+ }
+
+ Collections.sort(transactions, new TransactionsCustomComparator());
+ return transactions;
+ }
+
+ public CryptoCoin getCryptoCoin(){
+ return CryptoCoin.valueOf(this.getCryptoNet().name());
+ }
+
+ /**
+ * Get the address as string of an adrees index
+ * @param index The index of the address
+ * @param change if it is change addres or is a external address
+ * @return The Address as string
+ */
+ public abstract String getAddressString(int index, boolean change);
+
+ /**
+ * Get the GeneralCoinAddress object of an address
+ * @param index the index of the address
+ * @param change if it is change addres or is a external address
+ * @return The GeneralCoinAddress of the address
+ */
+ public abstract GeneralCoinAddress getAddress(int index, boolean change);
+
+ /**
+ * Return the network parameters, this is used for the bitcoiinj library
+ */
+ public abstract NetworkParameters getNetworkParam();
+
+ /**
+ * Triggers the event onBalanceChange
+ */
+ public void balanceChange() {
+ this._fireOnChangeBalance(this.getBalance().get(0)); //TODO make it more genertic
+ }
+
+ public abstract List getBalance();
+
+ /**
+ * Compare the transaction, to order it for the list of transaction
+ */
+ public class TransactionsCustomComparator implements Comparator {
+ @Override
+ public int compare(GeneralTransaction o1, GeneralTransaction o2) {
+ return o1.getDate().compareTo(o2.getDate());
+ }
+ }
+
+ /**
+ * Add listener for the onChangebalance Event
+ */
+ /*public void addChangeBalanceListener(ChangeBalanceListener listener) {
+ this.mChangeBalanceListeners.add(listener);
+ }*/
+
+ /**
+ * Fire the onChangeBalance event
+ */
+ protected void _fireOnChangeBalance(CryptoNetBalance balance) {
+ /*for (ChangeBalanceListener listener : this.mChangeBalanceListeners) {
+ listener.balanceChange(balance);
+ }*/
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ GeneralCoinAccount that = (GeneralCoinAccount) o;
+
+ if (this.getCryptoNet() != that.getCryptoNet()) return false;
+ if (this.getAccountNumber() != that.getAccountNumber()) return false;
+ return this.mAccountKey != null ? this.mAccountKey.equals(that.mAccountKey)
+ : that.mAccountKey == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = this.getAccountNumber();
+ result = 31 * result + (this.mAccountKey != null ? this.mAccountKey.hashCode() : 0);
+ return result;
+ }
+
+ /**
+ * Updates a transaction
+ *
+ * @param transaction The transaction to update
+ */
+ public void updateTransaction(GeneralTransaction transaction){
+ // Checks if it has an external address
+ for (GeneralCoinAddress address : this.mExternalKeys.values()) {
+ if(address.updateTransaction(transaction)){
+ return;
+ }
+ }
+
+ for (GeneralCoinAddress address : this.mChangeKeys.values()) {
+ if(address.updateTransaction(transaction)){
+ return;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAddress.java b/app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAddress.java
new file mode 100644
index 0000000..68b8cc9
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAddress.java
@@ -0,0 +1,371 @@
+package cy.agorise.crystalwallet.models;
+
+import org.bitcoinj.core.Address;
+import org.bitcoinj.core.ECKey;
+import org.bitcoinj.core.NetworkParameters;
+import org.bitcoinj.crypto.DeterministicKey;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import cy.agorise.graphenej.Util;
+
+/**
+ * Represents an Address of a General Coin Account
+ */
+public class GeneralCoinAddress {
+ /**
+ * The id on the database
+ */
+ private long mId = -1;
+ /**
+ * The account that this address belongs
+ */
+ private final GeneralCoinAccount mAccount;
+ /**
+ * If this is change or external
+ */
+ private final boolean mIsChange;
+ /**
+ * The index fo this address in the account
+ */
+ private final int mIndex;
+ /**
+ * The ky used to calculate the address
+ */
+ private ECKey mKey;
+ /**
+ * The list of the transactions that used this address as input
+ */
+ private List mTransactionInput = new ArrayList<>();
+ /**
+ * The list of the transactions that used this address as output
+ */
+ private List mTransactionOutput = new ArrayList<>();
+
+ /**
+ * Contrsutcotr used from the database
+ * @param id The id on the database
+ * @param account The account of this address
+ * @param isChange if it is change or external address
+ * @param index the index on the account of this address
+ * @param publicHexKey The public Address String
+ */
+ public GeneralCoinAddress(long id, GeneralCoinAccount account, boolean isChange, int index, String publicHexKey) {
+ this.mId = id;
+ this.mAccount = account;
+ this.mIsChange = isChange;
+ this.mIndex = index;
+ this.mKey = ECKey.fromPublicOnly(Util.hexToBytes(publicHexKey));
+ }
+
+ /**
+ * Basic constructor
+ * @param account The account of this address
+ * @param isChange if it is change or external address
+ * @param index The index on the account of this address
+ * @param key The key to generate the private and the public key of this address
+ */
+ public GeneralCoinAddress(GeneralCoinAccount account, boolean isChange, int index, DeterministicKey key) {
+ this.mId = -1;
+ this.mAccount = account;
+ this.mIsChange = isChange;
+ this.mIndex = index;
+ this.mKey = key;
+ }
+
+ /**
+ * Getter of the database id
+ */
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * Setter of the database id
+ */
+ public void setId(long id) {
+ this.mId = id;
+ }
+ /**
+ * Getter for he account
+ */
+ public GeneralCoinAccount getAccount() {
+ return mAccount;
+ }
+
+ /**
+ * Indicates if this addres is change, if not is external
+ */
+ public boolean isIsChange() {
+ return mIsChange;
+ }
+
+ /**
+ * Getter for the index on the account of this address
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * Getter for the key of this address
+ */
+ public ECKey getKey() {
+ return mKey;
+ }
+
+ /**
+ * Set the key for generate private key, this is used when this address is loaded from the database
+ * and want to be used to send transactions
+ * @param key The key that generates the private and the public key
+ */
+ public void setKey(DeterministicKey key) {
+ this.mKey = key;
+ }
+
+ /**
+ * Get the address as a String
+ * @param param The network param of this address
+ */
+ public String getAddressString(NetworkParameters param) {
+ return mKey.toAddress(param).toString();
+ }
+
+ /**
+ * Returns the bitcoinj Address representing this address
+ * @param param The network parameter of this address
+ */
+ public Address getAddress(NetworkParameters param) {
+ return mKey.toAddress(param);
+ }
+
+ /**
+ * Gets the list of transaction that this address is input
+ */
+ public List getTransactionInput() {
+ return mTransactionInput;
+ }
+
+ /**
+ * Set the transactions that this address is input
+ */
+ public void setTransactionInput(List transactionInput) {
+ this.mTransactionInput = transactionInput;
+ }
+
+ /**
+ * Find if this address is input of a transaction
+ * @param inputToFind The GTxIO to find
+ * @param param The network parameter of this address
+ * @return if this address belongs to the transaction
+ */
+ public boolean hasTransactionInput(GTxIO inputToFind, NetworkParameters param) {
+ for (GTxIO input : mTransactionInput) {
+ if ((input.getTransaction().getTxid().equals(inputToFind.getTransaction().getTxid()))
+ && (input.getAddress().getAddressString(param).equals(inputToFind.getAddress()
+ .getAddressString(param)))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the list of transaction that this address is output
+ */
+ public List getTransactionOutput() {
+ return mTransactionOutput;
+ }
+
+ /**
+ * Find if this address is output of a transaction
+ * @param outputToFind The GTxIO to find
+ * @param param the network parameter of this address
+ * @return if this address belongs to the transaction
+ */
+ public boolean hasTransactionOutput(GTxIO outputToFind, NetworkParameters param) {
+ for (GTxIO output : mTransactionOutput) {
+ if ((output.getTransaction().getTxid().equals(outputToFind.getTransaction().getTxid()))
+ && (output.getAddress().getAddressString(param).equals(outputToFind.getAddress()
+ .getAddressString(param)))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the list of transaction that this address is output
+ */
+ public void setTransactionOutput(List outputTransaction) {
+ this.mTransactionOutput = outputTransaction;
+ }
+
+ /**
+ * Get the amount of uncofirmed balance
+ */
+ public long getUnconfirmedBalance() {
+ long answer = 0;
+ for (GTxIO input : mTransactionInput) {
+ if (input.getTransaction().getConfirm() < mAccount.getCryptoNet().getConfirmationsNeeded()) {
+ answer += input.getAmount();
+ }
+ }
+
+ for (GTxIO output : mTransactionOutput) {
+ if (output.getTransaction().getConfirm() < mAccount.getCryptoNet().getConfirmationsNeeded()) {
+ answer -= output.getAmount();
+ }
+ }
+
+ return answer;
+ }
+
+ /**
+ * Get the amount of confirmed balance
+ */
+ public long getConfirmedBalance() {
+ long answer = 0;
+ for (GTxIO input : mTransactionInput) {
+ if (input.getTransaction().getConfirm() >= mAccount.getCryptoNet().getConfirmationsNeeded()) {
+ answer += input.getAmount();
+ }
+ }
+
+ for (GTxIO output : mTransactionOutput) {
+ if (output.getTransaction().getConfirm() >= mAccount.getCryptoNet().getConfirmationsNeeded()) {
+ answer -= output.getAmount();
+ }
+ }
+
+ return answer;
+ }
+
+ /**
+ * Get the date of the last transaction or null if there is no transaction
+ */
+ public Date getLastDate() {
+ Date lastDate = null;
+ for (GTxIO input : mTransactionInput) {
+ if (lastDate == null || lastDate.before(input.getTransaction().getDate())) {
+ lastDate = input.getTransaction().getDate();
+ }
+ }
+ for (GTxIO output : mTransactionOutput) {
+ if (lastDate == null || lastDate.before(output.getTransaction().getDate())) {
+ lastDate = output.getTransaction().getDate();
+ }
+ }
+ return lastDate;
+ }
+
+ /**
+ * Get the amount of the less cofnirmed transaction, this is used to set how confirmations are
+ * left
+ */
+ public int getLessConfirmed(){
+ int lessConfirm = -1;
+ for (GTxIO input : mTransactionInput) {
+ if (lessConfirm == -1 || input.getTransaction().getConfirm() < lessConfirm) {
+ lessConfirm = input.getTransaction().getConfirm();
+ }
+ }
+
+ for (GTxIO output : mTransactionOutput) {
+ if (lessConfirm == -1 || output.getTransaction().getConfirm() < lessConfirm) {
+ lessConfirm = output.getTransaction().getConfirm();
+ }
+ }
+ return lessConfirm;
+ }
+
+ /**
+ * Gets the unspend transactions input
+ * @return The list with the unspend transasctions
+ */
+ public List getUTXos(){
+ List utxo = new ArrayList<>();
+ for(GTxIO gitx : mTransactionInput){
+ boolean find = false;
+ for(GTxIO gotx : mTransactionOutput){
+ if(gitx.getTransaction().getTxid().equals(gotx.getOriginalTxid())){
+ find = true;
+ break;
+ }
+ }
+ if(!find){
+ utxo.add(gitx);
+ }
+ }
+ return utxo;
+ }
+
+ /**
+ * Fire the onBalanceChange event
+ */
+ public void BalanceChange() {
+ this.getAccount().balanceChange();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ GeneralCoinAddress that = (GeneralCoinAddress) o;
+
+ return mIsChange == that.mIsChange && mIndex == that.mIndex && mId == -1
+ && (mAccount != null ? mAccount.equals(that.mAccount) : that.mAccount == null
+ && (mKey != null ? mKey.equals(that.mKey) : that.mKey == null
+ && (mTransactionInput != null ? mTransactionInput.equals(that.mTransactionInput)
+ : that.mTransactionInput == null && (mTransactionOutput != null
+ ? mTransactionOutput.equals(that.mTransactionOutput)
+ : that.mTransactionOutput == null))));
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) mId;
+ result = 31 * result + (mAccount != null ? mAccount.hashCode() : 0);
+ result = 31 * result + (mIsChange ? 1 : 0);
+ result = 31 * result + mIndex;
+ result = 31 * result + (mKey != null ? mKey.hashCode() : 0);
+ result = 31 * result + (mTransactionInput != null ? mTransactionInput.hashCode() : 0);
+ result = 31 * result + (mTransactionOutput != null ? mTransactionOutput.hashCode() : 0);
+ return result;
+ }
+
+ /**
+ * Update the transactions of this Address
+ * @param transaction The transaction to update
+ * @return true if this address has the transaction false otherwise
+ */
+ public boolean updateTransaction(GeneralTransaction transaction){
+ for(GTxIO gitx : mTransactionInput){
+ if(gitx.getTransaction().equals(transaction)){
+ gitx.getTransaction().setConfirm(transaction.getConfirm());
+ gitx.getTransaction().setBlock(transaction.getBlock());
+ gitx.getTransaction().setBlockHeight(transaction.getBlockHeight());
+ gitx.getTransaction().setDate(transaction.getDate());
+ gitx.getTransaction().setMemo(transaction.getMemo());
+ return true;
+ }
+ }
+
+ for(GTxIO gotx : mTransactionOutput){
+ if(gotx.getTransaction().equals(transaction)){
+ gotx.getTransaction().setConfirm(transaction.getConfirm());
+ gotx.getTransaction().setBlock(transaction.getBlock());
+ gotx.getTransaction().setBlockHeight(transaction.getBlockHeight());
+ gotx.getTransaction().setDate(transaction.getDate());
+ gotx.getTransaction().setMemo(transaction.getMemo());
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
diff --git a/app/src/main/java/cy/agorise/crystalwallet/models/GeneralSetting.java b/app/src/main/java/cy/agorise/crystalwallet/models/GeneralSetting.java
index 33e4b8e..b452337 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/models/GeneralSetting.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/models/GeneralSetting.java
@@ -23,6 +23,8 @@ public class GeneralSetting {
public final static String SETTING_PATTERN = "PATTERN";
public final static String SETTING_NAME_RECEIVED_FUNDS_SOUND_PATH = "RECEIVED_FUNDS_SOUND_PATH";
public final static String SETTING_LAST_LICENSE_READ = "LAST_LICENSE_READ";
+ public final static String SETTING_YUBIKEY_OATH_TOTP_NAME = "YUBIKEY_OATH_TOTP_NAME";
+ public final static String SETTING_YUBIKEY_OATH_TOTP_PASSWORD = "YUBIKEY_OATH_TOTP_PASSWORD";
/**
* The id on the database
diff --git a/app/src/main/java/cy/agorise/crystalwallet/models/GeneralTransaction.java b/app/src/main/java/cy/agorise/crystalwallet/models/GeneralTransaction.java
new file mode 100644
index 0000000..f5f2064
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/models/GeneralTransaction.java
@@ -0,0 +1,236 @@
+package cy.agorise.crystalwallet.models;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import cy.agorise.crystalwallet.enums.CryptoCoin;
+
+/**
+ * A General Coin Transaction, of Cryptocurrency like bitcoin
+ *
+ * Created by henry on 06/02/2017.
+ */
+
+public class GeneralTransaction {
+ /**
+ * The id on the database
+ */
+ private long mId = -1;
+ /**
+ * The Tx id of this transaciton
+ */
+ private String mTxId;
+ /**
+ * the type of crypto coin fo this transaction
+ */
+ private CryptoCoin mType;
+ /**
+ * If this is confirmed, the block where it belongs, 0 means this hasn't be included in any block
+ */
+ private long mBlock;
+ /**
+ * The amount of fee of this transaction
+ */
+ private long mFee;
+ /**
+ * the number of confirmations of this transacion, 0 means it hasn't been included in any block
+ */
+ private int mConfirm;
+ /**
+ * The date of this transaction first broadcast
+ */
+ private Date mDate;
+ /**
+ * The height of this transaction on the block
+ */
+ private int mBlockHeight;
+ /**
+ * The memo of this transaciton
+ */
+ private String mMemo = null;
+ /**
+ * The account that this transaction belong as input or output.
+ */
+ private GeneralCoinAccount mAccount;
+ /**
+ * The inputs of this transactions
+ */
+ private List mTxInputs = new ArrayList();
+ /**
+ * the outputs of this transasctions
+ */
+ private List mTxOutputs = new ArrayList();
+
+ /**
+ * empty constructor
+ */
+ public GeneralTransaction() {
+ }
+
+ /**
+ * Constructor form the database
+ * @param id the id on the database
+ * @param txid the txid of this transaction
+ * @param type The cryptocoin type
+ * @param block The block where this transaction is, 0 means this hasn't be confirmed
+ * @param fee the fee of this transaction
+ * @param confirm the number of confirmations of this transasciton
+ * @param date the date of this transaction
+ * @param blockHeight the height on the block where this transasciton is
+ * @param memo the memo of this transaction
+ * @param account The account to this transaction belongs, as input or output
+ */
+ public GeneralTransaction(long id, String txid, CryptoCoin type, long block, long fee, int confirm, Date date, int blockHeight, String memo, GeneralCoinAccount account) {
+ this.mId = id;
+ this.mTxId = txid;
+ this.mType = type;
+ this.mBlock = block;
+ this.mFee = fee;
+ this.mConfirm = confirm;
+ this.mDate = date;
+ this.mBlockHeight = blockHeight;
+ this.mMemo = memo;
+ this.mAccount = account;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public void setId(long id) {
+ this.mId = id;
+ }
+
+ public String getTxid() { return mTxId; }
+
+ public void setTxid(String txid) { this.mTxId = txid; }
+
+ public CryptoCoin getType() {
+ return mType;
+ }
+
+ public void setType(CryptoCoin type) {
+ this.mType = type;
+ }
+
+ public long getBlock() {
+ return mBlock;
+ }
+
+ public void setBlock(long block) {
+ this.mBlock = block;
+ }
+
+ public long getFee() {
+ return mFee;
+ }
+
+ public void setFee(long fee) {
+ this.mFee = fee;
+ }
+
+ public int getConfirm() {
+ return mConfirm;
+ }
+
+ public void setConfirm(int confirm) {
+ this.mConfirm = confirm;
+ }
+
+ public Date getDate() {
+ return mDate;
+ }
+
+ public void setDate(Date date) {
+ this.mDate = date;
+ }
+
+ public int getBlockHeight() {
+ return mBlockHeight;
+ }
+
+ public void setBlockHeight(int blockHeight) {
+ this.mBlockHeight = blockHeight;
+ }
+
+ public String getMemo() {
+ return mMemo;
+ }
+
+ public void setMemo(String memo) {
+ this.mMemo = memo;
+ }
+
+ public List getTxInputs() {
+ return mTxInputs;
+ }
+
+ public void setTxInputs(List txInputs) {
+ this.mTxInputs = txInputs;
+ }
+
+ public List getTxOutputs() {
+ return mTxOutputs;
+ }
+
+ public void setTxOutputs(List txOutputs) {
+ this.mTxOutputs = txOutputs;
+ }
+
+ public GeneralCoinAccount getAccount() {
+ return mAccount;
+ }
+
+ public void setAccount(GeneralCoinAccount account) {
+ this.mAccount = account;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ GeneralTransaction that = (GeneralTransaction) o;
+
+ if (mTxId != null ? !mTxId.equals(that.mTxId) : that.mTxId != null) return false;
+ return mType == that.mType;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mTxId != null ? mTxId.hashCode() : 0;
+ result = 31 * result + mType.hashCode();
+ return result;
+ }
+
+
+ /**
+ * Returns how this transaction changes the balance of the account
+ * @return The amount of balance this transasciton adds to the total balance of the account
+ */
+ public double getAccountBalanceChange(){
+ double balance = 0;
+ boolean theresAccountInput = false;
+
+ for (GTxIO txInputs : this.getTxInputs()){
+ if (txInputs.isOut() && (txInputs.getAddress() != null)){
+ balance += -txInputs.getAmount();
+ theresAccountInput = true;
+ }
+ }
+
+ for (GTxIO txOutput : this.getTxOutputs()){
+ if (!txOutput.isOut() && (txOutput.getAddress() != null)){
+ balance += txOutput.getAmount();
+ }
+ }
+
+ if (theresAccountInput){
+ balance += -this.getFee();
+ }
+
+ return balance;
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/util/PasswordManager.java b/app/src/main/java/cy/agorise/crystalwallet/util/PasswordManager.java
index 7f52970..2705ff3 100644
--- a/app/src/main/java/cy/agorise/crystalwallet/util/PasswordManager.java
+++ b/app/src/main/java/cy/agorise/crystalwallet/util/PasswordManager.java
@@ -1,5 +1,11 @@
package cy.agorise.crystalwallet.util;
+import android.util.Log;
+
+import java.math.BigInteger;
+
+import cy.agorise.crystalwallet.util.yubikey.TOTP;
+
/**
* Created by Henry Varona on 29/1/2018.
*/
@@ -19,4 +25,35 @@ public class PasswordManager {
public static String encriptPassword(String password){
return password;
}
+
+ public static String totpd(String sharedSecret, long unixtime, byte[] salt){
+ char[] ch = sharedSecret.toCharArray();
+
+ StringBuilder builder = new StringBuilder();
+ for (char c : ch) {
+ String hexCode=String.format("%H", c);
+ builder.append(hexCode);
+ }
+ String secretHex = String.format("%040x", new BigInteger(1, sharedSecret.getBytes()));
+
+ long time = unixtime/30L;
+
+ String steps = Long.toHexString(time).toUpperCase();
+
+
+
+ /*for (int i=0;i<64;i++){
+ steps = Long.toHexString(time+i).toUpperCase();
+ Log.i("TOTPTEST", TOTP.generateTOTP(
+ secretHex,
+ steps,
+ "6", "HmacSHA1", salt));
+ secretHex = "00"+secretHex;
+ }*/
+
+ return TOTP.generateTOTP(
+ secretHex,
+ steps,
+ "6", "HmacSHA1", salt);
+ }
}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/Algorithm.java b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/Algorithm.java
new file mode 100644
index 0000000..9e1cfd8
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/Algorithm.java
@@ -0,0 +1,58 @@
+package cy.agorise.crystalwallet.util.yubikey;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import kotlin.jvm.internal.Intrinsics;
+
+public enum Algorithm {
+ SHA1((byte)1, "SHA1", 64),
+ SHA256((byte)2, "SHA256", 64),
+ SHA512((byte)3, "SHA512", 128);
+
+ protected Byte byteVal;
+ protected String name;
+ protected int blockSize;
+ protected MessageDigest messageDigest;
+
+ Algorithm(Byte byteVal, String name, int blockSize){
+ this.byteVal = byteVal;
+ this.name = name;
+ this.blockSize = blockSize;
+
+ try {
+ this.messageDigest = MessageDigest.getInstance(name);
+ } catch (NoSuchAlgorithmException e){
+ this.messageDigest = null;
+ }
+ }
+
+ public Byte getByteVal(){
+ return this.byteVal;
+ }
+
+ public String getName(){
+ return this.name;
+ }
+
+ public int getBlockSize() {
+ return this.blockSize;
+ }
+
+ public byte[] prepareKey(byte[] key){
+ int keyLength = key.length;
+ byte[] keyPrepared;
+ if ((0 <= keyLength) && (keyLength <= 13)) {
+ keyPrepared = Arrays.copyOf(key, 14);
+ return keyPrepared;
+ }
+ if ((14 <= keyLength) && (keyLength <= this.blockSize)){
+ keyPrepared = key;
+ return keyPrepared;
+ }
+
+ keyPrepared = this.messageDigest.digest(key);
+ return keyPrepared;
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/OathType.java b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/OathType.java
new file mode 100644
index 0000000..265481b
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/OathType.java
@@ -0,0 +1,16 @@
+package cy.agorise.crystalwallet.util.yubikey;
+
+public enum OathType {
+ HOTP((byte)0x10),
+ TOTP((byte)0x20);
+
+ protected Byte byteVal;
+
+ OathType(Byte byteVal){
+ this.byteVal = byteVal;
+ }
+
+ public Byte getByteVal(){
+ return this.byteVal;
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/TOTP.java b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/TOTP.java
new file mode 100644
index 0000000..368f0cd
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/TOTP.java
@@ -0,0 +1,185 @@
+package cy.agorise.crystalwallet.util.yubikey;
+
+import java.lang.reflect.UndeclaredThrowableException;
+import java.security.GeneralSecurityException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import javax.crypto.Mac;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.math.BigInteger;
+import java.util.TimeZone;
+
+/*
+* Source of this class: https://tools.ietf.org/html/rfc6238
+* */
+
+public class TOTP {
+
+ private TOTP() {}
+
+ /**
+ * This method uses the JCE to provide the crypto algorithm.
+ * HMAC computes a Hashed Message Authentication Code with the
+ * crypto hash algorithm as a parameter.
+ *
+ * @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
+ * HmacSHA512)
+ * @param keyBytes: the bytes to use for the HMAC key
+ * @param text: the message or text to be authenticated
+ */
+
+ private static byte[] hmac_sha(String crypto, byte[] keyBytes,
+ byte[] text, byte[] salt){
+ try {
+ Mac hmac;
+ hmac = Mac.getInstance(crypto);
+ SecretKeySpec macKey =
+ new SecretKeySpec(keyBytes, "RAW");
+ hmac.init(macKey);
+ return hmac.doFinal(text);
+ /*PBEKeySpec spec = new PBEKeySpec((new String(text)).toCharArray(), salt, 1000, 128);
+ SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ byte[] hash = skf.generateSecret(spec).getEncoded();
+ return hash;*/
+ } catch (GeneralSecurityException gse) {
+ throw new UndeclaredThrowableException(gse);
+ }
+ }
+
+
+ /**
+ * This method converts a HEX string to Byte[]
+ *
+ * @param hex: the HEX string
+ *
+ * @return: a byte array
+ */
+
+ private static byte[] hexStr2Bytes(String hex){
+ // Adding one byte to get the right conversion
+ // Values starting with "0" can be converted
+ byte[] bArray = new BigInteger("10" + hex,16).toByteArray();
+
+ // Copy all the REAL bytes, not the "first"
+ byte[] ret = new byte[bArray.length - 1];
+ for (int i = 0; i < ret.length; i++)
+ ret[i] = bArray[i+1];
+ return ret;
+ }
+
+ private static final int[] DIGITS_POWER
+ // 0 1 2 3 4 5 6 7 8
+ = {1,10,100,1000,10000,100000,1000000,10000000,100000000 };
+
+ /**
+ * This method generates a TOTP value for the given
+ * set of parameters.
+ *
+ * @param key: the shared secret, HEX encoded
+ * @param time: a value that reflects a time
+ * @param returnDigits: number of digits to return
+ *
+ * @return: a numeric String in base 10 that includes
+ * {@link truncationDigits} digits
+ */
+
+ public static String generateTOTP(String key,
+ String time,
+ String returnDigits,
+ byte[] salt){
+ return generateTOTP(key, time, returnDigits, "PBKDF2WithHmacSHA1", salt);
+ }
+
+
+ /**
+ * This method generates a TOTP value for the given
+ * set of parameters.
+ *
+ * @param key: the shared secret, HEX encoded
+ * @param time: a value that reflects a time
+ * @param returnDigits: number of digits to return
+ *
+ * @return: a numeric String in base 10 that includes
+ * {@link truncationDigits} digits
+ */
+
+ public static String generateTOTP256(String key,
+ String time,
+ String returnDigits,
+ byte[] salt){
+ return generateTOTP(key, time, returnDigits, "HmacSHA256", salt);
+ }
+
+ /**
+ * This method generates a TOTP value for the given
+ * set of parameters.
+ *
+ * @param key: the shared secret, HEX encoded
+ * @param time: a value that reflects a time
+ * @param returnDigits: number of digits to return
+ *
+ * @return: a numeric String in base 10 that includes
+ * {@link truncationDigits} digits
+ */
+
+ public static String generateTOTP512(String key,
+ String time,
+ String returnDigits,
+ byte[] salt){
+ return generateTOTP(key, time, returnDigits, "HmacSHA512", salt);
+ }
+
+
+ /**
+ * This method generates a TOTP value for the given
+ * set of parameters.
+ *
+ * @param key: the shared secret, HEX encoded
+ * @param time: a value that reflects a time
+ * @param returnDigits: number of digits to return
+ * @param crypto: the crypto function to use
+ *
+ * @return: a numeric String in base 10 that includes
+ * {@link truncationDigits} digits
+ */
+
+ public static String generateTOTP(String key,
+ String time,
+ String returnDigits,
+ String crypto,
+ byte[] salt){
+ int codeDigits = Integer.decode(returnDigits).intValue();
+ String result = null;
+
+ // Using the counter
+ // First 8 bytes are for the movingFactor
+ // Compliant with base RFC 4226 (HOTP)
+ while (time.length() < 16 )
+ time = "0" + time;
+
+ // Get the HEX in a Byte[]
+ byte[] msg = hexStr2Bytes(time);
+ byte[] k = hexStr2Bytes(key);
+ byte[] hash = hmac_sha(crypto, k, msg, salt);
+
+ // put selected bytes into result int
+ int offset = hash[hash.length - 1] & 0xf;
+
+ int binary =
+ ((hash[offset] & 0x7f) << 24) |
+ ((hash[offset + 1] & 0xff) << 16) |
+ ((hash[offset + 2] & 0xff) << 8) |
+ (hash[offset + 3] & 0xff);
+
+ int otp = binary % DIGITS_POWER[codeDigits];
+
+ result = Integer.toString(otp);
+ while (result.length() < codeDigits) {
+ result = "0" + result;
+ }
+ return result;
+ }
+}
diff --git a/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/YkOathApi.kt b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/YkOathApi.kt
new file mode 100644
index 0000000..4919240
--- /dev/null
+++ b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/YkOathApi.kt
@@ -0,0 +1,268 @@
+package cy.agorise.crystalwallet.util.yubikey
+
+import android.nfc.tech.IsoDep
+import android.util.Base64
+//import com.yubico.yubioath.exc.AppletMissingException
+//import com.yubico.yubioath.exc.AppletSelectException
+//import com.yubico.yubioath.exc.StorageFullException
+//import com.yubico.yubioath.transport.ApduError
+//import com.yubico.yubioath.transport.Backend
+import java.io.Closeable
+import java.io.IOException
+import java.nio.ByteBuffer
+import java.security.MessageDigest
+
+
+class YkOathApi @Throws(IOException::class/*, AppletSelectException::class*/)
+constructor(private var tag: IsoDep) : Closeable {
+ val deviceSalt: ByteArray
+ val deviceInfo: DeviceInfo
+ private var challenge = byteArrayOf()
+
+ init {
+ //try {
+ val resp = send(0xa4.toByte(), p1 = 0x04) { put(AID) }
+ val version = Version.parse(resp.parseTlv(VERSION_TAG))
+ deviceSalt = resp.parseTlv(NAME_TAG)
+ val id = getDeviceId(deviceSalt)
+
+ if (resp.hasRemaining()) {
+ challenge = resp.parseTlv(CHALLENGE_TAG)
+ }
+
+ deviceInfo = DeviceInfo(id, false, version, challenge.isNotEmpty())
+ /*} catch (e: ApduError) {
+ throw AppletMissingException()
+ }*/
+ }
+
+ /* fun isLocked(): Boolean = challenge.isNotEmpty()
+
+ fun reselect() {
+ send(0xa4.toByte(), p1 = 0x04) { put(AID) }.apply {
+ parseTlv(VERSION_TAG)
+ parseTlv(NAME_TAG)
+ challenge = if (hasRemaining()) {
+ parseTlv(CHALLENGE_TAG)
+ } else byteArrayOf()
+ }
+ }
+
+ fun unlock(signer: ChallengeSigner): Boolean {
+ val response = signer.sign(challenge)
+ val myChallenge = ByteArray(8)
+ val random = SecureRandom()
+ random.nextBytes(myChallenge)
+ val myResponse = signer.sign(myChallenge)
+
+ return try {
+ val resp = send(VALIDATE_INS) {
+ tlv(RESPONSE_TAG, response)
+ tlv(CHALLENGE_TAG, myChallenge)
+ }
+ Arrays.equals(myResponse, resp.parseTlv(RESPONSE_TAG))
+ } catch (e: ApduError) {
+ false
+ }
+ }
+
+ fun setLockCode(secret: ByteArray) {
+ val challenge = ByteArray(8)
+ val random = SecureRandom()
+ random.nextBytes(challenge)
+ val response = Mac.getInstance("HmacSHA1").apply {
+ init(SecretKeySpec(secret, algorithm))
+ }.doFinal(challenge)
+
+ send(SET_CODE_INS) {
+ tlv(KEY_TAG, byteArrayOf(OathType.TOTP.byteVal or Algorithm.SHA1.byteVal) + secret)
+ tlv(CHALLENGE_TAG, challenge)
+ tlv(RESPONSE_TAG, response)
+ }
+ deviceInfo.hasPassword = true
+ }
+
+ fun unsetLockCode() {
+ send(SET_CODE_INS) { tlv(KEY_TAG) }
+ deviceInfo.hasPassword = false
+ }*/
+
+ fun listCredentials(): List {
+ val resp = send(LIST_INS)
+
+ return mutableListOf().apply {
+ while (resp.hasRemaining()) {
+ val nameBytes = resp.parseTlv(NAME_LIST_TAG)
+ add(String(nameBytes, 1, nameBytes.size - 1)) //First byte is algorithm
+ }
+ }
+ }
+
+ @Throws(IOException::class)
+ fun putCode(name: String, key: ByteArray, type: OathType, algorithm: Algorithm, digits: Byte, imf: Int, touch: Boolean) {
+ //send(tag, 0xa4.toByte(), p1 = 0x04) { put(AID) }
+ //if (touch && deviceInfo.version.major < 4) {
+ // throw IllegalArgumentException("Require touch requires YubiKey 4")
+ //}
+ //try {
+ send(PUT_INS) {
+ tlv(NAME_TAG, name.toByteArray())
+ tlv(KEY_TAG, byteArrayOf(type.byteVal or algorithm.byteVal, digits) + algorithm.prepareKey(key))
+ if (touch) put(PROPERTY_TAG).put(REQUIRE_TOUCH_PROP)
+ if (type == OathType.HOTP && imf > 0) put(IMF_TAG).put(4).putInt(imf)
+ }
+ //} catch (e: ApduError) {
+ // throw if (e.status == APDU_FILE_FULL) StorageFullException("No more room for OATH credentials!") else e
+ //}
+ }
+
+ /*fun deleteCode(name: String) {
+ send(DELETE_INS) { tlv(NAME_TAG, name.toByteArray()) }
+ }*/
+
+ fun calculate(name: String, challenge: ByteArray, truncate: Boolean = true): ByteArray {
+ val resp = send(CALCULATE_INS, p2 = if (truncate) 1 else 0) {
+ tlv(NAME_TAG, name.toByteArray())
+ tlv(CHALLENGE_TAG, challenge)
+ }
+ return resp.parseTlv(resp.slice().get())
+ }
+
+ /*fun calculateAll(challenge: ByteArray): List {
+ val resp = send(CALCULATE_ALL_INS, p2 = 1) {
+ tlv(CHALLENGE_TAG, challenge)
+ }
+
+ return mutableListOf().apply {
+ while (resp.hasRemaining()) {
+ val name = String(resp.parseTlv(NAME_TAG))
+ val respType = resp.slice().get() // Peek
+ val hashBytes = resp.parseTlv(respType)
+ val oathType = if (respType == NO_RESPONSE_TAG) OathType.HOTP else OathType.TOTP
+ val touch = respType == TOUCH_TAG
+
+ add(ResponseData(name, oathType, touch, hashBytes))
+ }
+ }
+ }*/
+ @Throws(IOException::class)
+ private fun send(ins: Byte, p1: Byte = 0, p2: Byte = 0, data: ByteBuffer.() -> Unit = {}): ByteBuffer {
+ val apdu = ByteBuffer.allocate(256).put(0).put(ins).put(p1).put(p2).put(0).apply(data).let {
+ it.put(4, (it.position() - 5).toByte()).array().copyOfRange(0, it.position())
+ }
+
+ return ByteBuffer.allocate(4096).apply {
+ var resp = splitApduResponse(tag.transceive(apdu))
+ while (resp.status != APDU_OK) {
+ if ((resp.status shr 8).toByte() == APDU_DATA_REMAINING_SW1) {
+ put(resp.data)
+ resp = splitApduResponse(tag.transceive(byteArrayOf(0, SEND_REMAINING_INS, 0, 0)))
+ } else {
+ throw IOException(""+resp.status)
+ }
+ }
+ put(resp.data).limit(position()).rewind()
+ }
+ }
+
+ override fun close() {
+ /*backend.close()
+ backend = object : Backend {
+ override val persistent: Boolean = false
+ override fun sendApdu(apdu: ByteArray): ByteArray = throw IOException("SENDING APDU ON CLOSED BACKEND!")
+ override fun close() = throw IOException("Backend already closed!")
+ }*/
+ }
+
+ data class Version(val major: Int, val minor: Int, val micro: Int) {
+ companion object {
+ fun parse(data: ByteArray): Version = Version(data[0].toInt(), data[1].toInt(), data[2].toInt())
+ }
+
+ override fun toString(): String = "%d.%d.%d".format(major, minor, micro)
+
+ fun compare(major: Int, minor: Int, micro: Int): Int {
+ return if (major > this.major || (major == this.major && (minor > this.minor || minor == this.minor && micro > this.micro))) {
+ -1
+ } else if (major == this.major && minor == this.minor && micro == this.micro) {
+ 0
+ } else {
+ 1
+ }
+ }
+
+ fun compare(version: Version): Int = compare(version.major, version.minor, version.micro)
+ }
+
+ class DeviceInfo(val id: String, val persistent: Boolean, val version: Version, initialHasPassword: Boolean) {
+ var hasPassword = initialHasPassword
+ internal set
+ }
+
+ class ResponseData(val key: String, val oathType: OathType, val touch: Boolean, val data: ByteArray)
+
+ private infix fun Byte.or(b: Byte): Byte = (toInt() or b.toInt()).toByte()
+
+ companion object {
+ const private val APDU_OK = 0x9000
+ const private val APDU_FILE_FULL = 0x6a84
+ const private val APDU_DATA_REMAINING_SW1 = 0x61.toByte()
+
+ const private val NAME_TAG: Byte = 0x71
+ const private val NAME_LIST_TAG: Byte = 0x72
+ const private val KEY_TAG: Byte = 0x73
+ const private val CHALLENGE_TAG: Byte = 0x74
+ const private val RESPONSE_TAG: Byte = 0x75
+ const private val T_RESPONSE_TAG: Byte = 0x76
+ const private val NO_RESPONSE_TAG: Byte = 0x77
+ const private val PROPERTY_TAG: Byte = 0x78
+ const private val VERSION_TAG: Byte = 0x79
+ const private val IMF_TAG: Byte = 0x7a
+ const private val TOUCH_TAG: Byte = 0x7c
+
+ const private val ALWAYS_INCREASING_PROP: Byte = 0x01
+ const private val REQUIRE_TOUCH_PROP: Byte = 0x02
+
+ const private val PUT_INS: Byte = 0x01
+ const private val DELETE_INS: Byte = 0x02
+ const private val SET_CODE_INS: Byte = 0x03
+ const private val RESET_INS: Byte = 0x04
+
+ const private val LIST_INS = 0xa1.toByte()
+ const private val CALCULATE_INS = 0xa2.toByte()
+ const private val VALIDATE_INS = 0xa3.toByte()
+ const private val CALCULATE_ALL_INS = 0xa4.toByte()
+ const private val SEND_REMAINING_INS = 0xa5.toByte()
+
+ private val AID = byteArrayOf(0xa0.toByte(), 0x00, 0x00, 0x05, 0x27, 0x21, 0x01, 0x01)
+
+ private fun getDeviceId(id: ByteArray): String {
+ val digest = MessageDigest.getInstance("SHA256").apply {
+ update(id)
+ }.digest()
+
+ return Base64.encodeToString(digest.sliceArray(0 until 16), Base64.NO_PADDING or Base64.NO_WRAP)
+ }
+
+ @Throws(IOException::class)
+ private fun ByteBuffer.parseTlv(tag: Byte): ByteArray {
+ val readTag = get()
+ if (readTag != tag) {
+ throw IOException("Required tag: %02x, got %02x".format(tag, readTag))
+ }
+ return ByteArray(0xff and get().toInt()).apply { get(this) }
+ }
+
+ private fun ByteBuffer.tlv(tag: Byte, data: ByteArray = byteArrayOf()): ByteBuffer {
+ return put(tag).put(data.size.toByte()).put(data)
+ }
+
+ private data class Response(val data: ByteArray, val status: Int)
+
+ private fun splitApduResponse(resp: ByteArray): Response {
+ return Response(
+ resp.copyOfRange(0, resp.size - 2),
+ ((0xff and resp[resp.size - 2].toInt()) shl 8) or (0xff and resp[resp.size - 1].toInt()))
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_pocket_request.xml b/app/src/main/res/layout/activity_pocket_request.xml
new file mode 100644
index 0000000..4c10557
--- /dev/null
+++ b/app/src/main/res/layout/activity_pocket_request.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_security_settings.xml b/app/src/main/res/layout/fragment_security_settings.xml
index d3ae66f..c24b050 100644
--- a/app/src/main/res/layout/fragment_security_settings.xml
+++ b/app/src/main/res/layout/fragment_security_settings.xml
@@ -68,7 +68,7 @@
android:layout_width="match_parent"
android:layout_height="140dp"
android:background="@color/lightGray"
- android:visibility="gone"
+ android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
@@ -102,15 +102,14 @@
android:text="@string/user_name_password_placeholder"
app:layout_constraintTop_toBottomOf="@id/tvPocketSecurity"
app:layout_constraintStart_toStartOf="@id/tvPocketSecurity"/>
-
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/tech.xml b/app/src/main/res/xml/tech.xml
new file mode 100644
index 0000000..c0e872f
--- /dev/null
+++ b/app/src/main/res/xml/tech.xml
@@ -0,0 +1,13 @@
+
+
+
+ android.nfc.tech.MifareUltralight
+ android.nfc.tech.Ndef
+ android.nfc.tech.NfcA
+
+
+ android.nfc.tech.MifareClassic
+ android.nfc.tech.Ndef
+ android.nfc.tech.NfcA
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index c6ac57d..b63175e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,6 +6,7 @@ buildscript {
jcenter()
google()
}
+ ext.kotlin_version = '1.2.51'
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'