- 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
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","")
@ -36,12 +50,6 @@ class TransfersTests {
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
* the TransferDao#getTransfersWithMissingValueIn(symbol: String) will return only the
@ -52,6 +60,7 @@ class TransfersTests {
*/
@Test
fun testGetTransfersMissingEquivalentValues(){
prepareMissingEquivalentValues()
db.transferDao()
.getTransfersWithMissingValueIn("usd")
.test()
@ -71,6 +80,7 @@ class TransfersTests {
*/
@Test
fun testGetTransfersMissingEquivalentValues2(){
prepareMissingEquivalentValues()
val transfers: List<Transfer> = LiveDataTestUtil.getValue(db.transferDao().getTransfersWithMissingValueIn("usd"))
Assert.assertNotNull(transfers)
Assert.assertEquals(1, transfers.size)
@ -78,4 +88,34 @@ class TransfersTests {
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,6 +18,7 @@ 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
@ -31,16 +33,16 @@ 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
@ -59,6 +61,7 @@ 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
@ -72,8 +75,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
@ -91,6 +94,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
*/
@ -145,7 +152,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
}
})
mDisposable = RxBus.getBusInstance()
val disposable = RxBus.getBusInstance()
.asFlowable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
@ -153,6 +160,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
}, {
this.handleError(it)
})
mCompositeDisposable.add(disposable)
}
/**
@ -203,6 +211,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)
}
@ -227,12 +236,33 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
} else if (message.updateCode == ConnectionStatusUpdate.API_UPDATE) {
// If we got an API update
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.
* @param accountDetails De-serialized account details object
@ -281,7 +311,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) {
@ -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() {
if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetAccountBalances(mCurrentAccount, ArrayList()),
@ -450,6 +496,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,18 @@
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.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 +29,9 @@ 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)")
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_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
}
}

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

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

View file

@ -4,24 +4,52 @@ import android.app.Application
import android.util.Log
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 io.reactivex.Observable
import io.reactivex.functions.Function
import io.reactivex.schedulers.Schedulers
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 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) {
mRepository.setBlockTime(blockNumber, timestamp)
mTransferRepository.setBlockTime(blockNumber, timestamp)
}
internal fun getTransferBlockNumberWithMissingTime(): LiveData<Long> {
return mRepository.getTransferBlockNumberWithMissingTime()
return mTransferRepository.getTransferBlockNumberWithMissingTime()
}
fun getTransfersWithMissingValueIn(symbol: String) {
mRepository.getTransfersWithMissingValueIn(symbol)
fun getTransfersWithMissingValueIn(symbol: String) : LiveData<List<Transfer>>{
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)
}
}