feat: add history sync worker
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package com.bitcointxoko.gudariwallet.api
|
||||
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
import com.google.gson.stream.JsonWriter
|
||||
|
||||
/**
|
||||
* LNbits returns `comment` in PaymentExtra as either:
|
||||
* - a plain string "Thanks!"
|
||||
* - a JSON array ["Thanks!"]
|
||||
* - null
|
||||
*
|
||||
* Registered centrally in [GsonProvider] — but also applied via @JsonAdapter
|
||||
* on the field for extra safety.
|
||||
*/
|
||||
class FlexibleStringAdapter : TypeAdapter<String?>() {
|
||||
|
||||
override fun write(out: JsonWriter, value: String?) {
|
||||
if (value == null) out.nullValue() else out.value(value)
|
||||
}
|
||||
|
||||
override fun read(reader: JsonReader): String? {
|
||||
return when (reader.peek()) {
|
||||
JsonToken.NULL -> { reader.nextNull(); null }
|
||||
JsonToken.BEGIN_ARRAY -> {
|
||||
val parts = mutableListOf<String>()
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.peek()) {
|
||||
JsonToken.NULL -> reader.nextNull()
|
||||
else -> parts.add(reader.nextString())
|
||||
}
|
||||
}
|
||||
reader.endArray()
|
||||
parts.joinToString(", ").ifEmpty { null }
|
||||
}
|
||||
else -> reader.nextString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,16 @@ interface LNbitsApi {
|
||||
@Header("X-Api-Key") apiKey: String
|
||||
): List<WsPayment>
|
||||
|
||||
/** GET /api/v1/payments/paginated — returns bulk payment history */
|
||||
@GET("api/v1/payments/paginated")
|
||||
suspend fun getPaymentsPaginated(
|
||||
@Header("X-Api-Key") apiKey : String,
|
||||
@Query("offset") offset : Int,
|
||||
@Query("limit") limit : Int
|
||||
): PaginatedPaymentsResponse
|
||||
|
||||
|
||||
|
||||
/** POST /api/v1/payments/decode — returns decoded payment request */
|
||||
@POST("api/v1/payments/decode")
|
||||
suspend fun decodeInvoice(
|
||||
|
||||
@@ -3,9 +3,10 @@ package com.bitcointxoko.gudariwallet.api
|
||||
import com.bitcointxoko.gudariwallet.BuildConfig
|
||||
import com.bitcointxoko.gudariwallet.security.SecretStore
|
||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
@@ -16,11 +17,10 @@ object LNbitsClient {
|
||||
|
||||
// Single shared client — used by both Retrofit and WebSocket
|
||||
val httpClient: OkHttpClient = OkHttpClient.Builder()
|
||||
.connectTimeout(WalletConstants.CONNECT_TIMEOUT_S, TimeUnit.SECONDS) // time to establish TCP connection
|
||||
.readTimeout(WalletConstants.READ_TIMEOUT_S, TimeUnit.SECONDS) // time to wait for data (longer for WebSocket pings)
|
||||
.writeTimeout(WalletConstants.WRITE_TIMEOUT_S, TimeUnit.SECONDS) // time to send a request body
|
||||
.connectTimeout(WalletConstants.CONNECT_TIMEOUT_S, TimeUnit.SECONDS)
|
||||
.readTimeout(WalletConstants.READ_TIMEOUT_S, TimeUnit.SECONDS)
|
||||
.writeTimeout(WalletConstants.WRITE_TIMEOUT_S, TimeUnit.SECONDS)
|
||||
.apply {
|
||||
// Only log in debug builds — prevents API keys leaking in production logs
|
||||
if (BuildConfig.DEBUG) {
|
||||
addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
@@ -30,14 +30,57 @@ object LNbitsClient {
|
||||
.build()
|
||||
|
||||
fun create(secrets: SecretStore): LNbitsApi {
|
||||
val baseUrl = runBlocking(Dispatchers.IO) {
|
||||
secrets.credentials.first().baseUrl
|
||||
}.ifBlank { "https://placeholder.invalid" }
|
||||
val normalizedUrl = if (baseUrl.endsWith("/")) baseUrl else "$baseUrl/"
|
||||
// Auth header interceptor — reads invoice key per-request (unchanged)
|
||||
val authInterceptor = Interceptor { chain ->
|
||||
val key = runBlocking { secrets.invoiceKey() }
|
||||
chain.proceed(
|
||||
chain.request().newBuilder()
|
||||
.header("X-Api-Key", key)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
// 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 realBaseUrl = runBlocking { secrets.baseUrl() }
|
||||
.ifBlank { "https://placeholder.invalid" }
|
||||
.trimEnd('/')
|
||||
val realHttpUrl = realBaseUrl.toHttpUrl()
|
||||
|
||||
val originalUrl = chain.request().url
|
||||
val newUrl = originalUrl.newBuilder()
|
||||
.scheme(realHttpUrl.scheme)
|
||||
.host(realHttpUrl.host)
|
||||
.port(realHttpUrl.port)
|
||||
.build()
|
||||
|
||||
chain.proceed(
|
||||
chain.request().newBuilder()
|
||||
.url(newUrl)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
val client = httpClient.newBuilder()
|
||||
.addInterceptor(authInterceptor)
|
||||
.addInterceptor(dynamicBaseUrlInterceptor)
|
||||
.apply {
|
||||
// Logging goes LAST — after URL rewrite — so logs show the real URL
|
||||
if (BuildConfig.DEBUG) {
|
||||
addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
})
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(normalizedUrl)
|
||||
.client(httpClient)
|
||||
.baseUrl("https://placeholder.invalid/") // structurally required by Retrofit;
|
||||
// always overwritten by the interceptor
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create(GsonProvider.gson))
|
||||
.build()
|
||||
.create(LNbitsApi::class.java)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bitcointxoko.gudariwallet.api
|
||||
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
@@ -205,7 +206,8 @@ data class PaymentRecord(
|
||||
|
||||
data class PaymentExtra(
|
||||
@SerializedName("tag") val tag: String?,
|
||||
@SerializedName("comment") val comment: String?,
|
||||
@JsonAdapter(FlexibleStringAdapter::class)
|
||||
val comment: String?,
|
||||
@SerializedName("success_action") val successAction: SuccessAction?,
|
||||
@SerializedName("wallet_id") val walletId: String?,
|
||||
@SerializedName("destination") val destination: String?,
|
||||
@@ -308,4 +310,10 @@ data class WsPayment(
|
||||
data class FiatRateResponse(
|
||||
val rate: Double, // sat to fiat rate
|
||||
val price : Double // BTC price in the requested fiat currency
|
||||
)
|
||||
)
|
||||
|
||||
// ── History sync ─────────────────────────────────────────────────────────────
|
||||
data class PaginatedPaymentsResponse(
|
||||
@SerializedName("data") val data: List<PaymentRecord>,
|
||||
@SerializedName("total") val total: Int
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user