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.
This commit is contained in:
parent
262a0d58ad
commit
35561059ce
14 changed files with 209 additions and 16 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package cy.agorise.bitsybitshareswallet.models.coingecko
|
||||
|
||||
data class HistoricalPrice(val id: String,
|
||||
val symbol: String,
|
||||
val market_data: MarketData)
|
|
@ -0,0 +1,3 @@
|
|||
package cy.agorise.bitsybitshareswallet.models.coingecko
|
||||
|
||||
data class MarketData(var current_price: HashMap<String, Double>)
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"])
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue