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'