Added method to create Account using the Agorise's faucet, and also making use of Retrofit's suggested ServiceGenerator.

This commit is contained in:
Severiano Jaramillo 2019-01-08 16:35:14 -06:00
parent 860947b351
commit 78909498c0
9 changed files with 301 additions and 9 deletions

View file

@ -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'

View file

@ -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<FaucetResponse> {
override fun onResponse(call: Call<FaucetResponse>, response: Response<FaucetResponse>) {
// The network call was a success and we got a response
getCreatedAccountInfo(response.body())
}
override fun onFailure(call: Call<FaucetResponse>, 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)

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,10 @@
package cy.agorise.bitsybitshareswallet.models;
public class FaucetResponse {
public FaucetAccount account;
public Error error;
public class Error {
public String[] base;
}
}

View file

@ -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<ResponseBody>
@Headers("Content-Type: application/json")
@POST("/api/v1/accounts")
fun registerPrivateAccount(@Body faucetRequest: FaucetRequest): Call<FaucetResponse>
}

View file

@ -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<Class<?>, 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<Class<?>, 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 <T> void setService(Class<T> klass, T thing) {
Services.put(klass, thing);
}
public <T> T getService(Class<T> serviceClass) {
T service = serviceClass.cast(Services.get(serviceClass));
if (service == null) {
service = createService(serviceClass);
setService(serviceClass, service);
}
return service;
}
public static <S> S createService(Class<S> 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);
}
}

View file

@ -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"

View file

@ -32,6 +32,10 @@
<string name="text__verifying_account_availability">Verifying account availability…</string>
<string name="error__account_not_available">Account not available</string>
<string name="text__account_is_available">Account is available</string>
<string name="title_error">Error</string>
<string name="error__faucet">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.</string>
<string name="error__faucet_template">The faucet returned an error. Msg: %1$s</string>
<string name="error__created_account_not_found">The app could not retrieve information about the newly created account</string>
<!-- Home -->
<string name="title_transactions">Transactions</string>