refactor: migrate to kotlinx serialization, eliminate gson dependency
This commit is contained in:
Generated
+3
@@ -7,12 +7,15 @@
|
|||||||
<w>Nostr</w>
|
<w>Nostr</w>
|
||||||
<w>Tink</w>
|
<w>Tink</w>
|
||||||
<w>amboss</w>
|
<w>amboss</w>
|
||||||
|
<w>cltv</w>
|
||||||
<w>hkdf</w>
|
<w>hkdf</w>
|
||||||
<w>inflight</w>
|
<w>inflight</w>
|
||||||
<w>lnbc</w>
|
<w>lnbc</w>
|
||||||
<w>lnbits</w>
|
<w>lnbits</w>
|
||||||
<w>lnurlp</w>
|
<w>lnurlp</w>
|
||||||
<w>msat</w>
|
<w>msat</w>
|
||||||
|
<w>msats</w>
|
||||||
|
<w>nostr</w>
|
||||||
<w>satoshis</w>
|
<w>satoshis</w>
|
||||||
<w>sats</w>
|
<w>sats</w>
|
||||||
<w>snackbar</w>
|
<w>snackbar</w>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.protobuf)
|
alias(libs.plugins.protobuf)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -132,7 +133,7 @@ dependencies {
|
|||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
implementation(libs.retrofit2.retrofit)
|
implementation(libs.retrofit2.retrofit)
|
||||||
implementation(libs.converter.gson)
|
implementation(libs.converter.kotlinx.serialization)
|
||||||
implementation(libs.okhttp)
|
implementation(libs.okhttp)
|
||||||
debugImplementation(libs.logging.interceptor)
|
debugImplementation(libs.logging.interceptor)
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.biometric)
|
implementation(libs.androidx.biometric)
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
implementation(libs.gson)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
implementation(libs.androidx.material.icons.extended)
|
implementation(libs.androidx.material.icons.extended)
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
package com.bitcointxoko.gudariwallet.api
|
package com.bitcointxoko.gudariwallet.api
|
||||||
|
|
||||||
import com.google.gson.TypeAdapter
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import com.google.gson.stream.JsonReader
|
import kotlinx.serialization.KSerializer
|
||||||
import com.google.gson.stream.JsonToken
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import com.google.gson.stream.JsonWriter
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonNull
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LNbits returns `comment` in PaymentExtra as either:
|
* LNbits returns `comment` in PaymentExtra as either:
|
||||||
@@ -11,31 +18,29 @@ import com.google.gson.stream.JsonWriter
|
|||||||
* - a JSON array ["Thanks!"]
|
* - a JSON array ["Thanks!"]
|
||||||
* - null
|
* - null
|
||||||
*
|
*
|
||||||
* Registered centrally in [GsonProvider] — but also applied via @JsonAdapter
|
* Mirrors the behaviour of the old Gson FlexibleStringAdapter.
|
||||||
* on the field for extra safety.
|
|
||||||
*/
|
*/
|
||||||
class FlexibleStringAdapter : TypeAdapter<String?>() {
|
object FlexibleStringSerializer : KSerializer<String?> {
|
||||||
|
|
||||||
override fun write(out: JsonWriter, value: String?) {
|
override val descriptor: SerialDescriptor =
|
||||||
if (value == null) out.nullValue() else out.value(value)
|
buildClassSerialDescriptor("FlexibleString")
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
override fun serialize(encoder: Encoder, value: String?) {
|
||||||
|
if (value == null) encoder.encodeNull()
|
||||||
|
else encoder.encodeString(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun read(reader: JsonReader): String? {
|
override fun deserialize(decoder: Decoder): String? {
|
||||||
return when (reader.peek()) {
|
val jsonDecoder = decoder as JsonDecoder
|
||||||
JsonToken.NULL -> { reader.nextNull(); null }
|
return when (val element = jsonDecoder.decodeJsonElement()) {
|
||||||
JsonToken.BEGIN_ARRAY -> {
|
is JsonNull -> null
|
||||||
val parts = mutableListOf<String>()
|
is JsonPrimitive -> element.content // plain string → as-is
|
||||||
reader.beginArray()
|
is JsonArray -> element // array → join elements
|
||||||
while (reader.hasNext()) {
|
.mapNotNull { if (it is JsonNull) null else it.jsonPrimitive.content }
|
||||||
when (reader.peek()) {
|
.joinToString(", ")
|
||||||
JsonToken.NULL -> reader.nextNull()
|
.ifEmpty { null }
|
||||||
else -> parts.add(reader.nextString())
|
else -> null
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.endArray()
|
|
||||||
parts.joinToString(", ").ifEmpty { null }
|
|
||||||
}
|
|
||||||
else -> reader.nextString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package com.bitcointxoko.gudariwallet.api
|
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Single source of truth for the app's Gson instance.
|
|
||||||
*
|
|
||||||
* All serialization/deserialization — Retrofit, SharedPreferences caches,
|
|
||||||
* and WebSocket message parsing — must use this instance so that registered
|
|
||||||
* type adapters (e.g. [SuccessActionAdapter]) are applied consistently
|
|
||||||
* everywhere.
|
|
||||||
*
|
|
||||||
* To add a new adapter: register it here once. It will automatically apply
|
|
||||||
* to every deserialization path in the app.
|
|
||||||
*/
|
|
||||||
object GsonProvider {
|
|
||||||
val gson: Gson = GsonBuilder()
|
|
||||||
.registerTypeAdapter(SuccessAction::class.java, SuccessActionAdapter())
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.api
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for the app's Json instance.
|
||||||
|
*
|
||||||
|
* All serialization/deserialization — Retrofit, DataStore caches,
|
||||||
|
* and WebSocket message parsing — must use this instance so that
|
||||||
|
* configuration is applied consistently everywhere.
|
||||||
|
*
|
||||||
|
* Custom type serializers (e.g. [SuccessActionSerializer],
|
||||||
|
* [FlexibleStringSerializer]) are registered directly on the model
|
||||||
|
* classes via @Serializable(with = ...) so no explicit registration
|
||||||
|
* is needed here.
|
||||||
|
*/
|
||||||
|
object JsonProvider {
|
||||||
|
val json: Json = Json {
|
||||||
|
ignoreUnknownKeys = true // tolerate extra fields from the API
|
||||||
|
isLenient = true // accept unquoted/malformed JSON from some endpoints
|
||||||
|
explicitNulls = false // omit null fields when serializing requests
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ import com.bitcointxoko.gudariwallet.util.WalletConstants
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
object LNbitsClient {
|
object LNbitsClient {
|
||||||
@@ -59,8 +60,11 @@ object LNbitsClient {
|
|||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl("https://placeholder.invalid/") // structurally required by Retrofit;
|
.baseUrl("https://placeholder.invalid/") // structurally required by Retrofit;
|
||||||
.client(client)
|
.client(client)
|
||||||
.addConverterFactory(GsonConverterFactory.create(GsonProvider.gson))
|
.addConverterFactory(
|
||||||
.build()
|
JsonProvider.json.asConverterFactory(
|
||||||
|
"application/json; charset=UTF-8".toMediaType()
|
||||||
|
)
|
||||||
|
) .build()
|
||||||
.create(LNbitsApi::class.java)
|
.create(LNbitsApi::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,66 @@
|
|||||||
package com.bitcointxoko.gudariwallet.api
|
package com.bitcointxoko.gudariwallet.api
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.google.gson.TypeAdapter
|
import kotlinx.serialization.KSerializer
|
||||||
import com.google.gson.annotations.JsonAdapter
|
import kotlinx.serialization.SerialName
|
||||||
import com.google.gson.annotations.SerializedName
|
import kotlinx.serialization.Serializable
|
||||||
import com.google.gson.stream.JsonReader
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import com.google.gson.stream.JsonToken
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
import com.google.gson.stream.JsonWriter
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.JsonNull
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
// ── Wallet ────────────────────────────────────────────────────────────────────
|
// ── Wallet ────────────────────────────────────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class WalletResponse(
|
data class WalletResponse(
|
||||||
val id: String,
|
val id: String = "",
|
||||||
val name: String,
|
val name: String = "",
|
||||||
val balance: Long // millisatoshis
|
val balance: Long = 0L // millisatoshis
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Create invoice ────────────────────────────────────────────────────────────
|
// ── Create invoice ────────────────────────────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class CreateInvoiceRequest(
|
data class CreateInvoiceRequest(
|
||||||
val out: Boolean,
|
val out: Boolean,
|
||||||
val amount: Long, // satoshis
|
val amount: Long, // satoshis
|
||||||
val memo: String
|
val memo: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class CreateInvoiceResponse(
|
data class CreateInvoiceResponse(
|
||||||
@SerializedName("payment_hash") val paymentHash: String,
|
@SerialName("payment_hash") val paymentHash: String,
|
||||||
@SerializedName("payment_request") val paymentRequest: String,
|
@SerialName("payment_request") val paymentRequest: String,
|
||||||
@SerializedName("expiry") val expiry : String?
|
@SerialName("expiry") val expiry: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Pay invoice ───────────────────────────────────────────────────────────────
|
// ── Pay invoice ───────────────────────────────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class PayInvoiceRequest(
|
data class PayInvoiceRequest(
|
||||||
val out: Boolean,
|
val out: Boolean,
|
||||||
val bolt11: String
|
val bolt11: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class PayInvoiceResponse(
|
data class PayInvoiceResponse(
|
||||||
@SerializedName("payment_hash") val paymentHash: String,
|
@SerialName("payment_hash") val paymentHash: String,
|
||||||
@SerializedName("checking_id") val checkingId: String
|
@SerialName("checking_id") val checkingId: String
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Check payment ─────────────────────────────────────────────────────────────
|
// ── Check payment ─────────────────────────────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class PaymentStatusResponse(
|
data class PaymentStatusResponse(
|
||||||
val paid: Boolean,
|
val paid: Boolean,
|
||||||
val details: PaymentDetails? = null
|
val details: PaymentDetails? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class PaymentDetails(
|
data class PaymentDetails(
|
||||||
val amount: Long?,
|
val amount: Long?,
|
||||||
val fee: Long?,
|
val fee: Long?,
|
||||||
@@ -52,82 +68,80 @@ data class PaymentDetails(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ── Decode BOLT-11 ────────────────────────────────────────────────────────────
|
// ── Decode BOLT-11 ────────────────────────────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class DecodeInvoiceRequest(
|
data class DecodeInvoiceRequest(
|
||||||
val data: String
|
val data: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class DecodeInvoiceResponse(
|
data class DecodeInvoiceResponse(
|
||||||
@SerializedName("payment_hash") val paymentHash : String? = null,
|
@SerialName("payment_hash") val paymentHash: String? = null,
|
||||||
@SerializedName("amount_msat") val amountMsat : Long? = null,
|
@SerialName("amount_msat") val amountMsat: Long? = null,
|
||||||
@SerializedName("description") val description : String? = null,
|
@SerialName("description") val description: String? = null,
|
||||||
@SerializedName("payee") val payee : String? = null,
|
@SerialName("payee") val payee: String? = null,
|
||||||
@SerializedName("route_hints") val routeHints : List<List<RouteHintHop>>? = null
|
@SerialName("route_hints") val routeHints: List<List<RouteHintHop>>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class RouteHintHop(
|
data class RouteHintHop(
|
||||||
@SerializedName("public_key") val publicKey : String,
|
@SerialName("public_key") val publicKey: String,
|
||||||
@SerializedName("short_channel_id") val shortChannelId : String? = null,
|
@SerialName("short_channel_id") val shortChannelId: String? = null,
|
||||||
@SerializedName("base_fee") val baseFeeMsat : Long = 0,
|
@SerialName("base_fee") val baseFeeMsat: Long = 0,
|
||||||
@SerializedName("ppm_fee") val ppmFee : Long = 0,
|
@SerialName("ppm_fee") val ppmFee: Long = 0,
|
||||||
@SerializedName("cltv_expiry_delta") val cltvExpiryDelta : Int = 0
|
@SerialName("cltv_expiry_delta") val cltvExpiryDelta: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── LNURL scan ────────────────────────────────────────────────────────────────
|
// ── LNURL scan ────────────────────────────────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class LnurlScanResponse(
|
data class LnurlScanResponse(
|
||||||
val tag: String?,
|
val tag: String?,
|
||||||
// ── payRequest fields ────────────────────────────────────────────────────
|
// ── payRequest fields ─────────────────────────────────────────────────────
|
||||||
val callback: String? = null,
|
val callback: String? = null,
|
||||||
@SerializedName("minSendable") val minSendable: Long? = null,
|
@SerialName("minSendable") val minSendable: Long? = null,
|
||||||
@SerializedName("maxSendable") val maxSendable: Long? = null,
|
@SerialName("maxSendable") val maxSendable: Long? = null,
|
||||||
val metadata: String? = null,
|
val metadata: String? = null,
|
||||||
@SerializedName("commentAllowed") val commentAllowed: Int? = null,
|
@SerialName("commentAllowed") val commentAllowed: Int? = null,
|
||||||
@SerializedName("allowsNostr") val allowsNostr: Boolean? = null,
|
@SerialName("allowsNostr") val allowsNostr: Boolean? = null,
|
||||||
@SerializedName("nostrPubkey") val nostrPubkey: String? = null,
|
@SerialName("nostrPubkey") val nostrPubkey: String? = null,
|
||||||
val bolt11: String? = null,
|
val bolt11: String? = null,
|
||||||
@SerializedName("amount_msat") val amountMsat: Long? = null,
|
@SerialName("amount_msat") val amountMsat: Long? = null,
|
||||||
val disposable: Boolean? = null, // LUD-06: null means treat as true (do not cache)
|
val disposable: Boolean? = null, // LUD-06: null means treat as true (do not cache)
|
||||||
// ── withdrawRequest fields ───────────────────────────────────────────────
|
// ── withdrawRequest fields ────────────────────────────────────────────────
|
||||||
val k1 : String? = null,
|
val k1: String? = null,
|
||||||
@SerializedName("defaultDescription")
|
@SerialName("defaultDescription") val defaultDescription: String? = null,
|
||||||
val defaultDescription: String? = null,
|
@SerialName("minWithdrawable") val minWithdrawable: Long? = null,
|
||||||
@SerializedName("minWithdrawable")
|
@SerialName("maxWithdrawable") val maxWithdrawable: Long? = null,
|
||||||
val minWithdrawable : Long? = null,
|
@SerialName("balanceCheck") val balanceCheck: String? = null,
|
||||||
@SerializedName("maxWithdrawable")
|
@SerialName("currentBalance") val currentBalance: Long? = null,
|
||||||
val maxWithdrawable : Long? = null,
|
// ── Error fields ──────────────────────────────────────────────────────────
|
||||||
@SerializedName("balanceCheck")
|
val status: String? = null, // "OK" or "ERROR"
|
||||||
val balanceCheck : String? = null,
|
val reason: String? = null // error message when status == "ERROR"
|
||||||
@SerializedName("currentBalance")
|
|
||||||
val currentBalance : Long? = null,
|
|
||||||
// ── Error fields ─────────────────────────────────────────────────────────
|
|
||||||
val status : String? = null, // "OK" or "ERROR"
|
|
||||||
val reason : String? = null // error message when status == "ERROR"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── LNURL-pay callback response (LUD-06) ─────────────────────────────────────
|
// ── LNURL-pay callback response (LUD-06) ─────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class LnurlCallbackResponse(
|
data class LnurlCallbackResponse(
|
||||||
val pr : String, // BOLT-11 invoice to pay
|
val pr: String,
|
||||||
val routes : List<Any>? = null,
|
val routes: List<JsonElement>? = null,
|
||||||
@SerializedName("successAction")
|
@SerialName("successAction") val successAction: SuccessAction? = null,
|
||||||
val successAction : SuccessAction? = null,
|
val status: String? = null,
|
||||||
// Error fields (server may return status=ERROR instead of pr)
|
val reason: String? = null
|
||||||
val status : String? = null,
|
|
||||||
val reason : String? = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// ── LNURL pay ─────────────────────────────────────────────────────────────────
|
// ── LNURL pay ─────────────────────────────────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class LnurlPayRes(
|
data class LnurlPayRes(
|
||||||
val tag: String?,
|
val tag: String?,
|
||||||
val callback: String?,
|
val callback: String?,
|
||||||
@SerializedName("minSendable") val minSendable: Long?,
|
@SerialName("minSendable") val minSendable: Long?,
|
||||||
@SerializedName("maxSendable") val maxSendable: Long?,
|
@SerialName("maxSendable") val maxSendable: Long?,
|
||||||
val metadata: String?,
|
val metadata: String?,
|
||||||
@SerializedName("commentAllowed") val commentAllowed: Int?,
|
@SerialName("commentAllowed") val commentAllowed: Int?,
|
||||||
@SerializedName("allowsNostr") val allowsNostr: Boolean? = null,
|
@SerialName("allowsNostr") val allowsNostr: Boolean? = null,
|
||||||
@SerializedName("nostrPubkey") val nostrPubkey: String? = null
|
@SerialName("nostrPubkey") val nostrPubkey: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class PayLnurlRequest(
|
data class PayLnurlRequest(
|
||||||
val res: LnurlPayRes,
|
val res: LnurlPayRes,
|
||||||
val lnurl: String,
|
val lnurl: String,
|
||||||
@@ -135,45 +149,48 @@ data class PayLnurlRequest(
|
|||||||
val comment: String? = null
|
val comment: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class PayLnurlResponse(
|
data class PayLnurlResponse(
|
||||||
@SerializedName("payment_hash") val paymentHash: String,
|
@SerialName("payment_hash") val paymentHash: String,
|
||||||
@SerializedName("checking_id") val checkingId: String
|
@SerialName("checking_id") val checkingId: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class WithdrawCallbackResponse(
|
data class WithdrawCallbackResponse(
|
||||||
val status : String,
|
val status: String,
|
||||||
val reason : String? = null
|
val reason: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── LNURLp pay link (lightning address) ──────────────────────────────────────
|
// ── LNURLp pay link (lightning address) ──────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class LnurlpLink(
|
data class LnurlpLink(
|
||||||
val id : String,
|
val id: String,
|
||||||
val wallet : String,
|
val wallet: String,
|
||||||
val username : String?, // the part before the @ in the lightning address
|
val username: String?, // the part before the @ in the lightning address
|
||||||
val description : String?,
|
val description: String?,
|
||||||
val min : Long,
|
@Serializable(with = LongAsDoubleSerializer::class) val min: Long,
|
||||||
val max : Long
|
@Serializable(with = LongAsDoubleSerializer::class) val max: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// ── Payment history ───────────────────────────────────────────────────────────
|
// ── Payment history ───────────────────────────────────────────────────────────
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@Serializable
|
||||||
data class PaymentRecord(
|
data class PaymentRecord(
|
||||||
@SerializedName("payment_hash") val paymentHash: String,
|
@SerialName("payment_hash") val paymentHash: String = "",
|
||||||
@SerializedName("checking_id") val checkingId: String,
|
@SerialName("checking_id") val checkingId: String = "",
|
||||||
@SerializedName("amount") val amountMsat: Long,
|
@SerialName("amount") val amountMsat: Long = 0L,
|
||||||
@SerializedName("fee") val feeMsat: Long,
|
@SerialName("fee") val feeMsat: Long = 0L,
|
||||||
@SerializedName("memo") val memo: String?,
|
@SerialName("memo") val memo: String? = null,
|
||||||
@SerializedName("time") val time: String,
|
@SerialName("time") val time: String = "",
|
||||||
@SerializedName("status") val status: String,
|
@SerialName("status") val status: String = "",
|
||||||
@SerializedName("bolt11") val bolt11: String?,
|
@SerialName("bolt11") val bolt11: String? = null,
|
||||||
@SerializedName("pending") val pending: Boolean,
|
@SerialName("pending") val pending: Boolean = false,
|
||||||
@SerializedName("preimage") val preimage: String?,
|
@SerialName("preimage") val preimage: String? = null,
|
||||||
@SerializedName("extra") val extra: PaymentExtra?,
|
@SerialName("extra") val extra: PaymentExtra? = null,
|
||||||
// Not from the API — populated only when loaded from Room via toDomain()
|
// Not from the API — populated only when loaded from Room via toDomain()
|
||||||
val createdAt: Long? = null,
|
val createdAt: Long? = null,
|
||||||
val pubkey: String? = null,
|
val pubkey: String? = null,
|
||||||
val alias: String? = null
|
val alias: String? = null
|
||||||
) {
|
) {
|
||||||
val amountSat: Long get() = kotlin.math.abs(amountMsat) / 1000L
|
val amountSat: Long get() = kotlin.math.abs(amountMsat) / 1000L
|
||||||
val feeSat: Long get() = kotlin.math.abs(feeMsat) / 1000L
|
val feeSat: Long get() = kotlin.math.abs(feeMsat) / 1000L
|
||||||
@@ -181,13 +198,13 @@ data class PaymentRecord(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromPayment(
|
fun fromPayment(
|
||||||
paymentHash : String,
|
paymentHash: String,
|
||||||
amountSats : Long,
|
amountSats: Long,
|
||||||
feeSats : Long?,
|
feeSats: Long?,
|
||||||
bolt11 : String?,
|
bolt11: String?,
|
||||||
memo : String?,
|
memo: String?,
|
||||||
pubkey : String?,
|
pubkey: String?,
|
||||||
alias : String?,
|
alias: String?,
|
||||||
): PaymentRecord = PaymentRecord(
|
): PaymentRecord = PaymentRecord(
|
||||||
paymentHash = paymentHash,
|
paymentHash = paymentHash,
|
||||||
checkingId = paymentHash,
|
checkingId = paymentHash,
|
||||||
@@ -207,23 +224,25 @@ data class PaymentRecord(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@Serializable
|
||||||
data class PaymentExtra(
|
data class PaymentExtra(
|
||||||
@SerializedName("tag") val tag: String?,
|
@SerialName("tag") val tag: String? = null,
|
||||||
@JsonAdapter(FlexibleStringAdapter::class)
|
@Serializable(with = FlexibleStringSerializer::class)
|
||||||
val comment: String?,
|
val comment: String? = null,
|
||||||
@SerializedName("success_action") val successAction: SuccessAction?,
|
@SerialName("success_action") @Serializable(with = SuccessActionSerializer::class)
|
||||||
@SerializedName("wallet_id") val walletId: String?,
|
val successAction: SuccessAction? = null,
|
||||||
@SerializedName("destination") val destination: String?,
|
@SerialName("wallet_id") val walletId: String? = null,
|
||||||
@SerializedName("expires_at") val expiresAt: String?
|
@SerialName("destination") val destination: String? = null,
|
||||||
|
@SerialName("expires_at") val expiresAt: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@Serializable(with = SuccessActionSerializer::class)
|
||||||
data class SuccessAction(
|
data class SuccessAction(
|
||||||
@SerializedName("tag") val tag: String?,
|
@SerialName("tag") val tag: String?,
|
||||||
@SerializedName("message") val message: String?,
|
@SerialName("message") val message: String?,
|
||||||
@SerializedName("url") val url: String?,
|
@SerialName("url") val url: String?,
|
||||||
@SerializedName("description") val description: String?
|
@SerialName("description") val description: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -232,99 +251,101 @@ data class SuccessAction(
|
|||||||
* - a plain string "Thanks!"
|
* - a plain string "Thanks!"
|
||||||
* - null
|
* - null
|
||||||
*
|
*
|
||||||
* Gson's default deserialiser throws IllegalStateException when it sees a
|
* kotlinx.serialization's default deserialiser throws when it sees a
|
||||||
* string where it expects {. This adapter handles all three cases.
|
* string where it expects {. This serializer handles all three cases.
|
||||||
*
|
*
|
||||||
* Registered centrally in [GsonProvider] — applies to all deserialization
|
* Applied via @Serializable(with = SuccessActionSerializer::class) on
|
||||||
* paths automatically.
|
* the SuccessAction class and on PaymentExtra.successAction.
|
||||||
*/
|
*/
|
||||||
class SuccessActionAdapter : TypeAdapter<SuccessAction?>() {
|
object SuccessActionSerializer : KSerializer<SuccessAction?> {
|
||||||
|
|
||||||
override fun write(out: JsonWriter, value: SuccessAction?) {
|
override val descriptor: SerialDescriptor =
|
||||||
if (value == null) { out.nullValue(); return }
|
buildClassSerialDescriptor("SuccessAction")
|
||||||
out.beginObject()
|
|
||||||
out.name("tag"); out.value(value.tag)
|
override fun serialize(encoder: Encoder, value: SuccessAction?) {
|
||||||
out.name("message"); out.value(value.message)
|
val jsonEncoder = encoder as JsonEncoder
|
||||||
out.name("url"); out.value(value.url)
|
if (value == null) {
|
||||||
out.name("description"); out.value(value.description)
|
jsonEncoder.encodeJsonElement(JsonNull)
|
||||||
out.endObject()
|
} else {
|
||||||
|
jsonEncoder.encodeJsonElement(
|
||||||
|
JsonObject(
|
||||||
|
mapOf(
|
||||||
|
"tag" to JsonPrimitive(value.tag),
|
||||||
|
"message" to JsonPrimitive(value.message),
|
||||||
|
"url" to JsonPrimitive(value.url),
|
||||||
|
"description" to JsonPrimitive(value.description)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun read(reader: JsonReader): SuccessAction? {
|
override fun deserialize(decoder: Decoder): SuccessAction? {
|
||||||
return when (reader.peek()) {
|
val jsonDecoder = decoder as JsonDecoder
|
||||||
JsonToken.NULL -> { reader.nextNull(); null }
|
return when (val element = jsonDecoder.decodeJsonElement()) {
|
||||||
|
is JsonNull -> null
|
||||||
// Plain string — treat as a message-type success action
|
is JsonPrimitive -> {
|
||||||
JsonToken.STRING -> {
|
// Plain string — treat as a message-type success action
|
||||||
val text = reader.nextString()
|
val text = element.jsonPrimitive.content
|
||||||
if (text.isBlank()) null
|
if (text.isBlank()) null
|
||||||
else SuccessAction(tag = "message", message = text, url = null, description = null)
|
else SuccessAction(tag = "message", message = text, url = null, description = null)
|
||||||
}
|
}
|
||||||
|
is JsonObject -> {
|
||||||
// Normal object
|
SuccessAction(
|
||||||
JsonToken.BEGIN_OBJECT -> {
|
tag = element.jsonObject["tag"]?.jsonPrimitive?.content,
|
||||||
reader.beginObject()
|
message = element.jsonObject["message"]?.jsonPrimitive?.content,
|
||||||
var tag: String? = null
|
url = element.jsonObject["url"]?.jsonPrimitive?.content,
|
||||||
var message: String? = null
|
description = element.jsonObject["description"]?.jsonPrimitive?.content
|
||||||
var url: String? = null
|
)
|
||||||
var description: String? = null
|
|
||||||
while (reader.hasNext()) {
|
|
||||||
when (reader.nextName()) {
|
|
||||||
"tag" -> tag = reader.nextString()
|
|
||||||
"message" -> message = reader.nextString()
|
|
||||||
"url" -> url = reader.nextString()
|
|
||||||
"description" -> description = reader.nextString()
|
|
||||||
else -> reader.skipValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.endObject()
|
|
||||||
SuccessAction(tag = tag, message = message, url = url, description = description)
|
|
||||||
}
|
}
|
||||||
|
else -> null
|
||||||
else -> { reader.skipValue(); null }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PaymentDetailResponse(
|
data class PaymentDetailResponse(
|
||||||
val paid: Boolean,
|
val paid: Boolean,
|
||||||
val details: PaymentRecord?
|
val details: PaymentRecord?
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── WebSocket payment message ─────────────────────────────────────────────────
|
// ── WebSocket payment message ─────────────────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class WsPaymentMessage(
|
data class WsPaymentMessage(
|
||||||
@SerializedName("wallet_balance") val walletBalance: Long?,
|
@SerialName("wallet_balance") val walletBalance: Long?,
|
||||||
val payment: WsPayment?
|
val payment: WsPayment?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class WsPayment(
|
data class WsPayment(
|
||||||
@SerializedName("payment_hash") val paymentHash: String,
|
@SerialName("payment_hash") val paymentHash: String,
|
||||||
@SerializedName("checking_id") val checkingId: String,
|
@SerialName("checking_id") val checkingId: String,
|
||||||
@SerializedName("amount") val amount: Long,
|
@SerialName("amount") val amount: Long,
|
||||||
@SerializedName("fee") val fee: Long,
|
@SerialName("fee") val fee: Long,
|
||||||
@SerializedName("memo") val memo: String?,
|
@SerialName("memo") val memo: String?,
|
||||||
@SerializedName("time") val time: String,
|
@SerialName("time") val time: String,
|
||||||
@SerializedName("status") val status: String,
|
@SerialName("status") val status: String,
|
||||||
@SerializedName("bolt11") val bolt11: String?,
|
@SerialName("bolt11") val bolt11: String?,
|
||||||
@SerializedName("pending") val pending: Boolean,
|
@SerialName("pending") val pending: Boolean,
|
||||||
@SerializedName("preimage") val preimage: String?,
|
@SerialName("preimage") val preimage: String?,
|
||||||
@SerializedName("extra") val extra: PaymentExtra?
|
@SerialName("extra") val extra: PaymentExtra?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── Fiat rate ─────────────────────────────────────────────────────────────────
|
||||||
// ── Fiat rate ────────────────────────────────────────────────────────────────
|
@Serializable
|
||||||
data class FiatRateResponse(
|
data class FiatRateResponse(
|
||||||
val rate: Double, // sat to fiat rate
|
val rate: Double, // sat to fiat rate
|
||||||
val price : Double // BTC price in the requested fiat currency
|
val price: Double // BTC price in the requested fiat currency
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── History sync ─────────────────────────────────────────────────────────────
|
// ── History sync ──────────────────────────────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class PaginatedPaymentsResponse(
|
data class PaginatedPaymentsResponse(
|
||||||
@SerializedName("data") val data: List<PaymentRecord>,
|
@SerialName("data") val data: List<PaymentRecord>,
|
||||||
@SerializedName("total") val total: Int
|
@SerialName("total") val total: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── NWC Provider models ───────────────────────────────────────────────────────
|
// ── NWC Provider models ───────────────────────────────────────────────────────
|
||||||
|
@Serializable
|
||||||
data class NwcKey(
|
data class NwcKey(
|
||||||
val pubkey: String,
|
val pubkey: String,
|
||||||
val wallet: String,
|
val wallet: String,
|
||||||
@@ -335,6 +356,7 @@ data class NwcKey(
|
|||||||
val last_used: Long
|
val last_used: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class NwcBudget(
|
data class NwcBudget(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val pubkey: String,
|
val pubkey: String,
|
||||||
@@ -344,22 +366,41 @@ data class NwcBudget(
|
|||||||
val used_budget_msats: Long = 0
|
val used_budget_msats: Long = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Response for GET /nwc and GET /nwc/{pubkey} */
|
@Serializable
|
||||||
data class NwcGetResponse(
|
data class NwcGetResponse(
|
||||||
val data: NwcKey,
|
val data: NwcKey,
|
||||||
val budgets: List<NwcBudget>
|
val budgets: List<NwcBudget>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class NwcNewBudget(
|
data class NwcNewBudget(
|
||||||
val budget_msats: Long,
|
val budget_msats: Long,
|
||||||
val refresh_window: Int, // seconds
|
val refresh_window: Int, // seconds
|
||||||
val created_at : Long = System.currentTimeMillis() / 1000L
|
val created_at: Long = System.currentTimeMillis() / 1000L
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Request body for PUT /nwc/{pubkey} */
|
@Serializable
|
||||||
data class NwcRegistrationRequest(
|
data class NwcRegistrationRequest(
|
||||||
val permissions: List<String>,
|
val permissions: List<String>,
|
||||||
val description: String,
|
val description: String,
|
||||||
val expires_at: Long, // unix timestamp; 0 = no expiry
|
val expires_at: Long, // unix timestamp; 0 = no expiry
|
||||||
val budgets: List<NwcNewBudget> = emptyList()
|
val budgets: List<NwcNewBudget> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserializes a JSON number that may arrive as either an integer (1)
|
||||||
|
* or a float (1.0) into a Long, truncating any fractional part.
|
||||||
|
*/
|
||||||
|
object LongAsDoubleSerializer : KSerializer<Long> {
|
||||||
|
override val descriptor = buildClassSerialDescriptor("LongAsDouble")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: Long) = encoder.encodeLong(value)
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): Long {
|
||||||
|
val jsonDecoder = decoder as JsonDecoder
|
||||||
|
return when (val element = jsonDecoder.decodeJsonElement()) {
|
||||||
|
is JsonPrimitive -> element.content.toDouble().toLong()
|
||||||
|
else -> 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.bitcointxoko.gudariwallet.api
|
package com.bitcointxoko.gudariwallet.api
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
// ── mempool.space ─────────────────────────────────────────────────────────────
|
// ── mempool.space ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class MempoolNodeResponse(
|
data class MempoolNodeResponse(
|
||||||
val alias: String?,
|
val alias: String?,
|
||||||
val public_key: String?
|
val public_key: String?
|
||||||
@@ -17,6 +19,7 @@ interface MempoolApi {
|
|||||||
|
|
||||||
// ── 1ml.com ───────────────────────────────────────────────────────────────────
|
// ── 1ml.com ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class OneMlNodeResponse(
|
data class OneMlNodeResponse(
|
||||||
val alias: String?,
|
val alias: String?,
|
||||||
val pub_key: String?
|
val pub_key: String?
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package com.bitcointxoko.gudariwallet.api
|
package com.bitcointxoko.gudariwallet.api
|
||||||
|
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,17 +18,20 @@ class NodeAliasClient {
|
|||||||
.readTimeout(5, TimeUnit.SECONDS)
|
.readTimeout(5, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
private val converter = JsonProvider.json
|
||||||
|
.asConverterFactory("application/json; charset=UTF-8".toMediaType())
|
||||||
|
|
||||||
private val mempoolApi: MempoolApi = Retrofit.Builder()
|
private val mempoolApi: MempoolApi = Retrofit.Builder()
|
||||||
.baseUrl("https://mempool.space/")
|
.baseUrl("https://mempool.space/")
|
||||||
.client(client)
|
.client(client)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(converter)
|
||||||
.build()
|
.build()
|
||||||
.create(MempoolApi::class.java)
|
.create(MempoolApi::class.java)
|
||||||
|
|
||||||
private val oneMlApi: OneMlApi = Retrofit.Builder()
|
private val oneMlApi: OneMlApi = Retrofit.Builder()
|
||||||
.baseUrl("https://1ml.com/")
|
.baseUrl("https://1ml.com/")
|
||||||
.client(client)
|
.client(client)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(converter)
|
||||||
.build()
|
.build()
|
||||||
.create(OneMlApi::class.java)
|
.create(OneMlApi::class.java)
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import androidx.datastore.preferences.core.floatPreferencesKey
|
|||||||
import androidx.datastore.preferences.core.longPreferencesKey
|
import androidx.datastore.preferences.core.longPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.google.gson.Gson
|
import com.bitcointxoko.gudariwallet.api.JsonProvider
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
|
import kotlinx.serialization.builtins.serializer
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
private const val TAG = "FiatDataStore"
|
private const val TAG = "FiatDataStore"
|
||||||
@@ -27,13 +28,13 @@ private val Context.fiatDataStore: DataStore<Preferences> by preferencesDataStor
|
|||||||
class FiatDataStore(context: Context) {
|
class FiatDataStore(context: Context) {
|
||||||
|
|
||||||
private val dataStore = context.applicationContext.fiatDataStore
|
private val dataStore = context.applicationContext.fiatDataStore
|
||||||
private val gson = Gson()
|
private val json = JsonProvider.json
|
||||||
|
|
||||||
// ── Key factories ─────────────────────────────────────────────────────────
|
// ── Key factories ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun rateKey(currency: String) = floatPreferencesKey("fiat_rate_$currency")
|
private fun rateKey(currency: String) = floatPreferencesKey("fiat_rate_$currency")
|
||||||
private fun rateFetchedAtKey(currency: String) = longPreferencesKey("fiat_rate_fetched_at_$currency")
|
private fun rateFetchedAtKey(currency: String) = longPreferencesKey("fiat_rate_fetched_at_$currency")
|
||||||
private val currenciesKey = stringPreferencesKey("fiat_currencies_json")
|
private val currenciesKey = stringPreferencesKey("fiat_currencies_json")
|
||||||
|
|
||||||
// ── Rate ──────────────────────────────────────────────────────────────────
|
// ── Rate ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -87,10 +88,9 @@ class FiatDataStore(context: Context) {
|
|||||||
} else throw e
|
} else throw e
|
||||||
}
|
}
|
||||||
.first()
|
.first()
|
||||||
val json = prefs[currenciesKey] ?: return null
|
val jsonString = prefs[currenciesKey] ?: return null
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val type = object : TypeToken<List<String>>() {}.type
|
val list = json.decodeFromString(ListSerializer(String.serializer()), jsonString)
|
||||||
val list: List<String> = gson.fromJson(json, type)
|
|
||||||
Timber.d("FIAT [CURRENCIES] DataStore hit (${list.size} currencies)")
|
Timber.d("FIAT [CURRENCIES] DataStore hit (${list.size} currencies)")
|
||||||
list
|
list
|
||||||
}.getOrElse { e ->
|
}.getOrElse { e ->
|
||||||
@@ -106,7 +106,7 @@ class FiatDataStore(context: Context) {
|
|||||||
if (currencies.isEmpty()) return
|
if (currencies.isEmpty()) return
|
||||||
runCatching {
|
runCatching {
|
||||||
dataStore.edit { prefs ->
|
dataStore.edit { prefs ->
|
||||||
prefs[currenciesKey] = gson.toJson(currencies)
|
prefs[currenciesKey] = json.encodeToString(ListSerializer(String.serializer()), currencies)
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
Timber.w("FIAT [CURRENCIES] persist failed — ${e.message}")
|
Timber.w("FIAT [CURRENCIES] persist failed — ${e.message}")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.bitcointxoko.gudariwallet.data
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import com.bitcointxoko.gudariwallet.api.GsonProvider
|
import com.bitcointxoko.gudariwallet.api.JsonProvider
|
||||||
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
|
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
|
||||||
import com.bitcointxoko.gudariwallet.data.db.AppDatabase
|
import com.bitcointxoko.gudariwallet.data.db.AppDatabase
|
||||||
import com.bitcointxoko.gudariwallet.data.db.LnurlCacheEntity
|
import com.bitcointxoko.gudariwallet.data.db.LnurlCacheEntity
|
||||||
@@ -13,7 +13,7 @@ private const val TAG = "LnurlCacheRepository"
|
|||||||
class LnurlCacheRepository(context: Context) {
|
class LnurlCacheRepository(context: Context) {
|
||||||
|
|
||||||
private val dao = AppDatabase.getInstance(context).lnurlCacheDao()
|
private val dao = AppDatabase.getInstance(context).lnurlCacheDao()
|
||||||
private val gson = GsonProvider.gson
|
private val json = JsonProvider.json
|
||||||
|
|
||||||
// Fast in-memory path — avoids a DB round-trip for keys already loaded
|
// Fast in-memory path — avoids a DB round-trip for keys already loaded
|
||||||
// this session. Mirrors the pattern used in PaymentCacheRepository.
|
// this session. Mirrors the pattern used in PaymentCacheRepository.
|
||||||
@@ -62,13 +62,13 @@ class LnurlCacheRepository(context: Context) {
|
|||||||
*/
|
*/
|
||||||
suspend fun put(key: String, response: LnurlScanResponse) {
|
suspend fun put(key: String, response: LnurlScanResponse) {
|
||||||
val disposable = response.disposable
|
val disposable = response.disposable
|
||||||
if ( disposable == true) {
|
if (disposable == true) {
|
||||||
Timber.d("LNURL [SKIP CACHE ] $key — disposable=$disposable, not caching")
|
Timber.d("LNURL [SKIP CACHE ] $key — disposable=$disposable, not caching")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val entity = LnurlCacheEntity(
|
val entity = LnurlCacheEntity(
|
||||||
key = key,
|
key = key,
|
||||||
responseJson = gson.toJson(response),
|
responseJson = json.encodeToString(LnurlScanResponse.serializer(), response),
|
||||||
savedAt = System.currentTimeMillis()
|
savedAt = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -104,7 +104,7 @@ class LnurlCacheRepository(context: Context) {
|
|||||||
runCatching { dao.deleteByKey(key) }
|
runCatching { dao.deleteByKey(key) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deserialise(json: String): LnurlScanResponse? =
|
private fun deserialise(jsonString: String): LnurlScanResponse? =
|
||||||
runCatching { gson.fromJson(json, LnurlScanResponse::class.java) }
|
runCatching { json.decodeFromString(LnurlScanResponse.serializer(), jsonString) }
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
package com.bitcointxoko.gudariwallet.data.db
|
package com.bitcointxoko.gudariwallet.data.db
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
|
import com.bitcointxoko.gudariwallet.api.JsonProvider
|
||||||
import com.bitcointxoko.gudariwallet.api.PaymentExtra
|
import com.bitcointxoko.gudariwallet.api.PaymentExtra
|
||||||
import com.bitcointxoko.gudariwallet.api.GsonProvider
|
|
||||||
|
|
||||||
object PaymentConverters {
|
object PaymentConverters {
|
||||||
private val gson = GsonProvider.gson
|
private val json = JsonProvider.json
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun fromExtra(extra: PaymentExtra?): String? =
|
fun fromExtra(extra: PaymentExtra?): String? =
|
||||||
extra?.let { gson.toJson(it) }
|
extra?.let { json.encodeToString(PaymentExtra.serializer(), it) }
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun toExtra(json: String?): PaymentExtra? =
|
fun toExtra(string: String?): PaymentExtra? =
|
||||||
json?.let { gson.fromJson(it, PaymentExtra::class.java) }
|
string?.let { json.decodeFromString(PaymentExtra.serializer(), it) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.bitcointxoko.gudariwallet.data.db
|
package com.bitcointxoko.gudariwallet.data.db
|
||||||
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import com.bitcointxoko.gudariwallet.api.GsonProvider.gson
|
import com.bitcointxoko.gudariwallet.api.JsonProvider
|
||||||
|
import com.bitcointxoko.gudariwallet.api.PaymentExtra
|
||||||
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
||||||
import com.bitcointxoko.gudariwallet.data.db.PaymentRecordEntity
|
|
||||||
|
|
||||||
private const val TAG = "PaymentMappers"
|
private const val TAG = "PaymentMappers"
|
||||||
|
private val json = JsonProvider.json
|
||||||
|
|
||||||
// ── Parse helper — runs once at write time, never again ──────────────────────
|
// ── Parse helper — runs once at write time, never again ──────────────────────
|
||||||
|
|
||||||
@@ -36,10 +36,10 @@ fun PaymentRecord.toEntity(savedAt: Long): PaymentRecordEntity =
|
|||||||
bolt11 = bolt11,
|
bolt11 = bolt11,
|
||||||
pending = pending,
|
pending = pending,
|
||||||
preimage = preimage,
|
preimage = preimage,
|
||||||
extra = extra?.let { gson.toJson(it) },
|
extra = extra?.let { json.encodeToString(PaymentExtra.serializer(), it) },
|
||||||
savedAt = savedAt,
|
savedAt = savedAt,
|
||||||
pubkey = pubkey,
|
pubkey = pubkey,
|
||||||
alias = alias
|
alias = alias
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── PaymentRecordEntity → PaymentRecord ──────────────────────────────────────
|
// ── PaymentRecordEntity → PaymentRecord ──────────────────────────────────────
|
||||||
@@ -57,7 +57,7 @@ fun PaymentRecordEntity.toDomain(): PaymentRecord =
|
|||||||
bolt11 = bolt11,
|
bolt11 = bolt11,
|
||||||
pending = pending,
|
pending = pending,
|
||||||
preimage = preimage,
|
preimage = preimage,
|
||||||
extra = extra?.let { gson.fromJson(it, com.bitcointxoko.gudariwallet.api.PaymentExtra::class.java) },
|
extra = extra?.let { json.decodeFromString(PaymentExtra.serializer(), it) },
|
||||||
pubkey = pubkey,
|
pubkey = pubkey,
|
||||||
alias = alias
|
alias = alias
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -7,7 +7,7 @@ import android.content.pm.ServiceInfo
|
|||||||
import android.os.Build
|
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.GsonProvider
|
import com.bitcointxoko.gudariwallet.api.JsonProvider
|
||||||
import com.bitcointxoko.gudariwallet.api.LNbitsClient
|
import com.bitcointxoko.gudariwallet.api.LNbitsClient
|
||||||
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
||||||
import com.bitcointxoko.gudariwallet.api.WsPaymentMessage
|
import com.bitcointxoko.gudariwallet.api.WsPaymentMessage
|
||||||
@@ -98,7 +98,7 @@ class WalletNotificationService : Service() {
|
|||||||
private var reconnectAttempts = 0
|
private var reconnectAttempts = 0
|
||||||
private var currentBackoffMs = WalletConstants.WS_INITIAL_BACKOFF_MS
|
private var currentBackoffMs = WalletConstants.WS_INITIAL_BACKOFF_MS
|
||||||
|
|
||||||
private val gson = GsonProvider.gson
|
private val json = JsonProvider.json
|
||||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
@@ -193,7 +193,7 @@ class WalletNotificationService : Service() {
|
|||||||
|
|
||||||
private fun handleMessage(text: String) {
|
private fun handleMessage(text: String) {
|
||||||
val msg = runCatching {
|
val msg = runCatching {
|
||||||
gson.fromJson(text, WsPaymentMessage::class.java)
|
json.decodeFromString(WsPaymentMessage.serializer(), text)
|
||||||
}.getOrNull() ?: return
|
}.getOrNull() ?: return
|
||||||
|
|
||||||
val payment = msg.payment ?: return
|
val payment = msg.payment ?: return
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "9.2.1"
|
agp = "9.2.1"
|
||||||
biometric = "1.2.0-alpha05"
|
biometric = "1.2.0-alpha05"
|
||||||
converterGson = "3.0.0"
|
|
||||||
coreKtx = "1.10.1"
|
coreKtx = "1.10.1"
|
||||||
gson = "2.14.0"
|
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.1.5"
|
junitVersion = "1.1.5"
|
||||||
espressoCore = "3.5.1"
|
espressoCore = "3.5.1"
|
||||||
@@ -34,14 +32,13 @@ benchmark = "1.4.1"
|
|||||||
runner = "1.5.2"
|
runner = "1.5.2"
|
||||||
androidx-test-ext = "1.2.1"
|
androidx-test-ext = "1.2.1"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
|
kotlinx-serialization = "1.8.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
|
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
|
||||||
androidx-compose-animation = { module = "androidx.compose.animation:animation" }
|
androidx-compose-animation = { module = "androidx.compose.animation:animation" }
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
|
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
|
||||||
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
|
|
||||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
@@ -83,6 +80,9 @@ androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-ju
|
|||||||
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
|
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
|
||||||
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext" }
|
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext" }
|
||||||
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
|
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
|
||||||
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||||
|
converter-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" }
|
||||||
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
@@ -93,3 +93,4 @@ protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
|
|||||||
androidx-benchmark = { id = "androidx.benchmark", version.ref = "benchmark" }
|
androidx-benchmark = { id = "androidx.benchmark", version.ref = "benchmark" }
|
||||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
android-test = { id = "com.android.test", version.ref = "agp" }
|
android-test = { id = "com.android.test", version.ref = "agp" }
|
||||||
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user