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.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()
|
||||
|
||||
|
@ -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.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) {
|
||||
|
@ -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>()
|
||||
|
@ -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.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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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