Merge branch 'feat_equiv_values' into develop

master
Severiano Jaramillo 2019-02-05 21:12:03 -06:00
commit 577e3ae01f
20 changed files with 519 additions and 34 deletions

View File

@ -118,8 +118,9 @@ dependencies {
// Core library
androidTestImplementation 'androidx.test:core:1.1.0'
// testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"
androidTestImplementation "androidx.room:room-testing:$room_version"
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'com.jraska.livedata:testing-ktx:1.0.0'
}

View File

@ -0,0 +1,42 @@
package cy.agorise.bitsybitshareswallet
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
object LiveDataTestUtil {
fun <T> getValue(liveData: LiveData<T>): T {
val data = arrayOfNulls<Any>(1)
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data[0] = o
latch.countDown()
liveData.removeObserver(this)
}
}
liveData.observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
@Suppress("UNCHECKED_CAST")
return data[0] as T
}
}

View File

@ -1,20 +1,19 @@
package cy.agorise.bitsybitshareswallet;
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.runner.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.entities.Merchant
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.*
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
public class MerchantQueryTest {
class MerchantQueryTest {
@get:Rule val testRule = InstantTaskExecutorRule()
private lateinit var db: BitsyDatabase
@Before

View File

@ -0,0 +1,121 @@
package cy.agorise.bitsybitshareswallet
import android.content.Context
import android.util.Log
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.jraska.livedata.test
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.entities.EquivalentValue
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
import org.junit.*
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
class TransfersTests {
val TAG = "TransfersTests"
@get:Rule val testRule = InstantTaskExecutorRule()
private lateinit var db: BitsyDatabase
private val context = ApplicationProvider.getApplicationContext<Context>()
@Before
fun createDb() {
Log.d(TAG,"createDb")
db = Room.inMemoryDatabaseBuilder(context, BitsyDatabase::class.java).build()
}
@After
@Throws(IOException::class)
fun closeDb(){
Log.d(TAG,"closeDB")
db.close()
}
/**
* Prepares the database to the testGetTransfersMissingEquivalentValues and testGetTransfersMissingEquivalentValues2
* tests.
*/
private fun prepareMissingEquivalentValues(){
// We create 2 transfers for the 'transfers' table, but only one of them will have an equivalent value entry
val t1 = Transfer("1.11.702181910", 34118155, 1485018549, 264174, "1.3.0", "1.2.32567","1.2.139293",15869682,"1.3.0","")
val t2 = Transfer("1.11.684483739", 33890367, 1547171166, 11030, "1.3.0", "1.2.139293","1.2.1029856",98,"1.3.120","")
db.transferDao().insert(t1)
db.transferDao().insert(t2)
// Here's the equivalent value for the first transaction inserted (t1)
val equivalentValue = EquivalentValue("1.11.702181910", 0, "usd")
db.equivalentValueDao().insert(equivalentValue)
}
/**
* This test makes use of the LiveData Testing library and its objective is to prove that
* the TransferDao#getTransfersWithMissingValueIn(symbol: String) will return only the
* second 'transfer' entry.
* <p>
* @see cy.agorise.bitsybitshareswallet.database.daos.TransferDao.getTransfersWithMissingValueIn
* @see cy.agorise.bitsybitshareswallet.LiveDataTestUtil
*/
@Test
fun testGetTransfersMissingEquivalentValues(){
prepareMissingEquivalentValues()
db.transferDao()
.getTransfersWithMissingValueIn("usd")
.test()
.awaitValue()
.assertHasValue()
.assertValue { transfers -> transfers.size == 1 }
.assertValue { transfers -> transfers[0].id == "1.11.684483739"}
.assertValue { transfers -> transfers[0].blockNumber == 33890367L}
}
/**
* This test makes use of the simple LiveDataTestUtil class and its objective is to prove that
* the TransferDao#getTransfersWithMissingValueIn(symbol: String) will return only the
* second 'transfer' entry.
* <p>
* @see cy.agorise.bitsybitshareswallet.LiveDataTestUtil
*/
@Test
fun testGetTransfersMissingEquivalentValues2(){
prepareMissingEquivalentValues()
val transfers: List<Transfer> = LiveDataTestUtil.getValue(db.transferDao().getTransfersWithMissingValueIn("usd"))
Assert.assertNotNull(transfers)
Assert.assertEquals(1, transfers.size)
Assert.assertEquals("1.11.684483739", transfers[0].id)
Assert.assertEquals(33890367, transfers[0].blockNumber)
Log.d(TAG, "transfer ${transfers[0]}");
}
@Test
fun testGetTransfersWithMissingBtsValue(){
val t1 = Transfer("1.11.702181910",
34118155,
1485018549,
264174,
"1.3.0",
"1.2.32567",
"1.2.139293",
15869682,
"1.3.0","")
val t2 = Transfer("1.11.684483739",
33890367,
1547171166,
11030,
"1.3.0",
"1.2.139293",
"1.2.1029856",
98,
"1.3.120",
"",
1000)
db.transferDao().insert(t1)
db.transferDao().insert(t2)
db.transferDao().getTransfersWithMissingBtsValue()
.test()
.assertHasValue()
.assertValue { transfer -> transfer.id == "1.11.702181910" }
}
}

View File

@ -4,6 +4,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.AsyncTask
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
@ -17,29 +18,32 @@ import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore
import cy.agorise.bitsybitshareswallet.BuildConfig
import cy.agorise.bitsybitshareswallet.database.entities.Balance
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
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
import cy.agorise.graphenej.AssetAmount
import cy.agorise.graphenej.UserAccount
import cy.agorise.graphenej.api.ApiAccess
import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.android.NetworkService
import cy.agorise.graphenej.api.android.RxBus
import cy.agorise.graphenej.api.calls.*
import cy.agorise.graphenej.models.AccountProperties
import cy.agorise.graphenej.models.BlockHeader
import cy.agorise.graphenej.models.FullAccountDetails
import cy.agorise.graphenej.models.JsonRpcResponse
import cy.agorise.graphenej.models.*
import io.fabric.sdk.android.Fabric
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
@ -58,11 +62,13 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private const val RESPONSE_GET_ACCOUNT_BALANCES = 3
private const val RESPONSE_GET_ASSETS = 4
private const val RESPONSE_GET_BLOCK_HEADER = 5
private const val RESPONSE_GET_MARKET_HISTORY = 6
}
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
@ -71,8 +77,8 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private val mHandler = Handler()
// Disposable returned at the bus subscription
private var mDisposable: Disposable? = null
// Composite disposable used to clear all disposables once the activity is destroyed
private val mCompositeDisposable = CompositeDisposable()
private var storedOpCount: Long = -1
@ -90,6 +96,10 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private var blockNumberWithMissingTime = 0L
// Variable used to hold a reference to the specific Transfer instance which we're currently trying
// to resolve an equivalent BTS value
var transfer: Transfer? = null
/**
* Flag used to keep track of the NetworkService binding state
*/
@ -107,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)
@ -139,12 +154,11 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
mTransferViewModel.getTransferBlockNumberWithMissingTime().observe(this, Observer<Long>{ blockNumber ->
if (blockNumber != null && blockNumber != blockNumberWithMissingTime) {
blockNumberWithMissingTime = blockNumber
Log.d(TAG, "Block number: $blockNumber, Time: ${System.currentTimeMillis()}")
mHandler.post(mRequestBlockMissingTimeTask)
}
})
mDisposable = RxBus.getBusInstance()
val disposable = RxBus.getBusInstance()
.asFlowable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
@ -152,6 +166,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
}, {
this.handleError(it)
})
mCompositeDisposable.add(disposable)
}
/**
@ -202,6 +217,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
handleBlockHeader(message.result as BlockHeader, blockNumber)
requestIdToBlockNumberMap.remove(message.id)
}
RESPONSE_GET_MARKET_HISTORY -> handleMarketData(message.result as List<BucketObject>)
}
responseMap.remove(message.id)
}
@ -223,10 +239,35 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
// If we got a disconnection notification, we should clear our response map, since
// all its stored request ids will now be reset
responseMap.clear()
} else if (message.updateCode == ConnectionStatusUpdate.API_UPDATE) {
// If we got an API update
if(message.api == ApiAccess.API_HISTORY) {
// Starts the procedure that will obtain the missing equivalent values
mTransferViewModel
.getTransfersWithMissingBtsValue().observe(this, Observer<Transfer> {
if(it != null) handleTransfersWithMissingBtsValue(it)
})
}
}
}
}
/**
* Method called whenever we get a list of transfers with their bts value missing.
*/
private fun handleTransfersWithMissingBtsValue(transfer: Transfer) {
if(mNetworkService?.isConnected == true){
val base = Asset(transfer.transferAssetId)
val quote = Asset("1.3.0")
val bucket: Long = TimeUnit.SECONDS.convert(1, TimeUnit.DAYS)
val end: Long = transfer.timestamp * 1000L
val start: Long = (transfer.timestamp - bucket) * 1000L
val id = mNetworkService!!.sendMessage(GetMarketHistory(base, quote, bucket, start, end), GetMarketHistory.REQUIRED_API)
responseMap[id] = RESPONSE_GET_MARKET_HISTORY
this.transfer = transfer
}
}
/**
* Method called whenever a response to the 'get_full_accounts' API call has been detected.
* @param accountDetails De-serialized account details object
@ -275,7 +316,6 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
}
private fun handleBalanceUpdate(assetAmountList: List<AssetAmount>) {
Log.d(TAG, "handleBalanceUpdate")
val now = System.currentTimeMillis() / 1000
val balances = ArrayList<Balance>()
for (assetAmount in assetAmountList) {
@ -330,6 +370,25 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
}
}
private fun handleMarketData(buckets: List<BucketObject>) {
if(buckets.isNotEmpty()){
Log.d(TAG,"handleMarketData. Bucket is not empty")
val bucket = buckets[0]
val pair = Pair(transfer, bucket)
val disposable = Observable.just(pair)
.subscribeOn(Schedulers.computation())
.map { mTransferViewModel.updateBtsValue(it.first!!, it.second) }
.subscribe({},{
Log.e(TAG,"Error at updateBtsValue. Msg: ${it.message}")
for(line in it.stackTrace) Log.e(TAG, "${line.className}#${line.methodName}:${line.lineNumber}")
})
mCompositeDisposable.add(disposable)
}else{
Log.i(TAG,"handleMarketData. Bucket IS empty")
AsyncTask.execute { mTransferViewModel.updateBtsValue(transfer!!, Transfer.ERROR) }
}
}
private fun updateBalances() {
if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetAccountBalances(mCurrentAccount, ArrayList()),
@ -444,6 +503,6 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
override fun onDestroy() {
super.onDestroy()
if (!mDisposable!!.isDisposed) mDisposable!!.dispose()
if(!mCompositeDisposable.isDisposed) mCompositeDisposable.dispose()
}
}

View File

@ -17,4 +17,7 @@ interface AssetDao {
@Query("SELECT id, symbol, precision, description, issuer FROM assets INNER JOIN balances WHERE assets.id = balances.asset_id AND balances.asset_amount > 0")
fun getAllNonZero(): LiveData<List<Asset>>
@Query("SELECT * FROM assets WHERE id = :assetId")
fun getAssetDetails(assetId: String): Asset
}

View File

@ -1,18 +1,19 @@
package cy.agorise.bitsybitshareswallet.database.daos
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.*
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
import io.reactivex.Observable
import io.reactivex.Single
@Dao
interface TransferDao {
@Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(transfer: Transfer)
@Update()
fun update(transfer: Transfer)
// TODO find a way to return number of added rows
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertAll(transfers: List<Transfer>)
@ -29,6 +30,12 @@ interface TransferDao {
@Query("SELECT block_number FROM transfers WHERE timestamp='0' LIMIT 1")
fun getTransferBlockNumberWithMissingTime(): LiveData<Long>
@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) AND bts_value >= 0 LIMIT 1")
fun getTransfersWithMissingValueIn(symbol: String): Observable<Transfer>
@Query("DELETE FROM transfers")
fun deleteAll()
}

View File

@ -17,5 +17,18 @@ data class Transfer (
@ColumnInfo(name = "transfer_amount") val transferAmount: Long,
@ColumnInfo(name = "transfer_asset_id") val transferAssetId: String, // TODO should be foreign key to Asset
@ColumnInfo(name = "memo") val memo: String,
@ColumnInfo(name = "bts_value") val btsValue: Long? = -1
)
@ColumnInfo(name = "bts_value") var btsValue: Long? = Transfer.NOT_CALCULATED
){
companion object {
// Constant used to specify an uninitialized BTS equivalent value
val NOT_CALCULATED: Long? = -1L
// Constant used to specify a BTS equivalent value whose calculation returned an error
val ERROR: Long? = -2L
}
init {
if(transferAssetId.equals("1.3.0")){
// If the transferred asset is BTS, we can fill the btsValue field immediately
btsValue = transferAmount
}
}
}

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

@ -3,8 +3,8 @@ package cy.agorise.bitsybitshareswallet.repositories
import android.content.Context
import android.os.AsyncTask
import androidx.lifecycle.LiveData
import cy.agorise.bitsybitshareswallet.database.daos.AssetDao
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.daos.AssetDao
import cy.agorise.bitsybitshareswallet.database.entities.Asset
class AssetRepository internal constructor(context: Context) {
@ -24,6 +24,10 @@ class AssetRepository internal constructor(context: Context) {
insertAllAsyncTask(mAssetDao).execute(assets)
}
fun getAssetDetails(assetId: String): Asset {
return mAssetDao.getAssetDetails(assetId)
}
private class insertAllAsyncTask internal constructor(private val mAsyncTaskDao: AssetDao) :
AsyncTask<List<Asset>, Void, Void>() {

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,25 +2,42 @@ 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>) {
insertAllAsyncTask(mTransferDao).execute(transfers)
}
fun update(transfer: Transfer){
mTransferDao.insert(transfer)
}
fun setBlockTime(blockNumber: Long, timestamp: Long) {
setBlockTimeAsyncTask(mTransferDao).execute(Pair(blockNumber, timestamp))
}
@ -37,10 +54,71 @@ class TransferRepository internal constructor(context: Context) {
return mTransferDao.getTransferBlockNumberWithMissingTime()
}
fun getTransfersWithMissingBtsValue(): LiveData<Transfer> {
return mTransferDao.getTransfersWithMissingBtsValue()
}
fun deleteAll() {
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>() {
@ -67,4 +145,16 @@ 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 subscription is no longer necessary and the resources can
* be cleared.
*/
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

@ -3,16 +3,48 @@ package cy.agorise.bitsybitshareswallet.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import com.google.common.primitives.UnsignedLong
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
import cy.agorise.bitsybitshareswallet.repositories.AssetRepository
import cy.agorise.bitsybitshareswallet.repositories.TransferRepository
import cy.agorise.graphenej.Asset
import cy.agorise.graphenej.AssetAmount
import cy.agorise.graphenej.Converter
import cy.agorise.graphenej.models.BucketObject
class TransferViewModel(application: Application) : AndroidViewModel(application) {
private var mRepository = TransferRepository(application)
private val TAG = "TransferViewModel"
private var mTransferRepository = TransferRepository(application)
private var mAssetRepository = AssetRepository(application)
internal fun setBlockTime(blockNumber: Long, timestamp: Long) {
mRepository.setBlockTime(blockNumber, timestamp)
mTransferRepository.setBlockTime(blockNumber, timestamp)
}
internal fun getTransferBlockNumberWithMissingTime(): LiveData<Long> {
return mRepository.getTransferBlockNumberWithMissingTime()
return mTransferRepository.getTransferBlockNumberWithMissingTime()
}
}
fun getTransfersWithMissingBtsValue() : LiveData<Transfer> {
return mTransferRepository.getTransfersWithMissingBtsValue()
}
fun updateBtsValue(transfer: Transfer, bucket: BucketObject) {
val base = mAssetRepository.getAssetDetails(bucket.key.base.objectId) // Always BTS ?
val quote = mAssetRepository.getAssetDetails(bucket.key.quote.objectId) // Any asset other than BTS
val converter = Converter(Asset(base.id, base.symbol, base.precision), Asset(quote.id, quote.symbol, quote.precision), bucket)
// The "base" amount is always the amount we have, and the quote would be the amount we want to obtain.
// It can be strange that the second argument of the AssetAmount constructor here we pass the quote.id, quote.symbol and quote.precision
// when building the "base" amount instance. But this is just because the full node will apparently always treat BTS as the base.
val baseAmount = AssetAmount(UnsignedLong.valueOf(transfer.transferAmount), Asset(quote.id, quote.symbol, quote.precision))
val quoteAmount = converter.convert(baseAmount, Converter.OPEN_VALUE)
transfer.btsValue = quoteAmount
mTransferRepository.update(transfer)
}
fun updateBtsValue(transfer: Transfer, value: Long?) {
transfer.btsValue = value
mTransferRepository.update(transfer)
}
}

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"])
}
}