refactor: code smell runBlocking inside an OkHttp Interceptor
This commit is contained in:
@@ -18,8 +18,8 @@ class GudariWalletApplication : Application(), Configuration.Provider {
|
|||||||
override val workManagerConfiguration: Configuration
|
override val workManagerConfiguration: Configuration
|
||||||
get() {
|
get() {
|
||||||
val secrets = EncryptedSecretStore(this)
|
val secrets = EncryptedSecretStore(this)
|
||||||
val api = LNbitsClient.create(secrets)
|
val client = LNbitsClient(secrets)
|
||||||
val repo = LNbitsWalletRepository(api = api, secrets = secrets)
|
val repo = LNbitsWalletRepository(api = client.api, secrets = secrets)
|
||||||
val paymentCache = PaymentCacheRepository(this)
|
val paymentCache = PaymentCacheRepository(this)
|
||||||
val syncStore = HistoricalSyncStore(this)
|
val syncStore = HistoricalSyncStore(this)
|
||||||
return Configuration.Builder()
|
return Configuration.Builder()
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package com.bitcointxoko.gudariwallet.api
|
|||||||
|
|
||||||
import com.bitcointxoko.gudariwallet.security.SecretStore
|
import com.bitcointxoko.gudariwallet.security.SecretStore
|
||||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
@@ -10,61 +14,80 @@ import okhttp3.OkHttpClient
|
|||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
object LNbitsClient {
|
class LNbitsClient(secrets: SecretStore) {
|
||||||
|
|
||||||
|
// Scope lives as long as this client — SupervisorJob so one failure
|
||||||
|
// doesn't cancel the URL-watching coroutine.
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
// Cached in memory. Starts blank; populated as soon as DataStore emits.
|
||||||
|
private val cachedBaseUrl = AtomicReference("")
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
// Suspend until DataStore has emitted at least once.
|
||||||
|
// This primes cachedBaseUrl before any request can realistically fire.
|
||||||
|
val initial = secrets.credentials.first()
|
||||||
|
cachedBaseUrl.set(initial.baseUrl)
|
||||||
|
// Then keep it updated reactively for the lifetime of this client.
|
||||||
|
secrets.credentials.collect { credentials ->
|
||||||
|
cachedBaseUrl.set(credentials.baseUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Single shared client — used by both Retrofit and WebSocket
|
// Single shared client — used by both Retrofit and WebSocket
|
||||||
val httpClient: OkHttpClient = OkHttpClient.Builder()
|
val httpClient: OkHttpClient get() = LNbitsHttpClient.instance
|
||||||
.connectTimeout(WalletConstants.CONNECT_TIMEOUT_S, TimeUnit.SECONDS)
|
val api: LNbitsApi by lazy { buildApi() }
|
||||||
.readTimeout(WalletConstants.READ_TIMEOUT_S, TimeUnit.SECONDS)
|
|
||||||
.writeTimeout(WalletConstants.WRITE_TIMEOUT_S, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun create(secrets: SecretStore): LNbitsApi {
|
private fun buildApi(): LNbitsApi {
|
||||||
// Dynamic base URL interceptor — rewrites the placeholder host to the
|
|
||||||
// real base URL at request time, not at Retrofit construction time.
|
|
||||||
// This means credentials only need to be in DataStore by the time the
|
|
||||||
// first HTTP request fires, not at app startup.
|
|
||||||
val dynamicBaseUrlInterceptor = Interceptor { chain ->
|
val dynamicBaseUrlInterceptor = Interceptor { chain ->
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
val originalUrl = originalRequest.url
|
val originalUrl = originalRequest.url
|
||||||
|
|
||||||
// Only rewrite requests targeting the Retrofit placeholder host.
|
|
||||||
// External LNURL/Lightning Address requests must pass through unchanged.
|
|
||||||
if (originalUrl.host != "placeholder.invalid") {
|
if (originalUrl.host != "placeholder.invalid") {
|
||||||
return@Interceptor chain.proceed(originalRequest)
|
return@Interceptor chain.proceed(originalRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
val realBaseUrl = runBlocking { secrets.baseUrl() }
|
val raw = cachedBaseUrl.get().ifBlank { null }
|
||||||
.ifBlank { "https://placeholder.invalid" }
|
?: throw IllegalStateException(
|
||||||
.trimEnd('/')
|
"LNbits base URL is not configured. " +
|
||||||
val realHttpUrl = realBaseUrl.toHttpUrl()
|
"Complete onboarding before making API calls."
|
||||||
|
)
|
||||||
|
|
||||||
|
val realHttpUrl = raw.trimEnd('/').toHttpUrl()
|
||||||
|
|
||||||
val newUrl = originalUrl.newBuilder()
|
val newUrl = originalUrl.newBuilder()
|
||||||
.scheme(realHttpUrl.scheme)
|
.scheme(realHttpUrl.scheme)
|
||||||
.host(realHttpUrl.host)
|
.host(realHttpUrl.host)
|
||||||
.port(realHttpUrl.port)
|
.port(realHttpUrl.port)
|
||||||
|
// Preserve sub-path prefix (e.g. /lnbits in reverse-proxy setups)
|
||||||
|
.encodedPath(
|
||||||
|
realHttpUrl.encodedPath.trimEnd('/') + originalUrl.encodedPath
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
chain.proceed(
|
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
|
||||||
originalRequest.newBuilder()
|
|
||||||
.url(newUrl)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val client = httpClient.newBuilder()
|
val client = LNbitsHttpClient.instance.newBuilder()
|
||||||
.addInterceptor(dynamicBaseUrlInterceptor)
|
.addInterceptor(dynamicBaseUrlInterceptor)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl("https://placeholder.invalid/") // structurally required by Retrofit;
|
.baseUrl("https://placeholder.invalid/")
|
||||||
|
// ↑ Structurally required: Retrofit validates that baseUrl is a
|
||||||
|
// well-formed absolute URL at construction time. The interceptor
|
||||||
|
// rewrites it before any real request leaves the device.
|
||||||
.client(client)
|
.client(client)
|
||||||
.addConverterFactory(
|
.addConverterFactory(
|
||||||
JsonProvider.json.asConverterFactory(
|
JsonProvider.json.asConverterFactory(
|
||||||
"application/json; charset=UTF-8".toMediaType()
|
"application/json; charset=UTF-8".toMediaType()
|
||||||
)
|
)
|
||||||
) .build()
|
)
|
||||||
|
.build()
|
||||||
.create(LNbitsApi::class.java)
|
.create(LNbitsApi::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.api
|
||||||
|
|
||||||
|
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single shared OkHttpClient for the entire app.
|
||||||
|
* Used by both LNbitsClient (Retrofit) and WalletNotificationService (WebSocket).
|
||||||
|
*/
|
||||||
|
object LNbitsHttpClient {
|
||||||
|
val instance: OkHttpClient = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(WalletConstants.CONNECT_TIMEOUT_S, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(WalletConstants.READ_TIMEOUT_S, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(WalletConstants.WRITE_TIMEOUT_S, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
+2
-2
@@ -8,7 +8,7 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import com.bitcointxoko.gudariwallet.api.JsonProvider
|
import com.bitcointxoko.gudariwallet.api.JsonProvider
|
||||||
import com.bitcointxoko.gudariwallet.api.LNbitsClient
|
import com.bitcointxoko.gudariwallet.api.LNbitsHttpClient
|
||||||
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
||||||
import com.bitcointxoko.gudariwallet.api.WsPaymentMessage
|
import com.bitcointxoko.gudariwallet.api.WsPaymentMessage
|
||||||
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
||||||
@@ -157,7 +157,7 @@ class WalletNotificationService : Service() {
|
|||||||
|
|
||||||
val request = Request.Builder().url(wsUrl).build()
|
val request = Request.Builder().url(wsUrl).build()
|
||||||
|
|
||||||
webSocket = LNbitsClient.httpClient.newWebSocket(request, object : WebSocketListener() {
|
webSocket = LNbitsHttpClient.instance.newWebSocket(request, object : WebSocketListener() {
|
||||||
|
|
||||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
Timber.d("WebSocket connected")
|
Timber.d("WebSocket connected")
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ import com.bitcointxoko.gudariwallet.ui.send.PaymentPoller
|
|||||||
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
||||||
|
|
||||||
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
||||||
|
private val lnbitsClient = LNbitsClient(EncryptedSecretStore(app))
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
val secrets = EncryptedSecretStore(app)
|
val secrets = EncryptedSecretStore(app)
|
||||||
val api = LNbitsClient.create(secrets)
|
|
||||||
val nodeAliasSvc = NodeAliasService(apiClient = NodeAliasClient(), context = app)
|
val nodeAliasSvc = NodeAliasService(apiClient = NodeAliasClient(), context = app)
|
||||||
val aliasRepo = NodeAliasServiceRepository(nodeAliasSvc)
|
val aliasRepo = NodeAliasServiceRepository(nodeAliasSvc)
|
||||||
val repo = LNbitsWalletRepository(api = api, secrets = secrets)
|
val repo = LNbitsWalletRepository(api = lnbitsClient.api, secrets = secrets)
|
||||||
val lnurlCache = LnurlCacheRepository(app)
|
val lnurlCache = LnurlCacheRepository(app)
|
||||||
val lnurlScanner = LnurlScanner(repo = repo, lnurlCache = lnurlCache)
|
val lnurlScanner = LnurlScanner(repo = repo, lnurlCache = lnurlCache)
|
||||||
val lnurlPayer = LnurlPayer(repo = repo, scanner = lnurlScanner, cache = lnurlCache)
|
val lnurlPayer = LnurlPayer(repo = repo, scanner = lnurlScanner, cache = lnurlCache)
|
||||||
|
|||||||
Reference in New Issue
Block a user