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.
This commit is contained in:
parent
b077de95ac
commit
765ed13a6a
11 changed files with 209 additions and 38 deletions
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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>>
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 ///////////////////////
|
||||
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue