Add table storing payments metadata

This commit is contained in:
Dominique Padiou 2024-03-11 17:58:42 +01:00
parent e6e6707685
commit 3027bd1aca
No known key found for this signature in database
GPG Key ID: 574C8C6A1673E987
7 changed files with 131 additions and 40 deletions

View File

@ -8,6 +8,8 @@ import fr.acinq.bitcoin.utils.Either
import fr.acinq.bitcoin.utils.toEither
import fr.acinq.lightning.Lightning.randomBytes32
import fr.acinq.lightning.NodeParams
import fr.acinq.lightning.bin.db.SqlitePaymentsDb
import fr.acinq.lightning.bin.db.WalletPaymentId
import fr.acinq.lightning.bin.json.ApiType.*
import fr.acinq.lightning.blockchain.fee.FeeratePerByte
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
@ -17,10 +19,7 @@ import fr.acinq.lightning.channel.states.ClosingFeerates
import fr.acinq.lightning.io.Peer
import fr.acinq.lightning.io.WrappedChannelCommand
import fr.acinq.lightning.payment.Bolt11Invoice
import fr.acinq.lightning.utils.sat
import fr.acinq.lightning.utils.sum
import fr.acinq.lightning.utils.toByteVector
import fr.acinq.lightning.utils.toMilliSatoshi
import fr.acinq.lightning.utils.*
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.http.*
@ -101,6 +100,20 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
val invoice = peer.createInvoice(randomBytes32(), amount.toMilliSatoshi(), Either.Left(description))
call.respond(GeneratedInvoice(invoice.amount?.truncateToSatoshi(), invoice.paymentHash, serialized = invoice.write()))
}
get("payments/incoming/{paymentHash}") {
val paymentHash = call.parameters.getByteVector32("paymentHash")
println("fetching details for payment=$paymentHash")
paymentDb.getIncomingPayment(paymentHash)?.let {
val metadata = paymentDb.metadataQueries.get(WalletPaymentId.IncomingPaymentId(paymentHash))
call.respond(IncomingPayment(it, metadata))
} ?: call.respond(HttpStatusCode.NotFound)
}
get("payments/outgoing/{uuid}") {
val uuid = call.parameters.getUUID("uuid")
paymentDb.getLightningOutgoingPayment(uuid)?.let {
call.respond(OutgoingPayment(it))
} ?: call.respond(HttpStatusCode.NotFound)
}
post("payinvoice") {
val formParameters = call.receiveParameters()
val overrideAmount = formParameters["amountSat"]?.let { it.toLongOrNull() ?: invalidType("amountSat", "integer") }?.sat?.toMilliSatoshi()
@ -166,6 +179,8 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
}
}
private val paymentDb: SqlitePaymentsDb by lazy { peer.db.payments as SqlitePaymentsDb }
private fun missing(argName: String): Nothing = throw MissingRequestParameterException(argName)
private fun invalidType(argName: String, typeName: String): Nothing = throw ParameterConversionException(argName, typeName)
@ -173,6 +188,7 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
private fun Parameters.getString(argName: String): String = (this[argName] ?: missing(argName))
private fun Parameters.getByteVector32(argName: String): ByteVector32 = getString(argName).let { hex -> kotlin.runCatching { ByteVector32.fromValidHex(hex) }.getOrNull() ?: invalidType(argName, "hex32") }
private fun Parameters.getUUID(argName: String): UUID = getString(argName).let { uuid -> kotlin.runCatching { UUID.fromString(uuid) }.getOrNull() ?: invalidType(argName, "uuid") }
private fun Parameters.getAddressAndConvertToScript(argName: String): ByteVector = Script.write(Bitcoin.addressToPublicKeyScript(nodeParams.chainHash, getString(argName)).right ?: error("invalid address")).toByteVector()

View File

@ -0,0 +1,13 @@
package fr.acinq.lightning.bin.db
/**
* Metadata for a given payment, incoming or outgoing.
*
* @param externalId A custom identifier used in an external system and attached to a payment at creation.
* Useful to track a payment from the outside.
* @param createdAt Timestamp in millis when the metadata was created.
*/
data class PaymentMetadata(
val externalId: String?,
val createdAt: Long
)

View File

@ -24,15 +24,16 @@ import fr.acinq.bitcoin.TxId
import fr.acinq.bitcoin.utils.Either
import fr.acinq.lightning.bin.db.payments.*
import fr.acinq.lightning.bin.db.payments.LinkTxToPaymentQueries
import fr.acinq.lightning.bin.db.payments.PaymentsMetadataQueries
import fr.acinq.lightning.channel.ChannelException
import fr.acinq.lightning.db.*
import fr.acinq.lightning.logging.LoggerFactory
import fr.acinq.lightning.logging.info
import fr.acinq.lightning.payment.FinalFailure
import fr.acinq.lightning.utils.*
import fr.acinq.lightning.wire.FailureMessage
import fr.acinq.phoenix.db.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class SqlitePaymentsDb(
@ -67,13 +68,14 @@ class SqlitePaymentsDb(
)
)
internal val inQueries = IncomingQueries(database)
internal val outQueries = OutgoingQueries(database)
private val inQueries = IncomingQueries(database)
private val outQueries = OutgoingQueries(database)
private val spliceOutQueries = SpliceOutgoingQueries(database)
private val channelCloseQueries = ChannelCloseOutgoingQueries(database)
private val cpfpQueries = SpliceCpfpOutgoingQueries(database)
private val linkTxToPaymentQueries = LinkTxToPaymentQueries(database)
private val inboundLiquidityQueries = InboundLiquidityQueries(database)
val metadataQueries = PaymentsMetadataQueries(database)
override suspend fun addOutgoingLightningParts(
parentId: UUID,
@ -161,7 +163,7 @@ class SqlitePaymentsDb(
}
override suspend fun getLightningOutgoingPayment(id: UUID): LightningOutgoingPayment? = withContext(Dispatchers.Default) {
outQueries.getPaymentStrict(id)
outQueries.getPayment(id)
}
override suspend fun getLightningOutgoingPaymentFromPartId(partId: UUID): LightningOutgoingPayment? = withContext(Dispatchers.Default) {

View File

@ -23,7 +23,6 @@ import fr.acinq.bitcoin.utils.Either
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.ShortChannelId
import fr.acinq.lightning.channel.ChannelException
import fr.acinq.lightning.db.ChannelCloseOutgoingPayment
import fr.acinq.lightning.db.HopDesc
import fr.acinq.lightning.db.LightningOutgoingPayment
import fr.acinq.lightning.db.OutgoingPayment
@ -150,18 +149,10 @@ class OutgoingQueries(val database: PaymentsDatabase) {
}
}
fun getPaymentWithoutParts(id: UUID): LightningOutgoingPayment? {
return queries.getPaymentWithoutParts(
id = id.toString(),
mapper = Companion::mapLightningOutgoingPaymentWithoutParts
).executeAsOneOrNull()
}
/**
* Returns a [LightningOutgoingPayment] for this id - if instead we find legacy converted to a new type (such as
* [ChannelCloseOutgoingPayment], this payment is ignored and we return null instead.
* Returns a [LightningOutgoingPayment] for this id.
*/
fun getPaymentStrict(id: UUID): LightningOutgoingPayment? = queries.getPayment(
fun getPayment(id: UUID): LightningOutgoingPayment? = queries.getPayment(
id = id.toString(),
mapper = Companion::mapLightningOutgoingPayment
).executeAsList().let { parts ->
@ -173,27 +164,6 @@ class OutgoingQueries(val database: PaymentsDatabase) {
}
}
/**
* May return a [ChannelCloseOutgoingPayment] instead of the expected [LightningOutgoingPayment]. That's because
* channel closing used to be stored as [LightningOutgoingPayment] with special closing parts. We convert those to
* the propert object type.
*/
fun getPaymentRelaxed(id: UUID): OutgoingPayment? = queries.getPayment(
id = id.toString(),
mapper = Companion::mapLightningOutgoingPayment
).executeAsList().let { parts ->
// this payment may be a legacy channel closing - otherwise, only take regular LN payment parts, and group them
parts.firstOrNull { it is ChannelCloseOutgoingPayment } ?: parts.filterIsInstance<LightningOutgoingPayment>().let {
groupByRawLightningOutgoing(it).firstOrNull()
}?.let {
filterUselessParts(it)
}
}
fun getOldestCompletedDate(): Long? {
return queries.getOldestCompletedDate().executeAsOneOrNull()
}
fun listLightningOutgoingPayments(paymentHash: ByteVector32): List<LightningOutgoingPayment> {
return queries.listPaymentsForPaymentHash(paymentHash.toByteArray(), Companion::mapLightningOutgoingPayment).executeAsList()
.filterIsInstance<LightningOutgoingPayment>()

View File

@ -0,0 +1,26 @@
package fr.acinq.lightning.bin.db.payments
import fr.acinq.lightning.bin.db.PaymentMetadata
import fr.acinq.lightning.bin.db.WalletPaymentId
import fr.acinq.phoenix.db.PaymentsDatabase
class PaymentsMetadataQueries(private val database: PaymentsDatabase) {
private val queries = database.paymentsMetadataQueries
fun get(walletPaymentId: WalletPaymentId): PaymentMetadata? {
return queries.get(walletPaymentId.dbType.value, walletPaymentId.dbId, mapper = ::mapper)
.executeAsOneOrNull()
}
companion object {
fun mapper(
type: Long,
id: String,
external_id: String?,
created_at: Long,
): PaymentMetadata {
return PaymentMetadata(externalId = external_id, createdAt = created_at)
}
}
}

View File

@ -17,6 +17,8 @@ import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.PublicKey
import fr.acinq.bitcoin.Satoshi
import fr.acinq.bitcoin.TxId
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.bin.db.PaymentMetadata
import fr.acinq.lightning.channel.states.ChannelState
import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
import fr.acinq.lightning.db.LightningOutgoingPayment
@ -89,4 +91,37 @@ sealed class ApiType {
constructor(event: fr.acinq.lightning.io.PaymentNotSent) : this(event.request.paymentHash, event.reason.reason.toString())
}
@Serializable
@SerialName("incoming_payment")
data class IncomingPayment(val paymentHash: ByteVector32, val preimage: ByteVector32, val externalId: String?, val description: String?, val isPaid: Boolean, val receivedSat: Satoshi, val fees: MilliSatoshi, val completedAt: Long?, val createdAt: Long) {
constructor(payment: fr.acinq.lightning.db.IncomingPayment, metadata: PaymentMetadata?) : this (
paymentHash = payment.paymentHash,
preimage = payment.preimage,
externalId = metadata?.externalId,
description = when (val or = payment.origin) {
is fr.acinq.lightning.db.IncomingPayment.Origin.Invoice -> or.paymentRequest.description
else -> null
},
isPaid = payment.completedAt != null,
receivedSat = payment.amount.truncateToSatoshi(),
fees = payment.fees,
completedAt = payment.completedAt,
createdAt = payment.createdAt,
)
}
@Serializable
@SerialName("outgoing_payment")
data class OutgoingPayment(val paymentHash: ByteVector32, val preimage: ByteVector32?, val isPaid: Boolean, val sent: Satoshi, val fees: MilliSatoshi, val invoice: String?, val completedAt: Long?, val createdAt: Long) {
constructor(payment: LightningOutgoingPayment) : this (
paymentHash = payment.paymentHash,
preimage = (payment.status as? LightningOutgoingPayment.Status.Completed.Succeeded.OffChain)?.preimage,
invoice = (payment.details as? LightningOutgoingPayment.Details.Normal)?.paymentRequest?.write(),
isPaid = payment.completedAt != null,
sent = payment.amount.truncateToSatoshi(),
fees = payment.fees,
completedAt = payment.completedAt,
createdAt = payment.createdAt,
)
}
}

View File

@ -0,0 +1,29 @@
-- This table stores metadata corresponding to a payment. Does not contain critical data.
-- * type => the type of a payment, an int as defined in the DbType enum
-- * id => the internal identifier of a payment, can be a payment hash (incoming) or a UUID (outgoing)
-- * external_id => an arbitrary string defined by the user to track the payment in their own system
CREATE TABLE IF NOT EXISTS payments_metadata (
type INTEGER NOT NULL,
id TEXT NOT NULL,
external_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (type, id)
);
-- queries for payments_metadata tablexxxx`
count:
SELECT COUNT(*) FROM payments_metadata
WHERE type = ? AND id = ?;
insert:
INSERT INTO payments_metadata (
type,
id,
external_id,
created_at
) VALUES (?, ?, ?, ?);
get:
SELECT * FROM payments_metadata
WHERE type = ? AND id = ?;