Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,9 @@ dependencies {
implementation(platform(libs.koin.bom))
implementation(libs.bundles.koin)

implementation(libs.squareup.okhttp)
implementation(platform(libs.squareup.okhttp.bom))
implementation(libs.bundles.squareup.okhttp)
implementation(libs.bundles.squareup.retrofit)
implementation(libs.jakewarthon.timber)
implementation(libs.commons.io)
implementation(libs.bundles.eclipse.jetty)
Expand Down
25 changes: 0 additions & 25 deletions app/src/main/java/com/brainwallet/data/model/CurrencyEntity.java

This file was deleted.

42 changes: 42 additions & 0 deletions app/src/main/java/com/brainwallet/data/model/CurrencyEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.brainwallet.data.model

import kotlinx.serialization.SerialName
import java.io.Serializable

@kotlinx.serialization.Serializable
data class CurrencyEntity(
@JvmField
var code: String ="",
@JvmField
var name: String = "",
@JvmField
@SerialName("n")
var rate: Float = 0F,
@JvmField
var symbol: String = ""
) : Serializable {
// @JvmField
// var code: String? = null
// @JvmField
// var name: String? = null
// @JvmField
// var rate: Float = 0f
// @JvmField
// var symbol: String? = null
//
// constructor(code: String?, name: String?, rate: Float, symbol: String?) {
// this.code = code
// this.name = name
// this.rate = rate
// this.symbol = symbol
// }
//
// constructor()
//
// companion object {
// //Change this after modifying the class
// private const val serialVersionUID = 7526472295622776147L
//
// val TAG: String = CurrencyEntity::class.java.name
// }
}
40 changes: 40 additions & 0 deletions app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.brainwallet.data.repository

import android.content.Context
import com.brainwallet.data.model.CurrencyEntity
import com.brainwallet.data.source.RemoteApiSource
import com.brainwallet.tools.manager.BRSharedPrefs
import com.brainwallet.tools.manager.FeeManager
import com.brainwallet.tools.sqlite.CurrencyDataSource

interface LtcRepository {
suspend fun fetchRates(): List<CurrencyEntity>
//todo

class Impl(
private val context: Context,
private val remoteApiSource: RemoteApiSource,
private val currencyDataSource: CurrencyDataSource
) : LtcRepository {

//todo: make it offline first here later, currently just using CurrencyDataSource.getAllCurrencies
override suspend fun fetchRates(): List<CurrencyEntity> {
val rates = remoteApiSource.getRates()

//legacy logic
FeeManager.updateFeePerKb(context)
val selectedISO = BRSharedPrefs.getIsoSymbol(context)
rates.forEachIndexed { index, currencyEntity ->
if (currencyEntity.code.equals(selectedISO, ignoreCase = true)) {
BRSharedPrefs.putIso(context, currencyEntity.code)
BRSharedPrefs.putCurrencyListPosition(context, index - 1)
}
}

//save to local
currencyDataSource.putCurrencies(rates)
return rates
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.brainwallet.data.repository

import android.content.SharedPreferences
import androidx.core.content.edit
import com.brainwallet.di.json
import kotlinx.serialization.json.jsonObject
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Response
import okio.IOException
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

interface SelectedPeersRepository {

suspend fun fetchSelectedPeers(): Set<String>

class Impl(
private val okHttpClient: OkHttpClient,
private val sharedPreferences: SharedPreferences,
) : SelectedPeersRepository {

private companion object {
const val PREF_KEY_SELECTED_PEERS = "selected_peers"
const val PREF_KEY_SELECTED_PEERS_CACHED_AT = "${PREF_KEY_SELECTED_PEERS}_cached_at"
}

override suspend fun fetchSelectedPeers(): Set<String> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function will fetch from here https://api.blockchair.com/litecoin/nodes, then if cache didn't expired & not empty then return cachedPeers, if not, will fetch from API then save into local with some filter criteria
currently only filter port 9333

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok @andhikayuana . Please make sure that url can be easily changed to via the remote config a call in the new APIManager. We are very close to making that available.

val lastUpdateTime = sharedPreferences.getLong(PREF_KEY_SELECTED_PEERS_CACHED_AT, 0)
val currentTime = System.currentTimeMillis()
val cachedPeers = sharedPreferences.getStringSet(PREF_KEY_SELECTED_PEERS, null)

// Check if cache exists and is less than 6 hours old
if (!cachedPeers.isNullOrEmpty() && (currentTime - lastUpdateTime) < 6 * 60 * 60 * 1000) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache 6hrs

return cachedPeers
}

val request = okhttp3.Request.Builder()
.url(LITECOIN_NODES_URL)
.build()

return suspendCoroutine { continuation ->
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
continuation.resume(emptySet()) //just return empty if failed or need hardcoded?
}

override fun onResponse(call: Call, response: Response) {
val jsonString = response.body?.string()

val parsedResult = jsonString?.let {
val jsonElement = json.parseToJsonElement(it)
val dataObject = jsonElement.jsonObject["data"]?.jsonObject
val nodesObject = dataObject?.get("nodes")?.jsonObject

//filter criteria
val requiredServices = 0x01 or 0x04 // NODE_NETWORK | NODE_BLOOM
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add filter criteria


nodesObject?.entries
?.filter { entry ->
val flags =
entry.value.jsonObject["flags"]?.toString()?.toIntOrNull()
flags != null && (flags and requiredServices) == requiredServices
}
?.map { it.key.replace(":9333", "") }
?.toSet().also { Timber.d("Total Selected Peers ${it?.size}") }
?: emptySet()

} ?: emptySet()

sharedPreferences.edit {
putStringSet(PREF_KEY_SELECTED_PEERS, parsedResult)
putLong(PREF_KEY_SELECTED_PEERS_CACHED_AT, currentTime)
}

continuation.resume(parsedResult)
}
})
}
}

}

companion object {
const val LITECOIN_NODES_URL = "https://api.blockchair.com/litecoin/nodes"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great for the interim @andhikayuana . My concern is that as we get more users, they will all hit the rate limits. So we are working hard with the backend team to implement an endpoint for Brainwallet mobile clients...issue is here

Then, you can just store the nodes into encrypted preferences

}
}
17 changes: 17 additions & 0 deletions app/src/main/java/com/brainwallet/data/source/RemoteApiSource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.brainwallet.data.source

import com.brainwallet.data.model.CurrencyEntity
import retrofit2.http.GET

//TODO
interface RemoteApiSource {

@GET("api/v1/rates")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very close @andhikayuana .

I will had 'v2' in the url for the backend

suspend fun getRates(): List<CurrencyEntity>

@GET("v1/fee-per-kb")
suspend fun getFeePerKb()

// https://prod.apigsltd.net/moonpay/buy?address=ltc1qjnsg3p9rt4r4vy7ncgvrywdykl0zwhkhcp8ue0&code=USD&idate=1742331930290&uid=ec51fa950b271ff3
// suspend fun getMoonPayBuy()
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ interface RemoteConfigSource {

companion object {
const val KEY_FEATURE_MENU_HIDDEN_EXAMPLE = "feature_menu_hidden_example"
const val KEY_API_BASEURL_PROD_NEW_ENABLED = "key_api_baseurl_prod_new_enabled"
const val KEY_API_BASEURL_DEV_NEW_ENABLED = "key_api_baseurl_dev_new_enabled"
const val KEY_KEYSTORE_MANAGER_ENABLED = "key_keystore_manager_enabled"
const val KEY_FEATURE_SELECTED_PEERS_ENABLED = "feature_selected_peers_enabled"
}

fun initialize()
Expand Down
78 changes: 75 additions & 3 deletions app/src/main/java/com/brainwallet/di/Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package com.brainwallet.di
import android.content.Context
import android.content.SharedPreferences
import com.brainwallet.BuildConfig
import com.brainwallet.data.repository.LtcRepository
import com.brainwallet.data.repository.SettingRepository
import com.brainwallet.data.source.RemoteApiSource
import com.brainwallet.data.source.RemoteConfigSource
import com.brainwallet.tools.manager.BRApiManager
import com.brainwallet.data.repository.SelectedPeersRepository
import com.brainwallet.tools.sqlite.CurrencyDataSource
import com.brainwallet.tools.util.BRConstants
import com.brainwallet.ui.screens.home.SettingsViewModel
import com.brainwallet.ui.screens.inputwords.InputWordsViewModel
import com.brainwallet.ui.screens.ready.ReadyViewModel
Expand All @@ -17,25 +20,44 @@ import com.brainwallet.ui.screens.yourseedproveit.YourSeedProveItViewModel
import com.brainwallet.ui.screens.yourseedwords.YourSeedWordsViewModel
import com.brainwallet.util.cryptography.KeyStoreKeyGenerator
import com.brainwallet.util.cryptography.KeyStoreManager
import com.brainwallet.worker.CurrencyUpdateWorker
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.android.ext.koin.androidApplication
import org.koin.core.module.dsl.viewModel
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory

//todo module using koin as di framework here

val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
prettyPrint = true
}

val dataModule = module {
factory { provideOkHttpClient() }
single { provideRetrofit(get(), BRConstants.BW_API_PROD_HOST) }

single { provideApi(get()) }

single<RemoteConfigSource> {
RemoteConfigSource.FirebaseImpl(Firebase.remoteConfig).also {
it.initialize()
}
}
single { BRApiManager(get()) }
single<SelectedPeersRepository> { SelectedPeersRepository.Impl(get(), get()) }
single { CurrencyDataSource.getInstance(get()) }
single<SharedPreferences> { provideSharedPreferences(context = androidApplication()) }
single<SettingRepository> { SettingRepository.Impl(get(), get()) }
single<LtcRepository> { LtcRepository.Impl(get(), get(), get()) }
}

val viewModelModule = module {
Expand All @@ -51,11 +73,61 @@ val viewModelModule = module {

val appModule = module {
single<KeyStoreManager> { KeyStoreManager(get(), KeyStoreKeyGenerator.Impl()) }
single<CurrencyUpdateWorker> { CurrencyUpdateWorker(get()) }
}

private fun provideSharedPreferences(
context: Context,
name: String = "${BuildConfig.APPLICATION_ID}.prefs"
): SharedPreferences {
return context.getSharedPreferences(name, Context.MODE_PRIVATE)
}
}

private fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val requestBuilder = chain.request()
.newBuilder()
.addHeader("Accept", "application/json")
.addHeader("Content-Type", "application/json")
.addHeader("X-Litecoin-Testnet", "false")
.addHeader("Accept-Language", "en")
// .addHeader("User-agent",)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually need that User-agent in the query so we can differentiate from iOS, Android, iPhone and iPad @andhikayuana

chain.proceed(requestBuilder.build())
}
.addInterceptor { chain ->
val request = chain.request()
runCatching {
chain.proceed(request)
}.getOrElse {
//retry using dev host
val newRequest = request.newBuilder()
.url(BRConstants.BW_API_DEV_HOST + request.url.encodedPath)
.build()
chain.proceed(newRequest)
}
}
.addInterceptor(HttpLoggingInterceptor().apply {
setLevel(
when {
BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY
else -> HttpLoggingInterceptor.Level.NONE
}
)
})
.build()

internal fun provideRetrofit(
okHttpClient: OkHttpClient,
baseUrl: String = BRConstants.BW_API_PROD_HOST
): Retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(
json.asConverterFactory(
"application/json; charset=UTF8".toMediaType()
)
)
.build()

internal fun provideApi(retrofit: Retrofit): RemoteApiSource =
retrofit.create(RemoteApiSource::class.java)
Loading