diff --git a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/InsightApiGenerator.java b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/InsightApiGenerator.java new file mode 100644 index 0000000..54fe88e --- /dev/null +++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/InsightApiGenerator.java @@ -0,0 +1,56 @@ +package cy.agorise.crystalwallet.apigenerator; + +import android.content.Context; + +import java.util.HashMap; + +import cy.agorise.crystalwallet.apigenerator.insightapi.BroadcastTransaction; +import cy.agorise.crystalwallet.apigenerator.insightapi.GetEstimateFee; +import cy.agorise.crystalwallet.apigenerator.insightapi.GetTransactionByAddress; +import cy.agorise.crystalwallet.apigenerator.insightapi.GetTransactionData; +import cy.agorise.crystalwallet.enums.CryptoNet; +import cy.agorise.crystalwallet.network.CryptoNetManager; + +public class InsightApiGenerator { + + private static HashMap broadcaster = new HashMap(); + private static HashMap transactionGetters = new HashMap(); + private static HashMap transacitonFollowers = new HashMap(); + + public static void getTransactionFromAddress(CryptoNet cryptoNet, String address, + ApiRequest request, Context context, + boolean subscribe){ + if(!transactionGetters.containsKey(cryptoNet)){ + //TODO change this line + transactionGetters.put(cryptoNet,new GetTransactionByAddress(null,CryptoNetManager.getURL(cryptoNet),context)); + } + + } + + public static void followTransaction(CryptoNet cryptoNet, String txid, Context context){ + + } + + public static void broadcastTransaction(CryptoNet cryptoNet, String rawtx, ApiRequest request){ + if(!broadcaster.containsKey(cryptoNet)){ + //TODO change to multiple broadcast + broadcaster.put(cryptoNet,new BroadcastTransaction(rawtx,null, + CryptoNetManager.getURL(cryptoNet),null)); + broadcaster.get(cryptoNet).start(); + } + } + + public static void getEstimateFee(CryptoNet cryptoNet, final ApiRequest request){ + GetEstimateFee.getEstimateFee(CryptoNetManager.getURL(cryptoNet), new GetEstimateFee.estimateFeeListener() { + @Override + public void estimateFee(long value) { + request.listener.success(value,request.getId()); + } + + @Override + public void fail() { + request.listener.fail(request.getId()); + } + }); + } +} 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 index c175238..520594c 100644 --- a/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetEstimateFee.java +++ b/app/src/main/java/cy/agorise/crystalwallet/apigenerator/insightapi/GetEstimateFee.java @@ -22,29 +22,36 @@ public abstract class GetEstimateFee { /** * The funciton to get the rate for the transaction be included in the next 2 blocks - * @param coin The coin to get the rate + * @param serverUrl The url of the insight server + * @param listener the listener to this answer */ - public static void getEstimateFee(final CryptoCoin coin, String serverUrl, final estimateFeeListener listener) { - InsightApiServiceGenerator serviceGenerator = new InsightApiServiceGenerator(serverUrl); - InsightApiService service = serviceGenerator.getService(InsightApiService.class); - Call call = service.estimateFee(InsightApiConstants.getPath(coin)); - final JsonObject answer = new JsonObject(); - call.enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - listener.estimateFee((long) (answer.get("answer").getAsDouble())); + public static void getEstimateFee(String serverUrl, final estimateFeeListener listener) { + try { + InsightApiServiceGenerator serviceGenerator = new InsightApiServiceGenerator(serverUrl); + InsightApiService service = serviceGenerator.getService(InsightApiService.class); + Call call = service.estimateFee(serverUrl); + final JsonObject answer = new JsonObject(); + call.enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + listener.estimateFee((long) (answer.get("answer").getAsDouble())); - } + } - @Override - public void onFailure(Call call, Throwable t) { - listener.estimateFee(-1); - } - }); + @Override + public void onFailure(Call call, Throwable t) { + listener.fail(); + listener.estimateFee(-1); + } + }); + }catch(Exception e){ + listener.fail(); + } } public static interface estimateFeeListener{ public void estimateFee(long value); + public void fail(); } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/crystalwallet/manager/GeneralAccountManager.java b/app/src/main/java/cy/agorise/crystalwallet/manager/GeneralAccountManager.java new file mode 100644 index 0000000..1a4c6fa --- /dev/null +++ b/app/src/main/java/cy/agorise/crystalwallet/manager/GeneralAccountManager.java @@ -0,0 +1,145 @@ +package cy.agorise.crystalwallet.manager; + +import android.content.Context; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutPoint; +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.crypto.HDKeyDerivation; +import org.bitcoinj.script.Script; + +import java.util.ArrayList; +import java.util.List; + +import cy.agorise.crystalwallet.apigenerator.ApiRequest; +import cy.agorise.crystalwallet.apigenerator.ApiRequestListener; +import cy.agorise.crystalwallet.apigenerator.InsightApiGenerator; +import cy.agorise.crystalwallet.apigenerator.insightapi.BroadcastTransaction; +import cy.agorise.crystalwallet.models.CryptoNetAccount; +import cy.agorise.crystalwallet.models.GTxIO; +import cy.agorise.crystalwallet.models.GeneralCoinAddress; +import cy.agorise.crystalwallet.models.GeneralTransaction; +import cy.agorise.crystalwallet.requestmanagers.CryptoNetInfoRequest; +import cy.agorise.crystalwallet.requestmanagers.CryptoNetInfoRequestsListener; +import cy.agorise.crystalwallet.requestmanagers.GeneralAccountSendRequest; +import cy.agorise.graphenej.Util; + +public class GeneralAccountManager implements CryptoAccountManager, CryptoNetInfoRequestsListener { + + @Override + public void createAccountFromSeed(CryptoNetAccount account, ManagerRequest request, Context context) { + + } + + @Override + public void importAccountFromSeed(CryptoNetAccount account, Context context) { + + } + + @Override + public void loadAccountFromDB(CryptoNetAccount account, Context context) { + + } + + @Override + public void onNewRequest(CryptoNetInfoRequest request) { + + } + + public void send(final GeneralAccountSendRequest request){ + //TODO check server connection + //TODO validate to address + + InsightApiGenerator.getEstimateFee(request.getAccount().getCryptoNet(),new ApiRequest(1, new ApiRequestListener() { + @Override + public void success(Object answer, int idPetition) { + Transaction tx = new Transaction(request.getAccount().getNetworkParam()); + long currentAmount = 0; + long fee = -1; + long feeRate = (Long) answer; + fee = 226 * feeRate; + + List addresses = request.getAccount().getAddresses(); + List utxos = new ArrayList(); + for(GeneralCoinAddress address : addresses){ + List addrUtxos = address.getUTXos(); + for(GTxIO addrUtxo : addrUtxos){ + utxos.add(addrUtxo); + currentAmount += addrUtxo.getAmount(); + if(currentAmount >= request.getAmount()+ fee){ + break; + } + } + if(currentAmount >= request.getAmount() + fee){ + break; + } + } + + + if(currentAmount< request.getAmount() + fee){ + request.setStatus(GeneralAccountSendRequest.StatusCode.NO_BALANCE); + return; + } + + //String to an address + Address toAddr = Address.fromBase58(request.getAccount().getNetworkParam(), request.getToAccount()); + tx.addOutput(Coin.valueOf(request.getAmount()), toAddr); + + if(request.getMemo()!= null && !request.getMemo().isEmpty()){ + String memo = request.getMemo(); + if(request.getMemo().length()>40){ + memo = memo.substring(0,40); + } + byte[]scriptByte = new byte[memo.length()+2]; + scriptByte[0] = 0x6a; + scriptByte[1] = (byte) memo.length(); + System.arraycopy(memo.getBytes(),0,scriptByte,2,memo.length()); + Script memoScript = new Script(scriptByte); + tx.addOutput(Coin.valueOf(0),memoScript); + } + + //Change address + long remain = currentAmount - request.getAmount() - fee; + if( remain > 0 ) { + Address changeAddr = Address.fromBase58(request.getAccount().getNetworkParam(), request.getAccount().getNextChangeAddress()); + tx.addOutput(Coin.valueOf(remain), changeAddr); + } + + for(GTxIO utxo: utxos) { + Sha256Hash txHash = Sha256Hash.wrap(utxo.getTransaction().getTxid()); + Script script = new Script(Util.hexToBytes(utxo.getScriptHex())); + TransactionOutPoint outPoint = new TransactionOutPoint(request.getAccount().getNetworkParam(), utxo.getIndex(), txHash); + if(utxo.getAddress().getKey().isPubKeyOnly()){ + if(utxo.getAddress().isIsChange()){ + utxo.getAddress().setKey(HDKeyDerivation.deriveChildKey(request.getAccount().getChangeKey(), new ChildNumber(utxo.getAddress().getIndex(), false))); + }else{ + utxo.getAddress().setKey(HDKeyDerivation.deriveChildKey(request.getAccount().getExternalKey(), new ChildNumber(utxo.getAddress().getIndex(), false))); + } + } + tx.addSignedInput(outPoint, script, utxo.getAddress().getKey(), Transaction.SigHash.ALL, true); + } + + InsightApiGenerator.broadcastTransaction(request.getAccount().getCryptoNet(),Util.bytesToHex(tx.bitcoinSerialize()),new ApiRequest(1, new ApiRequestListener() { + @Override + public void success(Object answer, int idPetition) { + request.setStatus(GeneralAccountSendRequest.StatusCode.SUCCEEDED); + } + + @Override + public void fail(int idPetition) { + request.setStatus(GeneralAccountSendRequest.StatusCode.PETITION_FAILED); + } + })); + } + + @Override + public void fail(int idPetition) { + request.setStatus(GeneralAccountSendRequest.StatusCode.NO_FEE); + + } + })); + } +} diff --git a/app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAccount.java b/app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAccount.java index 68f44f2..af756b7 100644 --- a/app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAccount.java +++ b/app/src/main/java/cy/agorise/crystalwallet/models/GeneralCoinAccount.java @@ -323,6 +323,14 @@ public abstract class GeneralCoinAccount extends CryptoNetAccount { public abstract List getBalance(); + public DeterministicKey getChangeKey() { + return mChangeKey; + } + + public DeterministicKey getExternalKey() { + return mExternalKey; + } + /** * Compare the transaction, to order it for the list of transaction */ diff --git a/app/src/main/java/cy/agorise/crystalwallet/requestmanagers/GeneralAccountSendRequest.java b/app/src/main/java/cy/agorise/crystalwallet/requestmanagers/GeneralAccountSendRequest.java new file mode 100644 index 0000000..2922680 --- /dev/null +++ b/app/src/main/java/cy/agorise/crystalwallet/requestmanagers/GeneralAccountSendRequest.java @@ -0,0 +1,85 @@ +package cy.agorise.crystalwallet.requestmanagers; + +import android.content.Context; + +import cy.agorise.crystalwallet.enums.CryptoCoin; +import cy.agorise.crystalwallet.models.GeneralCoinAccount; + +public class GeneralAccountSendRequest extends CryptoNetInfoRequest { + /** + * The status code of this request + */ + public enum StatusCode{ + NOT_STARTED, + SUCCEEDED, + NO_INTERNET, + NO_SERVER_CONNECTION, + BAD_TO_ADDRESS, + NO_FEE, + NO_BALANCE, + PETITION_FAILED + } + + // The app context + private Context mContext; + //The soruce Account + private GeneralCoinAccount mAccount; + // The destination account address + private String mToAccount; + // The amount of the transaction + private long mAmount; + // The memo, can be null + private String mMemo; + // The state of this request + private StatusCode status = StatusCode.NOT_STARTED; + + public GeneralAccountSendRequest(CryptoCoin coin, Context context, GeneralCoinAccount account, String toAccount, long amount, String memo) { + super(coin); + this.mContext = context; + this.mAccount = account; + this.mToAccount = toAccount; + this.mAmount = amount; + this.mMemo = memo; + } + + public GeneralAccountSendRequest(CryptoCoin coin, Context context, GeneralCoinAccount account, String toAccount, long amount) { + this(coin,context,account,toAccount,amount,null); + + } + + public Context getContext() { + return mContext; + } + + public GeneralCoinAccount getAccount() { + return mAccount; + } + + public String getToAccount() { + return mToAccount; + } + + public long getAmount() { + return mAmount; + } + + public String getMemo() { + return mMemo; + } + + public void validate(){ + if ((this.status != StatusCode.NOT_STARTED)){ + this._fireOnCarryOutEvent(); + } + } + + public void setStatus(StatusCode code){ + this.status = code; + this._fireOnCarryOutEvent(); + } + + public StatusCode getStatus() { + return status; + } + +}