From f40d996282a2e3fb346b2f9eef10e0b2bbf8db71 Mon Sep 17 00:00:00 2001 From: hvarona Date: Thu, 2 Aug 2018 23:02:23 -0400 Subject: [PATCH] Implementation of model for alt-coin (not graphene cryptos) Insight api base implementation --- app/build.gradle | 5 + .../insightapi/AccountActivityWatcher.java | 160 +++++++ .../insightapi/BroadcastTransaction.java | 83 ++++ .../insightapi/GetEstimateFee.java | 74 ++++ .../insightapi/GetTransactionByAddress.java | 207 +++++++++ .../insightapi/GetTransactionData.java | 196 +++++++++ .../insightapi/InsightApiConstants.java | 116 ++++++ .../insightapi/InsightApiService.java | 52 +++ .../InsightApiServiceGenerator.java | 130 ++++++ .../insightapi/models/AddressTxi.java | 24 ++ .../insightapi/models/ScriptPubKey.java | 24 ++ .../insightapi/models/ScriptSig.java | 16 + .../apigenerator/insightapi/models/Txi.java | 69 +++ .../apigenerator/insightapi/models/Vin.java | 44 ++ .../apigenerator/insightapi/models/Vout.java | 32 ++ .../agorise/crystalwallet/models/GTxIO.java | 166 ++++++++ .../models/GeneralCoinAccount.java | 392 ++++++++++++++++++ .../models/GeneralCoinAddress.java | 371 +++++++++++++++++ .../models/GeneralTransaction.java | 236 +++++++++++ 19 files changed, 2397 insertions(+) create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/AccountActivityWatcher.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/BroadcastTransaction.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetEstimateFee.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetTransactionByAddress.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetTransactionData.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiConstants.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiService.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/InsightApiServiceGenerator.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/AddressTxi.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/ScriptPubKey.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/ScriptSig.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Txi.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Vin.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/models/Vout.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/models/GTxIO.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAccount.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAddress.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/models/GeneralTransaction.java diff --git a/app/build.gradle b/app/build.gradle index 14325b1..5cd5378 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,4 +83,9 @@ dependencies { 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/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/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/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; + } +}