Add the nodes WS to Bitsy.

- Renamed the MerchantsWebservice to BitsyWebservice because it is now going to serve as a source of information of more than just merchants and tellers, but also the nodes the app is gonna connect to.
- Created new NodeRepository which will be in charge of accessing and updating the nodes database table with the information obtained from the webservice.
- Bitsy is now going to try to obtain the list of nodes it is going to try to connect to from the dabatase and use a hardcoded list as a fallback. The list of nodes in the database is updated regularly as well as their respective latency, so that in future app's startups it can use those latencies to immediately connect to the best node in the last app session.
master
Severiano Jaramillo 2019-08-23 14:41:00 -05:00
parent b077de95ac
commit 765ed13a6a
11 changed files with 209 additions and 38 deletions

View File

@ -99,11 +99,11 @@ dependencies {
implementation "com.jakewharton.rxbinding3:rxbinding-material:$rx_bindings_version" // Material Components widgets
implementation "com.jakewharton.rxbinding3:rxbinding-appcompat:$rx_bindings_version" // AndroidX appcompat widgets
// Retrofit & OkHttp
implementation 'com.squareup.okhttp3:okhttp:3.12.2'
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
implementation 'com.squareup.okhttp3:okhttp:3.12.2'
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.2'
implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
//Firebase
implementation 'com.google.firebase:firebase-core:17.1.0'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'

View File

@ -35,6 +35,7 @@ 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.*
import cy.agorise.graphenej.network.FullNode
import io.fabric.sdk.android.Fabric
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
@ -279,7 +280,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
if (latestOpCount == 0L) {
Log.d(TAG, "The node returned 0 total_ops for current account and may not have installed the history plugin. " +
"\nAsk the NetworkService to remove the node from the list and connect to another one.")
mNetworkService?.removeCurrentNodeAndReconnect()
mNetworkService?.reconnectNode()
} else if (storedOpCount == -1L) {
// Initial case when the app starts
storedOpCount = latestOpCount
@ -478,10 +479,15 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
override fun onPause() {
super.onPause()
mNetworkService?.nodeLatencyVerifier?.nodeList?.let { nodes ->
mConnectedActivityViewModel.updateNodeLatencies(nodes as List<FullNode>)
}
// Unbinding from network service
if (mShouldUnbindNetwork) {
unbindService(this)
mShouldUnbindNetwork = false
mNetworkService = null
}
mHandler.removeCallbacks(mCheckMissingPaymentsTask)
mHandler.removeCallbacks(mRequestMissingUserAccountsTask)

View File

@ -0,0 +1,8 @@
package cy.agorise.bitsybitshareswallet.models
/**
* Node object used to deserialize the response from the WebService.
*/
data class NodeWS(
val url: String
)

View File

@ -2,11 +2,13 @@ package cy.agorise.bitsybitshareswallet.network
import cy.agorise.bitsybitshareswallet.database.entities.Merchant
import cy.agorise.bitsybitshareswallet.database.entities.Teller
import cy.agorise.bitsybitshareswallet.models.NodeWS
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query
interface MerchantsWebservice {
interface BitsyWebservice {
@GET("/api/v2/merchants")
fun getMerchants(@Query(value = "\$skip") skip: Int,
@ -17,4 +19,7 @@ interface MerchantsWebservice {
fun getTellers(@Query(value = "\$skip") skip: Int,
@Query(value = "\$limit") limit: Int = 50):
Call<FeathersResponse<Teller>>
@GET("/api/v2/nodes")
suspend fun getNodes(): Response<List<NodeWS>>
}

View File

@ -9,7 +9,7 @@ import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.daos.MerchantDao
import cy.agorise.bitsybitshareswallet.database.entities.Merchant
import cy.agorise.bitsybitshareswallet.network.FeathersResponse
import cy.agorise.bitsybitshareswallet.network.MerchantsWebservice
import cy.agorise.bitsybitshareswallet.network.BitsyWebservice
import cy.agorise.bitsybitshareswallet.utils.Constants
import io.reactivex.Single
import retrofit2.Call
@ -47,12 +47,12 @@ class MerchantRepository internal constructor(val context: Context) : retrofit2.
Log.d(TAG, "Updating merchants from webservice")
// TODO make sure it works when there are more merchants than those sent back in the first response
val retrofit = Retrofit.Builder()
.baseUrl(Constants.MERCHANTS_WEBSERVICE_URL)
.baseUrl(Constants.BITSY_WEBSERVICE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val ambassadorService = retrofit.create<MerchantsWebservice>(MerchantsWebservice::class.java)
val call = ambassadorService.getMerchants(0)
val bitsyWebservice = retrofit.create<BitsyWebservice>(BitsyWebservice::class.java)
val call = bitsyWebservice.getMerchants(0)
call.enqueue(this)
}
}

View File

@ -1,7 +1,130 @@
package cy.agorise.bitsybitshareswallet.repositories
import android.os.AsyncTask
import android.util.Log
import com.crashlytics.android.Crashlytics
import cy.agorise.bitsybitshareswallet.database.daos.NodeDao
import cy.agorise.bitsybitshareswallet.database.entities.Node
import cy.agorise.bitsybitshareswallet.network.BitsyWebservice
import cy.agorise.bitsybitshareswallet.network.ServiceGenerator
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.graphenej.network.FullNode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class NodeRepository(private val nodeDao: NodeDao) {
companion object {
private const val TAG = "NodeRepository"
// Minimum number of nodes required to update the nodes db table.
private const val MIN_NODES_SIZE = 3
// List of BitShares nodes the app will try to connect to
var BITSHARES_NODE_URLS = arrayOf(
// PP private nodes
"wss://nl.palmpay.io/ws",
// Other public nodes
"wss://btsws.roelandp.nl/ws",
"wss://api.bts.mobi/ws",
"wss://kimziv.com/ws",
"wss://api.bts.ai")
}
private val mBitsyWebservice: BitsyWebservice
init {
val sg = ServiceGenerator(Constants.BITSY_WEBSERVICE_URL)
mBitsyWebservice = sg.getService(BitsyWebservice::class.java)
}
/**
* Returns a Pair of items:
* First. A list of comma separated node urls in form of a string. The node urls come from the
* database if the nodes table is already populated, else a default list is used.
* Second. A Boolean that specifies if the app should try to autoConnect immediately, or wait
* for other event to launch the connect method.
*/
suspend fun getFormattedNodes(): Pair<String, Boolean> {
val nodes = nodeDao.getSortedNodes()
// TODO verify if this is the best way to fire and forget launch a coroutine inside another coroutine
// Launches a job to refresh the list of nodes into the database, without blocking the
// execution of this function, so that the formatted nodes can be returned immediately
// without waiting until the nodes have been updated in the database.
CoroutineScope(Dispatchers.Default).launch {
refreshNodes(nodes)
}
return if (nodes.size < MIN_NODES_SIZE) {
// If the nodes db table is empty or very small, it could mean that the nodes have not
// still been updated from the webservice, thus returning a default list of nodes as a fallback.
// False is returned since we want to verify the node latencies before choosing the best
// one and trying to connect to it.
Pair(getDefaultFormattedNodes(), false)
} else {
// Use the list of nodes stored in the database. True is returned since the list of nodes
// is already ordered by latency, and we don't need to wait to obtain the latency
// readings, thus the app can immediately try to connect to the first node in the list.
Pair(getDBFormattedNodes(nodes), true)
}
}
/**
* Verifies if the nodes information should be updated and if true, fetches the nodes
* information from the webservice and updates the database
*/
private suspend fun refreshNodes(nodes: List<Node>) {
val now = System.currentTimeMillis() / 1000
val lastUpdate: Long = if (nodes.size < MIN_NODES_SIZE) {
0
} else {
nodes[0].lastUpdate
}
val updatePeriod = Constants.NODES_UPDATE_PERIOD
// Verify if nodes list should be updated
if (now - updatePeriod > lastUpdate) {
val response = mBitsyWebservice.getNodes()
try {
// Update the list of nodes only if we got at least MIN_NODES_SIZE nodes
if (response.isSuccessful && (response.body()?.size ?: 0) >= MIN_NODES_SIZE) {
val nodesWS = response.body() ?: return
val nodesDB = nodesWS.map {
Node(url = it.url, lastUpdate = now)
}
Log.d(TAG, "Updating the list of nodes.")
nodeDao.updateNodes(nodesDB, now)
}
} catch (e: Exception) {
// Generic exception handling
Crashlytics.logException(e)
}
}
}
private fun getDefaultFormattedNodes(): String {
return BITSHARES_NODE_URLS.joinToString(separator = ",")
}
private fun getDBFormattedNodes(nodes: List<Node>): String {
return nodes.joinToString(separator = ",") { it.url }
}
/**
* Function that will receive an up-to-date list of FullNode instances and persist it on
* the database.
*
* @param nodes List of nodes with fresh latency measurements.
*/
fun updateNodeLatencies(nodes: List<FullNode>) {
AsyncTask.execute {
nodes.forEach {
nodeDao.updateLatency(it.latencyValue.toLong(), it.url)
}
}
}
}

View File

@ -9,7 +9,7 @@ import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.daos.TellerDao
import cy.agorise.bitsybitshareswallet.database.entities.Teller
import cy.agorise.bitsybitshareswallet.network.FeathersResponse
import cy.agorise.bitsybitshareswallet.network.MerchantsWebservice
import cy.agorise.bitsybitshareswallet.network.BitsyWebservice
import cy.agorise.bitsybitshareswallet.utils.Constants
import io.reactivex.Single
import retrofit2.Call
@ -47,12 +47,12 @@ class TellerRepository internal constructor(val context: Context) : retrofit2.Ca
Log.d(TAG, "Updating tellers from webservice")
// TODO make sure it works when there are more tellers than those sent back in the first response
val retrofit = Retrofit.Builder()
.baseUrl(Constants.MERCHANTS_WEBSERVICE_URL)
.baseUrl(Constants.BITSY_WEBSERVICE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val ambassadorService = retrofit.create<MerchantsWebservice>(MerchantsWebservice::class.java)
val call = ambassadorService.getTellers(0)
val bitsyWebservice = retrofit.create<BitsyWebservice>(BitsyWebservice::class.java)
val call = bitsyWebservice.getTellers(0)
call.enqueue(this)
}
}

View File

@ -2,29 +2,30 @@ package cy.agorise.bitsybitshareswallet.utils
import android.app.Application
import com.crashlytics.android.Crashlytics
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.repositories.NodeRepository
import cy.agorise.graphenej.api.ApiAccess
import cy.agorise.graphenej.api.android.NetworkServiceManager
import io.reactivex.plugins.RxJavaPlugins
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@Suppress("unused")
class BitsyApplication : Application() {
companion object {
private val BITSHARES_NODE_URLS = arrayOf(
// PP private nodes
"wss://nl.palmpay.io/ws",
/**
* Coroutine Job used to create appScope and safely cancel all coroutines launched using it.
*/
private val applicationJob = Job()
// Other public nodes
"wss://kc-us-dex.xeldal.com/ws", // missouri, usa
// "wss://bitshares.nu/ws", // Stockholm, Sweden
"wss://bitshares.openledger.info/ws" // Openledger node
// "wss://dallas.bitshares.apasia.tech/ws", // Dallas, USA
// "wss://atlanta.bitshares.apasia.tech/ws", // Atlanta, USA
// "wss://dex.rnglab.org", // Amsterdam, Netherlands
// "wss://citadel.li/node"
)
}
/**
* Application level scope used to launch coroutines not tied to ViewModels or Activities/Fragments.
*/
lateinit var appScope: CoroutineScope
private lateinit var mNodeRepository: NodeRepository
override fun onCreate() {
super.onCreate()
@ -33,15 +34,28 @@ class BitsyApplication : Application() {
// exception to Crashlytics so that we can fix the issues
RxJavaPlugins.setErrorHandler { throwable -> Crashlytics.logException(throwable)}
appScope = CoroutineScope(Dispatchers.Main + applicationJob)
val nodeDao = BitsyDatabase.getDatabase(applicationContext)!!.nodeDao()
mNodeRepository = NodeRepository(nodeDao)
appScope.launch {
startNetworkServiceConnection()
}
}
private suspend fun startNetworkServiceConnection() {
// Specifying some important information regarding the connection, such as the
// credentials and the requested API accesses
val requestedApis = ApiAccess.API_DATABASE or ApiAccess.API_HISTORY or ApiAccess.API_NETWORK_BROADCAST
val (nodes, autoConnect) = mNodeRepository.getFormattedNodes()
val networkManager = NetworkServiceManager.Builder()
.setUserName("")
.setPassword("")
.setRequestedApis(requestedApis)
.setCustomNodeUrls(setupNodes())
.setAutoConnect(true)
.setCustomNodeUrls(nodes)
.setAutoConnect(autoConnect)
.setNodeLatencyVerification(true)
.build(this)
@ -52,12 +66,10 @@ class BitsyApplication : Application() {
registerActivityLifecycleCallbacks(networkManager)
}
private fun setupNodes(): String {
val stringBuilder = StringBuilder()
for (url in BITSHARES_NODE_URLS) {
stringBuilder.append(url).append(",")
}
stringBuilder.replace(stringBuilder.length - 1, stringBuilder.length, "")
return stringBuilder.toString()
override fun onTerminate() {
super.onTerminate()
// Cancel the job which also cancels the scopes created using it, i.e. appScope
applicationJob.cancel()
}
}

View File

@ -108,7 +108,7 @@ object Constants {
/** Key used to store the night mode setting into the shared preferences */
const val KEY_NIGHT_MODE_ACTIVATED = "key_night_mode_activated"
const val MERCHANTS_WEBSERVICE_URL = "https://websvc.palmpay.io/"
const val BITSY_WEBSERVICE_URL = "https://websvc.palmpay.io/"
/** Key used to store the last time in millis that the merchants info was refreshed */
const val KEY_MERCHANTS_LAST_UPDATE = "key_merchants_last_update"
@ -125,6 +125,9 @@ object Constants {
/** Constant used to check if the current connected node is out of sync */
const val CHECK_NODE_OUT_OF_SYNC = 10 // 10 seconds
/** Minimum time period in seconds between BitShares nodes list updates */
const val NODES_UPDATE_PERIOD = (60 * 60).toLong() // 1 hour
/////////////////////// Crashlytics custom keys ///////////////////////

View File

@ -2,15 +2,29 @@ package cy.agorise.bitsybitshareswallet.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.repositories.NodeRepository
import cy.agorise.bitsybitshareswallet.repositories.TransferRepository
import cy.agorise.graphenej.network.FullNode
class ConnectedActivityViewModel(application: Application) : AndroidViewModel(application) {
private var mTransfersRepository = TransferRepository(application)
private val mNodeRepository: NodeRepository
init {
val nodeDao = BitsyDatabase.getDatabase(application)!!.nodeDao()
mNodeRepository = NodeRepository(nodeDao)
}
fun observeMissingEquivalentValuesIn(symbol: String) {
mTransfersRepository.observeMissingEquivalentValuesIn(symbol)
}
fun updateNodeLatencies(nodes: List<FullNode>) {
mNodeRepository.updateNodeLatencies(nodes)
}
override fun onCleared() {
super.onCleared()
mTransfersRepository.onCleared()

@ -1 +1 @@
Subproject commit 954cf3e16d77038feff711ed3b93818470e36b15
Subproject commit 606f7c183e170285d45f6977faba435966dac4e9