Merge branch 'feat_equiv_values' into develop
This commit is contained in:
commit
577e3ae01f
20 changed files with 519 additions and 34 deletions
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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" }
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>() {
|
||||
|
||||
|
|
|
@ -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,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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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