diff --git a/app/build.gradle b/app/build.gradle index d45ce5c..eafe733 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -72,9 +72,12 @@ dependencies { implementation "com.jakewharton.rxbinding3:rxbinding:$rx_bindings_version" implementation "com.jakewharton.rxbinding3:rxbinding-material:$rx_bindings_version" // Material Components widgets implementation "com.jakewharton.rxbinding3:rxbinding-appcompat:$rx_bindings_version" // AndroidX appcompat widgets - // Retrofit + // Retrofit & OkHttp implementation 'com.squareup.retrofit2:retrofit:2.5.0' implementation 'com.squareup.retrofit2:converter-gson:2.5.0' + implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0' + implementation 'com.squareup.okhttp3:okhttp:3.12.0' + implementation 'com.squareup.okhttp3:logging-interceptor:3.5.0' //Firebase implementation 'com.google.firebase:firebase-core:16.0.6' implementation 'com.google.firebase:firebase-crash:16.2.1' diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/CreateAccountFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/CreateAccountFragment.kt index 8c7a6ab..dc25c73 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/CreateAccountFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/CreateAccountFragment.kt @@ -8,6 +8,7 @@ import android.view.ViewGroup import androidx.navigation.fragment.findNavController import com.jakewharton.rxbinding3.widget.textChanges import cy.agorise.bitsybitshareswallet.R +import cy.agorise.bitsybitshareswallet.network.FaucetService import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.containsDigits import cy.agorise.bitsybitshareswallet.utils.containsVowels @@ -21,12 +22,20 @@ import cy.agorise.graphenej.models.JsonRpcResponse import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_create_account.* import org.bitcoinj.core.ECKey +import retrofit2.Callback import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader import java.util.concurrent.TimeUnit +import com.afollestad.materialdialogs.MaterialDialog +import cy.agorise.bitsybitshareswallet.models.FaucetRequest +import cy.agorise.bitsybitshareswallet.models.FaucetResponse +import cy.agorise.bitsybitshareswallet.network.ServiceGenerator +import retrofit2.Call +import retrofit2.Response -class CreateAccountFragment : ConnectedFragment() { + +class CreateAccountFragment : BaseAccountFragment() { companion object { private const val TAG = "CreateAccountFragment" @@ -34,10 +43,12 @@ class CreateAccountFragment : ConnectedFragment() { private const val BRAINKEY_FILE = "brainkeydict.txt" private const val MIN_ACCOUNT_NAME_LENGTH = 8 - private const val RESPONSE_GET_ACCOUNT_BY_NAME = 1 + // Used when trying to validate that the account name is available + private const val RESPONSE_GET_ACCOUNT_BY_NAME_VALIDATION = 1 + // Used when trying to obtain the info of the newly created account + private const val RESPONSE_GET_ACCOUNT_BY_NAME_CREATED = 2 } - private lateinit var mBrainKey: BrainKey private lateinit var mAddress: String /** Variables used to store the validation status of the form fields */ @@ -87,6 +98,7 @@ class CreateAccountFragment : ConnectedFragment() { btnCancel.setOnClickListener { findNavController().navigateUp() } btnCreate.isEnabled = false + btnCreate.setOnClickListener { createAccount() } // Generating BrainKey generateKeys() @@ -104,7 +116,7 @@ class CreateAccountFragment : ConnectedFragment() { val id = mNetworkService?.sendMessage(GetAccountByName(accountName), GetAccountByName.REQUIRED_API) if (id != null) - responseMap[id] = RESPONSE_GET_ACCOUNT_BY_NAME + responseMap[id] = RESPONSE_GET_ACCOUNT_BY_NAME_VALIDATION } enableDisableCreateButton() @@ -157,7 +169,8 @@ class CreateAccountFragment : ConnectedFragment() { if (responseMap.containsKey(response.id)) { val responseType = responseMap[response.id] when (responseType) { - RESPONSE_GET_ACCOUNT_BY_NAME -> handleAccountName(response.result) + RESPONSE_GET_ACCOUNT_BY_NAME_VALIDATION -> handleAccountNameValidation(response.result) + RESPONSE_GET_ACCOUNT_BY_NAME_CREATED -> handleAccountNameCreated(response.result) } responseMap.remove(response.id) } @@ -171,7 +184,7 @@ class CreateAccountFragment : ConnectedFragment() { * Handles the response from the NetworkService's GetAccountByName call to decide if the user's suggested * account is available or not. */ - private fun handleAccountName(result: Any?) { + private fun handleAccountNameValidation(result: Any?) { if (result is AccountProperties) { tilAccountName.helperText = "" tilAccountName.error = getString(R.string.error__account_not_available) @@ -185,6 +198,71 @@ class CreateAccountFragment : ConnectedFragment() { enableDisableCreateButton() } + /** + * Handles the response from the NetworkService's GetAccountByName call and stores the information of the newly + * created account if the result is successful, shows a toast error otherwise + */ + private fun handleAccountNameCreated(result: Any?) { + if (result is AccountProperties) { + onAccountSelected(result, tietPin.text.toString()) + } else { + context?.toast(getString(R.string.error__created_account_not_found)) + } + } + + /** + * Sends the account-creation request to the faucet server. + * Only account name and public address is sent here. + */ + private fun createAccount() { + val accountName = tietAccountName.text.toString() + val faucetRequest = FaucetRequest(accountName, mAddress, Constants.FAUCET_REFERRER) + + val sg = ServiceGenerator(Constants.FAUCET_URL) + val faucetService = sg.getService(FaucetService::class.java) + + val call = faucetService.registerPrivateAccount(faucetRequest) + + // Execute the call asynchronously. Get a positive or negative callback. + call.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + // The network call was a success and we got a response + getCreatedAccountInfo(response.body()) + } + + override fun onFailure(call: Call, t: Throwable) { + // the network call was a failure + MaterialDialog(context!!) + .title(R.string.title_error) + .message(cy.agorise.bitsybitshareswallet.R.string.error__faucet) + .negativeButton(android.R.string.ok) + .show() + } + }) + } + + private fun getCreatedAccountInfo(faucetResponse: FaucetResponse?) { + if (faucetResponse?.account != null) { + val id = mNetworkService?.sendMessage(GetAccountByName(faucetResponse.account?.name), + GetAccountByName.REQUIRED_API) + + if (id != null) + responseMap[id] = RESPONSE_GET_ACCOUNT_BY_NAME_CREATED + } else { + Log.d(TAG, "Private account creation failed ") + val content = if (faucetResponse?.error?.base?.size ?: 0 > 0) { + getString(R.string.error__faucet_template, faucetResponse?.error?.base?.get(0)) + } else { + getString(R.string.error__faucet_template, "None") + } + + MaterialDialog(context!!) + .title(R.string.title_error) + .message(text = content) + .show() + } + } + /** * Method that generates a fresh key that will be controlling the newly created account. */ @@ -197,11 +275,11 @@ class CreateAccountFragment : ConnectedFragment() { val brainKeySuggestion = BrainKey.suggest(dictionary) mBrainKey = BrainKey(brainKeySuggestion, 0) - val address = Address(ECKey.fromPublicOnly(mBrainKey.privateKey.pubKey)) + val address = Address(ECKey.fromPublicOnly(mBrainKey?.privateKey?.pubKey)) Log.d(TAG, "brain key: $brainKeySuggestion") Log.d(TAG, "address would be: " + address.toString()) mAddress = address.toString() - tvBrainKey.text = mBrainKey.brainKey + tvBrainKey.text = mBrainKey?.brainKey } catch (e: IOException) { Log.e(TAG, "IOException while trying to generate key. Msg: " + e.message) diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetAccount.java b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetAccount.java new file mode 100644 index 0000000..40d3ef4 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetAccount.java @@ -0,0 +1,73 @@ +package cy.agorise.bitsybitshareswallet.models; + +/** + * Class used to deserialize a the "account" object contained in the faucet response to the + * {@link cy.agorise.bitsybitshareswallet.network.FaucetService#registerPrivateAccount(FaucetRequest)} API call. + */ + +public class FaucetAccount { + public String name; + public String owner_key; + public String active_key; + public String memo_key; + public String referrer; + public String refcode; + + public FaucetAccount(String accountName, String address, String referrer){ + this.name = accountName; + this.owner_key = address; + this.active_key = address; + this.memo_key = address; + this.refcode = referrer; + this.referrer = referrer; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getOwnerKey() { + return owner_key; + } + + public void setOwnerKey(String owner_key) { + this.owner_key = owner_key; + } + + public String getActiveKey() { + return active_key; + } + + public void setActiveKey(String active_key) { + this.active_key = active_key; + } + + public String getMemoKey() { + return memo_key; + } + + public void setMemoKey(String memo_key) { + this.memo_key = memo_key; + } + + public String getRefcode() { + return refcode; + } + + public void setRefcode(String refcode) { + this.refcode = refcode; + } + + public String getReferrer() { + return referrer; + } + + public void setReferrer(String referrer) { + this.referrer = referrer; + } +} + diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetRequest.java b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetRequest.java new file mode 100644 index 0000000..0103ea9 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetRequest.java @@ -0,0 +1,13 @@ +package cy.agorise.bitsybitshareswallet.models; + +/** + * Class used to encapsulate a faucet account creation request + */ + +public class FaucetRequest { + private FaucetAccount account; + + public FaucetRequest(String accountName, String address, String referrer){ + account = new FaucetAccount(accountName, address, referrer); + } +} diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetResponse.java b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetResponse.java new file mode 100644 index 0000000..c7d7967 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetResponse.java @@ -0,0 +1,10 @@ +package cy.agorise.bitsybitshareswallet.models; + +public class FaucetResponse { + public FaucetAccount account; + public Error error; + + public class Error { + public String[] base; + } +} diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/network/FaucetService.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/network/FaucetService.kt new file mode 100644 index 0000000..c266eb0 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/network/FaucetService.kt @@ -0,0 +1,23 @@ +package cy.agorise.bitsybitshareswallet.network + +import cy.agorise.bitsybitshareswallet.models.FaucetRequest +import cy.agorise.bitsybitshareswallet.models.FaucetResponse +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.POST + +/** + * Interface to the faucet service. The faucet is used in order to register new BitShares accounts. + */ +interface FaucetService { + + @GET("/") + fun checkStatus(): Call + + @Headers("Content-Type: application/json") + @POST("/api/v1/accounts") + fun registerPrivateAccount(@Body faucetRequest: FaucetRequest): Call +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/network/ServiceGenerator.java b/app/src/main/java/cy/agorise/bitsybitshareswallet/network/ServiceGenerator.java new file mode 100644 index 0000000..6554677 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/network/ServiceGenerator.java @@ -0,0 +1,82 @@ +package cy.agorise.bitsybitshareswallet.network; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +public class ServiceGenerator{ + public static String API_BASE_URL; + private static HttpLoggingInterceptor logging; + private static OkHttpClient.Builder httpClient; + private static Retrofit.Builder builder; + + private static HashMap, Object> Services; + + public ServiceGenerator(String apiBaseUrl) { + API_BASE_URL= apiBaseUrl; + logging = new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY); + httpClient = new OkHttpClient.Builder().addInterceptor(logging); + builder = new Retrofit.Builder() + .baseUrl(API_BASE_URL) + .addConverterFactory(GsonConverterFactory.create(getGson())) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()); + Services = new HashMap, Object>(); + } + + /** + * Customizes the Gson instance with specific de-serialization logic + */ + private Gson getGson(){ + GsonBuilder builder = new GsonBuilder(); + + return builder.create(); + } + + public void setCallbackExecutor(Executor executor){ + builder.callbackExecutor(executor); + } + + public static void setService(Class klass, T thing) { + Services.put(klass, thing); + } + + public T getService(Class serviceClass) { + + T service = serviceClass.cast(Services.get(serviceClass)); + if (service == null) { + service = createService(serviceClass); + setService(serviceClass, service); + } + return service; + } + + public static S createService(Class serviceClass) { + + httpClient.interceptors().add(new Interceptor() { + @Override + public okhttp3.Response intercept(Interceptor.Chain chain) throws IOException { + okhttp3.Request original = chain.request(); + // Request customization: add request headers + okhttp3.Request.Builder requestBuilder = original.newBuilder().method(original.method(), original.body()); + + okhttp3.Request request = requestBuilder.build(); + return chain.proceed(request); + } + }); + httpClient.readTimeout(5, TimeUnit.MINUTES); + httpClient.connectTimeout(5, TimeUnit.MINUTES); + OkHttpClient client = httpClient.build(); + Retrofit retrofit = builder.client(client).build(); + return retrofit.create(serviceClass); + } +} diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt index 7aa59cf..15eb98a 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt @@ -14,6 +14,12 @@ object Constants { /** The minimum required length for a PIN number */ const val MIN_PIN_LENGTH = 6 + /** Name of the account passed to the faucet as the referrer */ + const val FAUCET_REFERRER = "agorise" + + /** Faucet URL used to create new accounts */ + const val FAUCET_URL = "https://faucet.palmpay.io" + /** The user selected encrypted PIN */ const val KEY_ENCRYPTED_PIN = "key_encrypted_pin" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 50853ae..55833a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,10 @@ Verifying account availability… Account not available Account is available + Error + The server returned an error. This might be due to a limitation purposefully set in place to disallow frequent requests coming from the same IP address in a short time lapse. Please wait 5 minutes and try again, or switch to a different network, for example from wifi to cell. + The faucet returned an error. Msg: %1$s + The app could not retrieve information about the newly created account Transactions