Add table storing payments metadata
This commit is contained in:
parent
e6e6707685
commit
3027bd1aca
@ -8,6 +8,8 @@ import fr.acinq.bitcoin.utils.Either
|
|||||||
import fr.acinq.bitcoin.utils.toEither
|
import fr.acinq.bitcoin.utils.toEither
|
||||||
import fr.acinq.lightning.Lightning.randomBytes32
|
import fr.acinq.lightning.Lightning.randomBytes32
|
||||||
import fr.acinq.lightning.NodeParams
|
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.bin.json.ApiType.*
|
||||||
import fr.acinq.lightning.blockchain.fee.FeeratePerByte
|
import fr.acinq.lightning.blockchain.fee.FeeratePerByte
|
||||||
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
|
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.Peer
|
||||||
import fr.acinq.lightning.io.WrappedChannelCommand
|
import fr.acinq.lightning.io.WrappedChannelCommand
|
||||||
import fr.acinq.lightning.payment.Bolt11Invoice
|
import fr.acinq.lightning.payment.Bolt11Invoice
|
||||||
import fr.acinq.lightning.utils.sat
|
import fr.acinq.lightning.utils.*
|
||||||
import fr.acinq.lightning.utils.sum
|
|
||||||
import fr.acinq.lightning.utils.toByteVector
|
|
||||||
import fr.acinq.lightning.utils.toMilliSatoshi
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.http.*
|
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))
|
val invoice = peer.createInvoice(randomBytes32(), amount.toMilliSatoshi(), Either.Left(description))
|
||||||
call.respond(GeneratedInvoice(invoice.amount?.truncateToSatoshi(), invoice.paymentHash, serialized = invoice.write()))
|
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") {
|
post("payinvoice") {
|
||||||
val formParameters = call.receiveParameters()
|
val formParameters = call.receiveParameters()
|
||||||
val overrideAmount = formParameters["amountSat"]?.let { it.toLongOrNull() ?: invalidType("amountSat", "integer") }?.sat?.toMilliSatoshi()
|
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 missing(argName: String): Nothing = throw MissingRequestParameterException(argName)
|
||||||
|
|
||||||
private fun invalidType(argName: String, typeName: String): Nothing = throw ParameterConversionException(argName, typeName)
|
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.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.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()
|
private fun Parameters.getAddressAndConvertToScript(argName: String): ByteVector = Script.write(Bitcoin.addressToPublicKeyScript(nodeParams.chainHash, getString(argName)).right ?: error("invalid address")).toByteVector()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
)
|
@ -24,15 +24,16 @@ import fr.acinq.bitcoin.TxId
|
|||||||
import fr.acinq.bitcoin.utils.Either
|
import fr.acinq.bitcoin.utils.Either
|
||||||
import fr.acinq.lightning.bin.db.payments.*
|
import fr.acinq.lightning.bin.db.payments.*
|
||||||
import fr.acinq.lightning.bin.db.payments.LinkTxToPaymentQueries
|
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.channel.ChannelException
|
||||||
import fr.acinq.lightning.db.*
|
import fr.acinq.lightning.db.*
|
||||||
import fr.acinq.lightning.logging.LoggerFactory
|
import fr.acinq.lightning.logging.LoggerFactory
|
||||||
|
import fr.acinq.lightning.logging.info
|
||||||
import fr.acinq.lightning.payment.FinalFailure
|
import fr.acinq.lightning.payment.FinalFailure
|
||||||
import fr.acinq.lightning.utils.*
|
import fr.acinq.lightning.utils.*
|
||||||
import fr.acinq.lightning.wire.FailureMessage
|
import fr.acinq.lightning.wire.FailureMessage
|
||||||
import fr.acinq.phoenix.db.*
|
import fr.acinq.phoenix.db.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class SqlitePaymentsDb(
|
class SqlitePaymentsDb(
|
||||||
@ -67,13 +68,14 @@ class SqlitePaymentsDb(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
internal val inQueries = IncomingQueries(database)
|
private val inQueries = IncomingQueries(database)
|
||||||
internal val outQueries = OutgoingQueries(database)
|
private val outQueries = OutgoingQueries(database)
|
||||||
private val spliceOutQueries = SpliceOutgoingQueries(database)
|
private val spliceOutQueries = SpliceOutgoingQueries(database)
|
||||||
private val channelCloseQueries = ChannelCloseOutgoingQueries(database)
|
private val channelCloseQueries = ChannelCloseOutgoingQueries(database)
|
||||||
private val cpfpQueries = SpliceCpfpOutgoingQueries(database)
|
private val cpfpQueries = SpliceCpfpOutgoingQueries(database)
|
||||||
private val linkTxToPaymentQueries = LinkTxToPaymentQueries(database)
|
private val linkTxToPaymentQueries = LinkTxToPaymentQueries(database)
|
||||||
private val inboundLiquidityQueries = InboundLiquidityQueries(database)
|
private val inboundLiquidityQueries = InboundLiquidityQueries(database)
|
||||||
|
val metadataQueries = PaymentsMetadataQueries(database)
|
||||||
|
|
||||||
override suspend fun addOutgoingLightningParts(
|
override suspend fun addOutgoingLightningParts(
|
||||||
parentId: UUID,
|
parentId: UUID,
|
||||||
@ -161,7 +163,7 @@ class SqlitePaymentsDb(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getLightningOutgoingPayment(id: UUID): LightningOutgoingPayment? = withContext(Dispatchers.Default) {
|
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) {
|
override suspend fun getLightningOutgoingPaymentFromPartId(partId: UUID): LightningOutgoingPayment? = withContext(Dispatchers.Default) {
|
||||||
|
@ -23,7 +23,6 @@ import fr.acinq.bitcoin.utils.Either
|
|||||||
import fr.acinq.lightning.MilliSatoshi
|
import fr.acinq.lightning.MilliSatoshi
|
||||||
import fr.acinq.lightning.ShortChannelId
|
import fr.acinq.lightning.ShortChannelId
|
||||||
import fr.acinq.lightning.channel.ChannelException
|
import fr.acinq.lightning.channel.ChannelException
|
||||||
import fr.acinq.lightning.db.ChannelCloseOutgoingPayment
|
|
||||||
import fr.acinq.lightning.db.HopDesc
|
import fr.acinq.lightning.db.HopDesc
|
||||||
import fr.acinq.lightning.db.LightningOutgoingPayment
|
import fr.acinq.lightning.db.LightningOutgoingPayment
|
||||||
import fr.acinq.lightning.db.OutgoingPayment
|
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
|
* Returns a [LightningOutgoingPayment] for this id.
|
||||||
* [ChannelCloseOutgoingPayment], this payment is ignored and we return null instead.
|
|
||||||
*/
|
*/
|
||||||
fun getPaymentStrict(id: UUID): LightningOutgoingPayment? = queries.getPayment(
|
fun getPayment(id: UUID): LightningOutgoingPayment? = queries.getPayment(
|
||||||
id = id.toString(),
|
id = id.toString(),
|
||||||
mapper = Companion::mapLightningOutgoingPayment
|
mapper = Companion::mapLightningOutgoingPayment
|
||||||
).executeAsList().let { parts ->
|
).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> {
|
fun listLightningOutgoingPayments(paymentHash: ByteVector32): List<LightningOutgoingPayment> {
|
||||||
return queries.listPaymentsForPaymentHash(paymentHash.toByteArray(), Companion::mapLightningOutgoingPayment).executeAsList()
|
return queries.listPaymentsForPaymentHash(paymentHash.toByteArray(), Companion::mapLightningOutgoingPayment).executeAsList()
|
||||||
.filterIsInstance<LightningOutgoingPayment>()
|
.filterIsInstance<LightningOutgoingPayment>()
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,8 @@ import fr.acinq.bitcoin.ByteVector32
|
|||||||
import fr.acinq.bitcoin.PublicKey
|
import fr.acinq.bitcoin.PublicKey
|
||||||
import fr.acinq.bitcoin.Satoshi
|
import fr.acinq.bitcoin.Satoshi
|
||||||
import fr.acinq.bitcoin.TxId
|
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.ChannelState
|
||||||
import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
|
import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
|
||||||
import fr.acinq.lightning.db.LightningOutgoingPayment
|
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())
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
@ -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 = ?;
|
Loading…
x
Reference in New Issue
Block a user