From 3027bd1aca48701a83615df94577aee3c906a77d Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:58:42 +0100 Subject: [PATCH] Add table storing payments metadata --- .../kotlin/fr/acinq/lightning/bin/Api.kt | 24 ++++++++++--- .../lightning/bin/db/PaymentsMetadata.kt | 13 +++++++ .../lightning/bin/db/SqlitePaymentsDb.kt | 10 +++--- .../bin/db/payments/OutgoingQueries.kt | 34 ++---------------- .../db/payments/PaymentsMetadataQueries.kt | 26 ++++++++++++++ .../lightning/bin/json/JsonSerializers.kt | 35 +++++++++++++++++++ .../fr/acinq/phoenix/db/PaymentsMetadata.sq | 29 +++++++++++++++ 7 files changed, 131 insertions(+), 40 deletions(-) create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/PaymentsMetadata.kt create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/PaymentsMetadataQueries.kt create mode 100644 src/commonMain/sqldelight/paymentsdb/fr/acinq/phoenix/db/PaymentsMetadata.sq diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt index 37baaba..5a4db8c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt @@ -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() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/PaymentsMetadata.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/PaymentsMetadata.kt new file mode 100644 index 0000000..1e2396a --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/PaymentsMetadata.kt @@ -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 +) \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt index 390fbe3..05950fa 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt @@ -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) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/OutgoingQueries.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/OutgoingQueries.kt index 2b2db72..5f79c77 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/OutgoingQueries.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/OutgoingQueries.kt @@ -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().let { - groupByRawLightningOutgoing(it).firstOrNull() - }?.let { - filterUselessParts(it) - } - } - - fun getOldestCompletedDate(): Long? { - return queries.getOldestCompletedDate().executeAsOneOrNull() - } - fun listLightningOutgoingPayments(paymentHash: ByteVector32): List { return queries.listPaymentsForPaymentHash(paymentHash.toByteArray(), Companion::mapLightningOutgoingPayment).executeAsList() .filterIsInstance() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/PaymentsMetadataQueries.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/PaymentsMetadataQueries.kt new file mode 100644 index 0000000..224776c --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/PaymentsMetadataQueries.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt index b45bb1f..a199355 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/src/commonMain/sqldelight/paymentsdb/fr/acinq/phoenix/db/PaymentsMetadata.sq b/src/commonMain/sqldelight/paymentsdb/fr/acinq/phoenix/db/PaymentsMetadata.sq new file mode 100644 index 0000000..cf9a893 --- /dev/null +++ b/src/commonMain/sqldelight/paymentsdb/fr/acinq/phoenix/db/PaymentsMetadata.sq @@ -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 = ?;