Files
gudari/app/src/main/java/com/bitcointxoko/gudariwallet/api/Models.kt
T
2026-06-12 18:15:04 +02:00

409 lines
17 KiB
Kotlin

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<List<RouteHintHop>>? = 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<JsonElement>? = 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<SuccessAction?> {
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<PaymentRecord> = 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<NwcBudget> = 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<String>,
val description: String,
val expires_at: Long,
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
}
}
}