From 85846b6c75d71897e3b8f829ac8eb0da30a4e546 Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Sun, 3 Feb 2019 15:08:32 -0500 Subject: [PATCH] - 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 --- .../bitsybitshareswallet/TransfersTests.kt | 52 ++++++++++++-- .../activities/ConnectedActivity.kt | 68 ++++++++++++++++--- .../database/daos/AssetDao.kt | 3 + .../database/daos/TransferDao.kt | 13 ++-- .../database/entities/Transfer.kt | 11 ++- .../repositories/AssetRepository.kt | 6 +- .../repositories/TransferRepository.kt | 8 +++ .../viewmodels/TransferViewModel.kt | 44 +++++++++--- 8 files changed, 172 insertions(+), 33 deletions(-) diff --git a/app/src/androidTest/java/cy/agorise/bitsybitshareswallet/TransfersTests.kt b/app/src/androidTest/java/cy/agorise/bitsybitshareswallet/TransfersTests.kt index f7bf939..607f93a 100644 --- a/app/src/androidTest/java/cy/agorise/bitsybitshareswallet/TransfersTests.kt +++ b/app/src/androidTest/java/cy/agorise/bitsybitshareswallet/TransfersTests.kt @@ -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 = 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" } + } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt index ed01a6a..6273de2 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt @@ -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) } 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 { + 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) { - Log.d(TAG, "handleBalanceUpdate") val now = System.currentTimeMillis() / 1000 val balances = ArrayList() for (assetAmount in assetAmountList) { @@ -336,6 +365,23 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection { } } + private fun handleMarketData(buckets: List) { + 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() } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/AssetDao.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/AssetDao.kt index 275f90f..6cc90c1 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/AssetDao.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/AssetDao.kt @@ -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> + + @Query("SELECT * FROM assets WHERE id = :assetId") + fun getAssetDetails(assetId: String): Asset } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/TransferDao.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/TransferDao.kt index eb7985e..2ef9c7e 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/TransferDao.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/TransferDao.kt @@ -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) @@ -29,6 +29,9 @@ interface TransferDao { @Query("SELECT block_number FROM transfers WHERE timestamp='0' LIMIT 1") fun getTransferBlockNumberWithMissingTime(): LiveData + @Query("SELECT * FROM transfers WHERE timestamp != 0 AND bts_value = -1 AND transfer_asset_id != '1.3.0' LIMIT 1") + fun getTransfersWithMissingBtsValue(): LiveData + @Query("SELECT * FROM transfers WHERE id NOT IN (SELECT transfer_id FROM equivalent_values WHERE symbol = :symbol)") fun getTransfersWithMissingValueIn(symbol: String): LiveData> diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/entities/Transfer.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/entities/Transfer.kt index cce1ae6..37f9e4a 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/entities/Transfer.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/entities/Transfer.kt @@ -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 - ) \ No newline at end of file + @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 + } +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AssetRepository.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AssetRepository.kt index 8ddd294..09f8624 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AssetRepository.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AssetRepository.kt @@ -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, Void, Void>() { diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/TransferRepository.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/TransferRepository.kt index 7b70063..a0918e6 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/TransferRepository.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/TransferRepository.kt @@ -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 { + return mTransferDao.getTransfersWithMissingBtsValue() + } + fun deleteAll() { deleteAllAsyncTask(mTransferDao).execute() } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransferViewModel.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransferViewModel.kt index 16e959f..07d90f8 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/TransferViewModel.kt @@ -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 { - return mRepository.getTransferBlockNumberWithMissingTime() + return mTransferRepository.getTransferBlockNumberWithMissingTime() } - fun getTransfersWithMissingValueIn(symbol: String) { - mRepository.getTransfersWithMissingValueIn(symbol) + fun getTransfersWithMissingValueIn(symbol: String) : LiveData>{ + return mTransferRepository.getTransfersWithMissingValueIn(symbol) + } + + fun getTransfersWithMissingBtsValue() : LiveData { + 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) } }