package com.bitcointxoko.gudariwallet.api import androidx.compose.runtime.Immutable import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor 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 ──────────────────────────────────────────────────────────────────── @Serializable data class WalletResponse( val id: String = "", val name: String = "", val balance: Long = 0L // millisatoshis ) // ── Create invoice ──────────────────────────────────────────────────────────── @Serializable data class CreateInvoiceRequest( val out: Boolean, val amount: Long, // satoshis val memo: String ) @Serializable data class CreateInvoiceResponse( @SerialName("payment_hash") val paymentHash: String = "", @SerialName("payment_request") val paymentRequest: String = "", @SerialName("expiry") val expiry: String? = null ) // ── Pay invoice ─────────────────────────────────────────────────────────────── @Serializable data class PayInvoiceRequest( val out: Boolean, val bolt11: String ) @Serializable data class PayInvoiceResponse( @SerialName("payment_hash") val paymentHash: String = "", @SerialName("checking_id") val checkingId: String = "" ) // ── Check payment ───────────────────────────────────────────────────────────── @Serializable data class PaymentStatusResponse( val paid: Boolean = false, val details: PaymentDetails? = null ) @Serializable data class PaymentDetails( val amount: Long? = null, val fee: Long? = null, val memo: String? = null ) // ── Decode BOLT-11 ──────────────────────────────────────────────────────────── @Serializable data class DecodeInvoiceRequest( val data: String ) @Serializable data class DecodeInvoiceResponse( @SerialName("payment_hash") val paymentHash: String? = null, @SerialName("amount_msat") val amountMsat: Long? = null, @SerialName("description") val description: String? = null, @SerialName("payee") val payee: String? = null, @SerialName("route_hints") val routeHints: List>? = null ) @Serializable data class RouteHintHop( @SerialName("public_key") val publicKey: String = "", @SerialName("short_channel_id") val shortChannelId: String? = null, @SerialName("base_fee") val baseFeeMsat: Long = 0, @SerialName("ppm_fee") val ppmFee: Long = 0, @SerialName("cltv_expiry_delta") val cltvExpiryDelta: Int = 0 ) // ── LNURL scan ──────────────────────────────────────────────────────────────── @Serializable data class LnurlScanResponse( val tag: String? = null, // ── payRequest fields ───────────────────────────────────────────────────── val callback: String? = null, @SerialName("minSendable") val minSendable: Long? = null, @SerialName("maxSendable") val maxSendable: Long? = null, val metadata: String? = null, @SerialName("commentAllowed") val commentAllowed: Int? = null, @SerialName("allowsNostr") val allowsNostr: Boolean? = null, @SerialName("nostrPubkey") val nostrPubkey: String? = null, val bolt11: String? = null, @SerialName("amount_msat") val amountMsat: Long? = null, val disposable: Boolean? = null, // LUD-06: null means treat as true (do not cache) // ── withdrawRequest fields ──────────────────────────────────────────────── val k1: String? = null, @SerialName("defaultDescription") val defaultDescription: String? = null, @SerialName("minWithdrawable") val minWithdrawable: Long? = null, @SerialName("maxWithdrawable") val maxWithdrawable: Long? = null, @SerialName("balanceCheck") val balanceCheck: String? = null, @SerialName("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) ───────────────────────────────────── @Serializable data class LnurlCallbackResponse( val pr: String = "", val routes: List? = null, @SerialName("successAction") val successAction: SuccessAction? = null, val status: String? = null, val reason: String? = null ) // ── LNURL pay ───────────────────────────────────────────────────────────────── @Serializable data class LnurlPayRes( val tag: String? = null, val callback: String? = null, @SerialName("minSendable") val minSendable: Long? = null, @SerialName("maxSendable") val maxSendable: Long? = null, val metadata: String? = null, @SerialName("commentAllowed") val commentAllowed: Int? = null, @SerialName("allowsNostr") val allowsNostr: Boolean? = null, @SerialName("nostrPubkey") val nostrPubkey: String? = null ) @Serializable data class PayLnurlRequest( val res: LnurlPayRes, val lnurl: String, val amount: Long, val comment: String? = null ) @Serializable data class PayLnurlResponse( @SerialName("payment_hash") val paymentHash: String = "", @SerialName("checking_id") val checkingId: String = "" ) @Serializable data class WithdrawCallbackResponse( val status: String = "", val reason: String? = null ) // ── LNURLp pay link (lightning address) ────────────────────────────────────── @Serializable data class LnurlpLink( val id: String = "", val wallet: String = "", val username: String? = null, val description: String? = null, @Serializable(with = LongAsDoubleSerializer::class) val min: Long = 0L, @Serializable(with = LongAsDoubleSerializer::class) val max: Long = 0L ) // ── Payment history ─────────────────────────────────────────────────────────── @Immutable @Serializable data class PaymentRecord( @SerialName("payment_hash") val paymentHash: String = "", @SerialName("checking_id") val checkingId: String = "", @SerialName("amount") val amountMsat: Long = 0L, @SerialName("fee") val feeMsat: Long = 0L, @SerialName("memo") val memo: String? = null, @SerialName("time") val time: String = "", @SerialName("status") val status: String = "", @SerialName("bolt11") val bolt11: String? = null, @SerialName("pending") val pending: Boolean = false, @SerialName("preimage") val preimage: String? = null, @SerialName("extra") val extra: PaymentExtra? = null, // Not from the API — populated only when loaded from Room via toDomain() val createdAt: Long? = null, val pubkey: String? = null, val alias: String? = null ) { val amountSat: Long get() = kotlin.math.abs(amountMsat) / 1000L val feeSat: Long get() = kotlin.math.abs(feeMsat) / 1000L val isOutgoing: Boolean get() = amountMsat < 0 companion object { fun fromPayment( paymentHash: String, amountSats: Long, feeSats: Long?, bolt11: String?, memo: String?, pubkey: String?, alias: String?, ): PaymentRecord = PaymentRecord( paymentHash = paymentHash, checkingId = paymentHash, amountMsat = -(amountSats * 1_000L), feeMsat = (feeSats ?: 0L) * 1_000L, memo = memo, time = (System.currentTimeMillis() / 1_000L).toString(), status = "complete", bolt11 = bolt11, pending = false, preimage = null, extra = null, pubkey = pubkey, alias = alias, ) } } @Immutable @Serializable data class PaymentExtra( @SerialName("tag") val tag: String? = null, @Serializable(with = FlexibleStringSerializer::class) val comment: String? = null, @SerialName("success_action") @Serializable(with = SuccessActionSerializer::class) val successAction: SuccessAction? = null, @SerialName("wallet_id") val walletId: String? = null, @SerialName("destination") val destination: String? = null, @SerialName("expires_at") val expiresAt: String? = null ) @Immutable @Serializable(with = SuccessActionSerializer::class) data class SuccessAction( @SerialName("tag") val tag: String?, @SerialName("message") val message: String?, @SerialName("url") val url: String?, @SerialName("description") val description: String? ) /** * LNbits returns `success_action` as either: * - a JSON object {"tag":"message","message":"Thanks!"} * - a plain string "Thanks!" * - null * * kotlinx.serialization's default deserializer throws when it sees a * string where it expects {. This serializer handles all three cases. * * Applied via @Serializable(with = SuccessActionSerializer::class) on * the SuccessAction class and on PaymentExtra.successAction. */ object SuccessActionSerializer : KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("SuccessAction") override fun serialize(encoder: Encoder, value: SuccessAction?) { val jsonEncoder = encoder as JsonEncoder if (value == null) { jsonEncoder.encodeJsonElement(JsonNull) } 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 deserialize(decoder: Decoder): SuccessAction? { val jsonDecoder = decoder as JsonDecoder return when (val element = jsonDecoder.decodeJsonElement()) { is JsonNull -> null is JsonPrimitive -> { // Plain string — treat as a message-type success action val text = element.jsonPrimitive.content if (text.isBlank()) null else SuccessAction(tag = "message", message = text, url = null, description = null) } is JsonObject -> { SuccessAction( tag = element.jsonObject["tag"]?.jsonPrimitive?.content, message = element.jsonObject["message"]?.jsonPrimitive?.content, url = element.jsonObject["url"]?.jsonPrimitive?.content, description = element.jsonObject["description"]?.jsonPrimitive?.content ) } else -> null } } } @Serializable data class PaymentDetailResponse( val paid: Boolean, val details: PaymentRecord? ) // ── WebSocket payment message ───────────────────────────────────────────────── @Serializable data class WsPaymentMessage( @SerialName("wallet_balance") val walletBalance: Long? = null, val payment: WsPayment? = null ) @Serializable data class WsPayment( @SerialName("payment_hash") val paymentHash: String = "", @SerialName("checking_id") val checkingId: String = "", @SerialName("amount") val amount: Long = 0L, @SerialName("fee") val fee: Long = 0L, @SerialName("memo") val memo: String? = null, @SerialName("time") val time: String = "", @SerialName("status") val status: String = "", @SerialName("bolt11") val bolt11: String? = null, @SerialName("pending") val pending: Boolean = false, @SerialName("preimage") val preimage: String? = null, @SerialName("extra") val extra: PaymentExtra? = null ) // ── Fiat rate ───────────────────────────────────────────────────────────────── @Serializable data class FiatRateResponse( val rate: Double = 0.0, // sat to fiat rate val price: Double = 0.0 // BTC price in the requested fiat currency ) // ── History sync ────────────────────────────────────────────────────────────── @Serializable data class PaginatedPaymentsResponse( @SerialName("data") val data: List = emptyList(), @SerialName("total") val total: Int = 0 ) // ── NWC Provider models ─────────────────────────────────────────────────────── @Serializable data class NwcKey( val pubkey: String = "", val wallet: String = "", val description: String = "", val expires_at: Long = 0L, val permissions: String = "", val created_at: Long = 0L, val last_used: Long = 0L ) @Serializable data class NwcBudget( val id: Int = 0, val pubkey: String = "", val budget_msats: Long = 0L, val refresh_window: Int = 0, val created_at: Long = 0L, val used_budget_msats: Long = 0L ) @Serializable data class NwcGetResponse( val data: NwcKey = NwcKey(), val budgets: List = emptyList() ) // Request models — no defaults needed (you control the fields, never deserializing) @Serializable data class NwcNewBudget( val budget_msats: Long, val refresh_window: Int, val created_at: Long = System.currentTimeMillis() / 1000L ) @Serializable data class NwcRegistrationRequest( val permissions: List, val description: String, val expires_at: Long, val budgets: List = 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 { 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 } } }