This commit finishes with the second part of the equivalent values calculation. In this part of the procedure a query is performed

looking for entries in the 'transactions' table that are missing a corresponding entry in the 'equivalent_values' table.

Whenere a transaction is detected to not have an equivalent value for a given fiat, a network request is made to the Coingecko's API in order
to obtain the BTS market value at the day of that transaction. An equivalent value is thus calculated using the 'bts_value' of the transaction.
master
Nelson R. Perez 2019-02-04 21:00:57 -05:00
parent 262a0d58ad
commit 35561059ce
14 changed files with 209 additions and 16 deletions

View File

@ -23,6 +23,7 @@ import cy.agorise.bitsybitshareswallet.processors.TransfersLoader
import cy.agorise.bitsybitshareswallet.repositories.AssetRepository
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceViewModel
import cy.agorise.bitsybitshareswallet.viewmodels.ConnectedActivityViewModel
import cy.agorise.bitsybitshareswallet.viewmodels.TransferViewModel
import cy.agorise.bitsybitshareswallet.viewmodels.UserAccountViewModel
import cy.agorise.graphenej.Asset
@ -67,6 +68,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private lateinit var mUserAccountViewModel: UserAccountViewModel
private lateinit var mBalanceViewModel: BalanceViewModel
private lateinit var mTransferViewModel: TransferViewModel
private lateinit var mConnectedActivityViewModel: ConnectedActivityViewModel
private lateinit var mAssetRepository: AssetRepository
@ -115,6 +117,11 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
mAssetRepository = AssetRepository(this)
// Configure ConnectedActivityViewModel to obtain missing equivalent values
mConnectedActivityViewModel = ViewModelProviders.of(this).get(ConnectedActivityViewModel::class.java)
mConnectedActivityViewModel.observeMissingEquivalentValuesIn("usd") //TODO: Obtain this from shared preferences?
// Configure UserAccountViewModel to obtain the missing account ids
mUserAccountViewModel = ViewModelProviders.of(this).get(UserAccountViewModel::class.java)

View File

@ -5,7 +5,6 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import cy.agorise.bitsybitshareswallet.database.entities.EquivalentValue
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
@Dao
interface EquivalentValueDao {

View File

@ -3,6 +3,7 @@ package cy.agorise.bitsybitshareswallet.database.daos
import androidx.lifecycle.LiveData
import androidx.room.*
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
import io.reactivex.Observable
import io.reactivex.Single
@Dao
@ -32,8 +33,8 @@ interface TransferDao {
@Query("SELECT * FROM transfers WHERE timestamp != 0 AND bts_value = -1 AND transfer_asset_id != '1.3.0' LIMIT 1")
fun getTransfersWithMissingBtsValue(): LiveData<Transfer>
@Query("SELECT * FROM transfers WHERE id NOT IN (SELECT transfer_id FROM equivalent_values WHERE symbol = :symbol)")
fun getTransfersWithMissingValueIn(symbol: String): LiveData<List<Transfer>>
@Query("SELECT * FROM transfers WHERE id NOT IN (SELECT transfer_id FROM equivalent_values WHERE symbol = :symbol) AND bts_value >= 0 LIMIT 1")
fun getTransfersWithMissingValueIn(symbol: String): Observable<Transfer>
@Query("DELETE FROM transfers")
fun deleteAll()

View File

@ -0,0 +1,5 @@
package cy.agorise.bitsybitshareswallet.models.coingecko
data class HistoricalPrice(val id: String,
val symbol: String,
val market_data: MarketData)

View File

@ -0,0 +1,3 @@
package cy.agorise.bitsybitshareswallet.models.coingecko
data class MarketData(var current_price: HashMap<String, Double>)

View File

@ -0,0 +1,22 @@
package cy.agorise.bitsybitshareswallet.models.coingecko
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import junit.framework.Assert
import java.lang.reflect.Type
class MarketDataDeserializer : JsonDeserializer<MarketData> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): MarketData {
val hashMap = HashMap<String, Double>()
val obj = json?.asJsonObject?.get("current_price")?.asJsonObject
if(obj != null){
val keySet = obj.asJsonObject.keySet()
for(key in keySet){
println("$key -> : ${obj[key].asDouble}")
hashMap[key] = obj[key].asDouble
}
}
return MarketData(hashMap)
}
}

View File

@ -0,0 +1,16 @@
package cy.agorise.bitsybitshareswallet.network
import retrofit2.Call
import cy.agorise.bitsybitshareswallet.models.coingecko.HistoricalPrice
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Query
interface CoingeckoService {
@Headers("Content-Type: application/json")
@GET("/api/v3/coins/bitshares/history")
fun getHistoricalValueSync(@Query("id") id: String,
@Query("date") date: String,
@Query("localization") localization: Boolean): Call<HistoricalPrice>
}

View File

@ -3,6 +3,8 @@ package cy.agorise.bitsybitshareswallet.network;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import cy.agorise.bitsybitshareswallet.models.coingecko.MarketData;
import cy.agorise.bitsybitshareswallet.models.coingecko.MarketDataDeserializer;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
@ -22,23 +24,26 @@ public class ServiceGenerator{
private static HashMap<Class<?>, Object> Services;
public ServiceGenerator(String apiBaseUrl) {
public ServiceGenerator(String apiBaseUrl, Gson gson) {
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()))
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
Services = new HashMap<Class<?>, Object>();
}
public ServiceGenerator(String apiBaseUrl){
this(apiBaseUrl, new Gson());
}
/**
* Customizes the Gson instance with specific de-serialization logic
*/
private Gson getGson(){
GsonBuilder builder = new GsonBuilder();
return builder.create();
}
@ -76,6 +81,11 @@ public class ServiceGenerator{
httpClient.readTimeout(5, TimeUnit.MINUTES);
httpClient.connectTimeout(5, TimeUnit.MINUTES);
OkHttpClient client = httpClient.build();
if(serviceClass == CoingeckoService.class){
// The MarketData class needs a custom de-serializer
Gson gson = new GsonBuilder().registerTypeAdapter(MarketData.class, new MarketDataDeserializer()).create();
builder.addConverterFactory(GsonConverterFactory.create(gson));
}
Retrofit retrofit = builder.client(client).build();
return retrofit.create(serviceClass);
}

View File

@ -0,0 +1,18 @@
package cy.agorise.bitsybitshareswallet.repositories
import android.content.Context
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.daos.EquivalentValueDao
import cy.agorise.bitsybitshareswallet.database.daos.TransferDao
class EquivalentValuesRepository(context: Context) {
private val mEquivalentValuesDao: EquivalentValueDao?
private val mTransfersDao: TransferDao?
init {
val db = BitsyDatabase.getDatabase(context)
mEquivalentValuesDao = db?.equivalentValueDao()
mTransfersDao = db?.transferDao()
}
}

View File

@ -2,19 +2,32 @@ package cy.agorise.bitsybitshareswallet.repositories
import android.content.Context
import android.os.AsyncTask
import android.util.Log
import androidx.lifecycle.LiveData
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.daos.EquivalentValueDao
import cy.agorise.bitsybitshareswallet.database.daos.TransferDao
import cy.agorise.bitsybitshareswallet.database.entities.EquivalentValue
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
import cy.agorise.bitsybitshareswallet.network.CoingeckoService
import cy.agorise.bitsybitshareswallet.network.ServiceGenerator
import cy.agorise.bitsybitshareswallet.utils.Constants
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import java.text.SimpleDateFormat
import java.util.*
class TransferRepository internal constructor(context: Context) {
private val TAG = "TransferRepository"
private val mTransferDao: TransferDao
private val mEquivalentValuesDao: EquivalentValueDao
private val compositeDisposable = CompositeDisposable()
init {
val db = BitsyDatabase.getDatabase(context)
mTransferDao = db!!.transferDao()
mEquivalentValuesDao = db!!.equivalentValueDao()
}
fun insertAll(transfers: List<Transfer>) {
@ -41,10 +54,6 @@ class TransferRepository internal constructor(context: Context) {
return mTransferDao.getTransferBlockNumberWithMissingTime()
}
fun getTransfersWithMissingValueIn(symbol: String): LiveData<List<Transfer>> {
return mTransferDao.getTransfersWithMissingValueIn(symbol)
}
fun getTransfersWithMissingBtsValue(): LiveData<Transfer> {
return mTransferDao.getTransfersWithMissingBtsValue()
}
@ -53,6 +62,63 @@ class TransferRepository internal constructor(context: Context) {
deleteAllAsyncTask(mTransferDao).execute()
}
/**
* Creates a subscription to the transfers table which will listen & process equivalent values.
*
* This function will create a subscription that will listen for missing equivalent values. This will
* automatically trigger a procedure designed to calculate the fiat equivalent value of any entry
* of the 'transactions' table that stil doesn't have a corresponding entry in the 'equivalent_values'
* table for that specific fiat currency.
*
* @param symbol The 3 letters symbol of the desired fiat currency.
*/
fun observeMissingEquivalentValuesIn(symbol: String) {
compositeDisposable.add(mTransferDao.getTransfersWithMissingValueIn(symbol)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.map { transfer -> obtainFiatValue(transfer, symbol) }
.subscribe({
Log.d(TAG,"Got equivalent value: $it")
mEquivalentValuesDao.insert(it)
},{
Log.e(TAG,"Error while trying to create a new equivalent value. Msg: ${it.message}")
for(element in it.stackTrace){
Log.e(TAG,"${element.className}#${element.methodName}:${element.lineNumber}")
}
}))
}
/**
* Creates an equivalent value for a given transaction.
*
* Function used to perform a request to the Coingecko's price API trying to obtain the
* equivalent value of a specific [Transaction].
*
* @param transfer The transfer whose equivalent value we want to obtain
* @param symbol The symbol of the fiat that the equivalent value should be calculated in
* @return An instance of the [EquivalentValue] class, ready to be inserted into the database.
*/
fun obtainFiatValue(transfer: Transfer, symbol: String): EquivalentValue {
val sg = ServiceGenerator(Constants.COINGECKO_URL)
val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.ROOT)
val date = Date(transfer.timestamp * 1000)
val response = sg.getService(CoingeckoService::class.java)
.getHistoricalValueSync("bitshares", dateFormat.format(date), false)
.execute()
var equivalentFiatValue = -1L
if(response.isSuccessful){
val price: Double = response.body()?.market_data?.current_price?.get(symbol) ?: -1.0
// The equivalent value is obtained by:
// 1- Dividing the base value by 100000 (BTS native precision)
// 2- Multiplying that BTS value by the unit price in the chosen fiat
// 3- Multiplying the resulting value by 100 in order to express it in cents
equivalentFiatValue = Math.round(transfer.btsValue?.div(1e5)?.times(price)?.times(100) ?: -1.0)
}else{
Log.w(TAG,"Request was not successful. code: ${response.code()}")
}
return EquivalentValue(transfer.id, equivalentFiatValue, symbol)
}
private class insertAllAsyncTask internal constructor(private val mAsyncTaskDao: TransferDao) :
AsyncTask<List<Transfer>, Void, Void>() {
@ -79,4 +145,15 @@ class TransferRepository internal constructor(context: Context) {
return null
}
}
/**
* Called whenever the disposables have to be cleared.
*
* Since this repository manages a subscription it is necessary to clear the disposable after we're done with it.
* The parent ViewModel will let us know when that
*/
fun onCleared() {
if(!compositeDisposable.isDisposed)
compositeDisposable.clear()
}
}

View File

@ -22,6 +22,9 @@ object Constants {
/** Faucet URL used to create new accounts */
const val FAUCET_URL = "https://faucet.palmpay.io"
/** Coingecko's API URL */
const val COINGECKO_URL = "https://api.coingecko.com"
/** The user selected encrypted PIN */
const val KEY_ENCRYPTED_PIN = "key_encrypted_pin"

View File

@ -0,0 +1,18 @@
package cy.agorise.bitsybitshareswallet.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import cy.agorise.bitsybitshareswallet.repositories.TransferRepository
class ConnectedActivityViewModel(application: Application) : AndroidViewModel(application) {
private var mTransfersRepository = TransferRepository(application)
fun observeMissingEquivalentValuesIn(symbol: String) {
mTransfersRepository.observeMissingEquivalentValuesIn(symbol)
}
override fun onCleared() {
super.onCleared()
mTransfersRepository.onCleared()
}
}

View File

@ -1,7 +1,6 @@
package cy.agorise.bitsybitshareswallet.viewmodels
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import com.google.common.primitives.UnsignedLong
@ -27,10 +26,6 @@ class TransferViewModel(application: Application) : AndroidViewModel(application
return mTransferRepository.getTransferBlockNumberWithMissingTime()
}
fun getTransfersWithMissingValueIn(symbol: String) : LiveData<List<Transfer>>{
return mTransferRepository.getTransfersWithMissingValueIn(symbol)
}
fun getTransfersWithMissingBtsValue() : LiveData<Transfer> {
return mTransferRepository.getTransfersWithMissingBtsValue()
}

View File

@ -0,0 +1,19 @@
package cy.agorise.bitsybitshareswallet
import com.google.gson.GsonBuilder
import cy.agorise.bitsybitshareswallet.models.coingecko.MarketData
import cy.agorise.bitsybitshareswallet.models.coingecko.MarketDataDeserializer
import org.junit.Assert
import org.junit.Test
class MarketDataDeserializerTest {
@Test
fun marketDataDeserializationTest(){
val str = "{\"current_price\": {\"aed\": 0.14139359620401012,\"ars\": 1.476552955052185,\"aud\": 0.05410080634896981,\"bch\": 0.0003021370317928406,\"bdt\": 3.2298217535732276,\"bhd\": 0.01451147244444769,\"bmd\": 0.03849350092032233,\"bnb\": 0.007113127493734956,\"brl\": 0.15000509277539803,\"btc\": 0.00001043269732289735,\"cad\": 0.051866143140042266,\"chf\": 0.03825734329217614,\"clp\": 26.587581916766037,\"cny\": 0.2652895096426772,\"czk\": 0.8706365729081245,\"dkk\": 0.25236393094264586,\"eos\": 0.01566778197589746,\"eth\": 0.0003870069548974383,\"eur\": 0.033804376612212375,\"gbp\": 0.030484350651335475,\"hkd\": 0.3012660745118239,\"huf\": 10.909058160819312,\"idr\": 558.1942568455942,\"ils\": 0.14452962323048843,\"inr\": 2.721290348862006,\"jpy\": 4.327150672205728,\"krw\": 43.47379006939362,\"kwd\": 0.011703102097803801,\"lkr\": 6.939897047172613,\"ltc\": 0.0013225337650442446,\"mmk\": 60.56217136246436,\"mxn\": 0.7738105980956592,\"myr\": 0.1608450935955668,\"nok\": 0.335428517669597,\"nzd\": 0.056803550529088344,\"php\": 2.046274976098886,\"pkr\": 5.3730315641051885,\"pln\": 0.1449376543402434,\"rub\": 2.596498268228413,\"sar\": 0.1444545609036934,\"sek\": 0.3498212376637053,\"sgd\": 0.05281188996415366,\"thb\": 1.2598922851221481,\"try\": 0.20393883733037357,\"twd\": 1.1869880579631216,\"usd\": 0.03849350092032233,\"vef\": 9565.159285292651,\"xag\": 0.002632124388265174,\"xau\": 0.00003094261577979185,\"xdr\": 0.02769368731511483,\"xlm\": 0.3411570542267162,\"xrp\": 0.11074614753363282,\"zar\": 0.5534635980499906}}"
val gson = GsonBuilder().registerTypeAdapter(MarketData::class.java, MarketDataDeserializer())
.create()
val marketData = gson.fromJson<MarketData>(str, MarketData::class.java)
Assert.assertEquals(0.03849350092032233, marketData.current_price["usd"])
Assert.assertEquals(0.033804376612212375, marketData.current_price["eur"])
}
}