-
Notifications
You must be signed in to change notification settings - Fork 2
Feat/new peer discovery #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
089995a
7e85987
0aa41e7
0cc29cb
54d2d9e
c6923b1
ef4e6d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
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 | ||
// } | ||
} |
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> { | ||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
} |
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -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 | ||
|
@@ -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 { | ||
|
@@ -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",) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We actually need that |
||
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.