- A LiveData wrapped query looking for entries of the 'transfer' table that lack a BTS value now automatically trigger a GetMarketHistory network call

- The network response is parsed and a BTS equivalent value is stored in the database
This commit is contained in:
Nelson R. Perez 2019-02-03 15:08:32 -05:00
parent 771aed9429
commit 85846b6c75
8 changed files with 172 additions and 33 deletions

View file

@ -23,8 +23,22 @@ class TransfersTests {
@Before @Before
fun createDb() { fun createDb() {
Log.d(TAG,"createDb")
db = Room.inMemoryDatabaseBuilder(context, BitsyDatabase::class.java).build() 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 // 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 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","") val t2 = Transfer("1.11.684483739", 33890367, 1547171166, 11030, "1.3.0", "1.2.139293","1.2.1029856",98,"1.3.120","")
@ -36,12 +50,6 @@ class TransfersTests {
db.equivalentValueDao().insert(equivalentValue) db.equivalentValueDao().insert(equivalentValue)
} }
@After
@Throws(IOException::class)
fun closeDb(){
db.close()
}
/** /**
* This test makes use of the LiveData Testing library and its objective is to prove that * 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 * the TransferDao#getTransfersWithMissingValueIn(symbol: String) will return only the
@ -52,6 +60,7 @@ class TransfersTests {
*/ */
@Test @Test
fun testGetTransfersMissingEquivalentValues(){ fun testGetTransfersMissingEquivalentValues(){
prepareMissingEquivalentValues()
db.transferDao() db.transferDao()
.getTransfersWithMissingValueIn("usd") .getTransfersWithMissingValueIn("usd")
.test() .test()
@ -71,6 +80,7 @@ class TransfersTests {
*/ */
@Test @Test
fun testGetTransfersMissingEquivalentValues2(){ fun testGetTransfersMissingEquivalentValues2(){
prepareMissingEquivalentValues()
val transfers: List<Transfer> = LiveDataTestUtil.getValue(db.transferDao().getTransfersWithMissingValueIn("usd")) val transfers: List<Transfer> = LiveDataTestUtil.getValue(db.transferDao().getTransfersWithMissingValueIn("usd"))
Assert.assertNotNull(transfers) Assert.assertNotNull(transfers)
Assert.assertEquals(1, transfers.size) Assert.assertEquals(1, transfers.size)
@ -78,4 +88,34 @@ class TransfersTests {
Assert.assertEquals(33890367, transfers[0].blockNumber) Assert.assertEquals(33890367, transfers[0].blockNumber)
Log.d(TAG, "transfer ${transfers[0]}"); 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.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.AsyncTask
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.IBinder import android.os.IBinder
@ -17,6 +18,7 @@ import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore import com.crashlytics.android.core.CrashlyticsCore
import cy.agorise.bitsybitshareswallet.BuildConfig import cy.agorise.bitsybitshareswallet.BuildConfig
import cy.agorise.bitsybitshareswallet.database.entities.Balance import cy.agorise.bitsybitshareswallet.database.entities.Balance
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
import cy.agorise.bitsybitshareswallet.processors.TransfersLoader import cy.agorise.bitsybitshareswallet.processors.TransfersLoader
import cy.agorise.bitsybitshareswallet.repositories.AssetRepository import cy.agorise.bitsybitshareswallet.repositories.AssetRepository
import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Constants
@ -31,16 +33,16 @@ import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.android.NetworkService import cy.agorise.graphenej.api.android.NetworkService
import cy.agorise.graphenej.api.android.RxBus import cy.agorise.graphenej.api.android.RxBus
import cy.agorise.graphenej.api.calls.* import cy.agorise.graphenej.api.calls.*
import cy.agorise.graphenej.models.AccountProperties import cy.agorise.graphenej.models.*
import cy.agorise.graphenej.models.BlockHeader
import cy.agorise.graphenej.models.FullAccountDetails
import cy.agorise.graphenej.models.JsonRpcResponse
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers 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.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
@ -59,6 +61,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private const val RESPONSE_GET_ACCOUNT_BALANCES = 3 private const val RESPONSE_GET_ACCOUNT_BALANCES = 3
private const val RESPONSE_GET_ASSETS = 4 private const val RESPONSE_GET_ASSETS = 4
private const val RESPONSE_GET_BLOCK_HEADER = 5 private const val RESPONSE_GET_BLOCK_HEADER = 5
private const val RESPONSE_GET_MARKET_HISTORY = 6
} }
private lateinit var mUserAccountViewModel: UserAccountViewModel private lateinit var mUserAccountViewModel: UserAccountViewModel
@ -72,8 +75,8 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private val mHandler = Handler() private val mHandler = Handler()
// Disposable returned at the bus subscription // Composite disposable used to clear all disposables once the activity is destroyed
private var mDisposable: Disposable? = null private val mCompositeDisposable = CompositeDisposable()
private var storedOpCount: Long = -1 private var storedOpCount: Long = -1
@ -91,6 +94,10 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private var blockNumberWithMissingTime = 0L 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 * Flag used to keep track of the NetworkService binding state
*/ */
@ -145,7 +152,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
} }
}) })
mDisposable = RxBus.getBusInstance() val disposable = RxBus.getBusInstance()
.asFlowable() .asFlowable()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ .subscribe({
@ -153,6 +160,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
}, { }, {
this.handleError(it) this.handleError(it)
}) })
mCompositeDisposable.add(disposable)
} }
/** /**
@ -203,6 +211,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
handleBlockHeader(message.result as BlockHeader, blockNumber) handleBlockHeader(message.result as BlockHeader, blockNumber)
requestIdToBlockNumberMap.remove(message.id) requestIdToBlockNumberMap.remove(message.id)
} }
RESPONSE_GET_MARKET_HISTORY -> handleMarketData(message.result as List<BucketObject>)
} }
responseMap.remove(message.id) responseMap.remove(message.id)
} }
@ -227,12 +236,33 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
} else if (message.updateCode == ConnectionStatusUpdate.API_UPDATE) { } else if (message.updateCode == ConnectionStatusUpdate.API_UPDATE) {
// If we got an API update // If we got an API update
if(message.api == ApiAccess.API_HISTORY) { if(message.api == ApiAccess.API_HISTORY) {
//TODO: Start the procedure that will obtain the missing equivalent values // 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){
Log.d(TAG,"Transfer: ${transfer}")
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. * Method called whenever a response to the 'get_full_accounts' API call has been detected.
* @param accountDetails De-serialized account details object * @param accountDetails De-serialized account details object
@ -281,7 +311,6 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
} }
private fun handleBalanceUpdate(assetAmountList: List<AssetAmount>) { private fun handleBalanceUpdate(assetAmountList: List<AssetAmount>) {
Log.d(TAG, "handleBalanceUpdate")
val now = System.currentTimeMillis() / 1000 val now = System.currentTimeMillis() / 1000
val balances = ArrayList<Balance>() val balances = ArrayList<Balance>()
for (assetAmount in assetAmountList) { for (assetAmount in assetAmountList) {
@ -336,6 +365,23 @@ 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}")
})
mCompositeDisposable.add(disposable)
}else{
Log.i(TAG,"handleMarketData. Bucket IS empty")
AsyncTask.execute { mTransferViewModel.updateBtsValue(transfer!!, Transfer.ERROR) }
}
}
private fun updateBalances() { private fun updateBalances() {
if (mNetworkService?.isConnected == true) { if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetAccountBalances(mCurrentAccount, ArrayList()), val id = mNetworkService!!.sendMessage(GetAccountBalances(mCurrentAccount, ArrayList()),
@ -450,6 +496,6 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
override fun onDestroy() { override fun onDestroy() {
super.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") @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>> fun getAllNonZero(): LiveData<List<Asset>>
@Query("SELECT * FROM assets WHERE id = :assetId")
fun getAssetDetails(assetId: String): Asset
} }

View file

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

View file

@ -17,5 +17,12 @@ data class Transfer (
@ColumnInfo(name = "transfer_amount") val transferAmount: Long, @ColumnInfo(name = "transfer_amount") val transferAmount: Long,
@ColumnInfo(name = "transfer_asset_id") val transferAssetId: String, // TODO should be foreign key to Asset @ColumnInfo(name = "transfer_asset_id") val transferAssetId: String, // TODO should be foreign key to Asset
@ColumnInfo(name = "memo") val memo: String, @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
}
}

View file

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

View file

@ -21,6 +21,10 @@ class TransferRepository internal constructor(context: Context) {
insertAllAsyncTask(mTransferDao).execute(transfers) insertAllAsyncTask(mTransferDao).execute(transfers)
} }
fun update(transfer: Transfer){
mTransferDao.insert(transfer)
}
fun setBlockTime(blockNumber: Long, timestamp: Long) { fun setBlockTime(blockNumber: Long, timestamp: Long) {
setBlockTimeAsyncTask(mTransferDao).execute(Pair(blockNumber, timestamp)) setBlockTimeAsyncTask(mTransferDao).execute(Pair(blockNumber, timestamp))
} }
@ -41,6 +45,10 @@ class TransferRepository internal constructor(context: Context) {
return mTransferDao.getTransfersWithMissingValueIn(symbol) return mTransferDao.getTransfersWithMissingValueIn(symbol)
} }
fun getTransfersWithMissingBtsValue(): LiveData<Transfer> {
return mTransferDao.getTransfersWithMissingBtsValue()
}
fun deleteAll() { fun deleteAll() {
deleteAllAsyncTask(mTransferDao).execute() deleteAllAsyncTask(mTransferDao).execute()
} }

View file

@ -4,24 +4,52 @@ import android.app.Application
import android.util.Log import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData 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.bitsybitshareswallet.repositories.TransferRepository
import io.reactivex.Observable import cy.agorise.graphenej.Asset
import io.reactivex.functions.Function import cy.agorise.graphenej.AssetAmount
import io.reactivex.schedulers.Schedulers import cy.agorise.graphenej.Converter
import cy.agorise.graphenej.models.BucketObject
class TransferViewModel(application: Application) : AndroidViewModel(application) { class TransferViewModel(application: Application) : AndroidViewModel(application) {
private val TAG = "TransferViewModel" private val TAG = "TransferViewModel"
private var mRepository = TransferRepository(application) private var mTransferRepository = TransferRepository(application)
private var mAssetRepository = AssetRepository(application)
internal fun setBlockTime(blockNumber: Long, timestamp: Long) { internal fun setBlockTime(blockNumber: Long, timestamp: Long) {
mRepository.setBlockTime(blockNumber, timestamp) mTransferRepository.setBlockTime(blockNumber, timestamp)
} }
internal fun getTransferBlockNumberWithMissingTime(): LiveData<Long> { internal fun getTransferBlockNumberWithMissingTime(): LiveData<Long> {
return mRepository.getTransferBlockNumberWithMissingTime() return mTransferRepository.getTransferBlockNumberWithMissingTime()
} }
fun getTransfersWithMissingValueIn(symbol: String) { fun getTransfersWithMissingValueIn(symbol: String) : LiveData<List<Transfer>>{
mRepository.getTransfersWithMissingValueIn(symbol) return mTransferRepository.getTransfersWithMissingValueIn(symbol)
}
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)
} }
} }