Add API endpoint to list payments by external id

Creating an invoice now accepts an optional external id.
Also added new payment endpoints to the CLI.
This commit is contained in:
Dominique Padiou 2024-03-11 20:49:30 +01:00
parent 65b1db5716
commit 9d984591bc
No known key found for this signature in database
GPG Key ID: 574C8C6A1673E987
5 changed files with 90 additions and 16 deletions

View File

@ -98,16 +98,32 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va
val amount = formParameters.getLong("amountSat").sat
val description = formParameters.getString("description")
val invoice = peer.createInvoice(randomBytes32(), amount.toMilliSatoshi(), Either.Left(description))
formParameters["externalId"]?.takeUnless { it.isBlank() }?.let { externalId ->
paymentDb.metadataQueries.insertExternalId(WalletPaymentId.IncomingPaymentId(invoice.paymentHash), externalId)
}
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/incoming") {
val externalId = call.parameters.getString("externalId")
val metadataList = paymentDb.metadataQueries.getByExternalId(externalId)
metadataList.mapNotNull { (paymentId, metadata) ->
when (paymentId) {
is WalletPaymentId.IncomingPaymentId -> paymentDb.getIncomingPayment(paymentId.paymentHash)?.let {
IncomingPayment(it, metadata)
}
else -> null
}
}.let { payments ->
call.respond(payments)
}
}
get("payments/outgoing/{uuid}") {
val uuid = call.parameters.getUUID("uuid")
paymentDb.getLightningOutgoingPayment(uuid)?.let {
@ -188,6 +204,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

@ -2,6 +2,7 @@ package fr.acinq.lightning.bin.db.payments
import fr.acinq.lightning.bin.db.PaymentMetadata
import fr.acinq.lightning.bin.db.WalletPaymentId
import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.phoenix.db.PaymentsDatabase
class PaymentsMetadataQueries(private val database: PaymentsDatabase) {
@ -13,10 +14,20 @@ class PaymentsMetadataQueries(private val database: PaymentsDatabase) {
.executeAsOneOrNull()
}
fun getByExternalId(id: String): List<Pair<WalletPaymentId, PaymentMetadata>> {
return queries.getByExternalId(id).executeAsList().mapNotNull { res ->
WalletPaymentId.create(type = res.type, id = res.id)?.let { it to mapper(res.external_id, res.created_at) }
}
}
fun insertExternalId(walletPaymentId: WalletPaymentId, id: String) {
database.transaction {
queries.insert(type = walletPaymentId.dbType.value, id = walletPaymentId.dbId, external_id = id, created_at = currentTimestampMillis())
}
}
companion object {
fun mapper(
type: Long,
id: String,
external_id: String?,
created_at: Long,
): PaymentMetadata {

View File

@ -93,15 +93,13 @@ sealed class ApiType {
@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) {
data class IncomingPayment(val paymentHash: ByteVector32, val preimage: ByteVector32, val externalId: String?, val description: String?, val invoice: 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
},
description = (payment.origin as? fr.acinq.lightning.db.IncomingPayment.Origin.Invoice)?.paymentRequest?.description,
invoice = (payment.origin as? fr.acinq.lightning.db.IncomingPayment.Origin.Invoice)?.paymentRequest?.write(),
isPaid = payment.completedAt != null,
receivedSat = payment.amount.truncateToSatoshi(),
fees = payment.fees,

View File

@ -15,6 +15,7 @@ import fr.acinq.bitcoin.ByteVector32
import fr.acinq.lightning.bin.conf.readConfFile
import fr.acinq.lightning.bin.homeDirectory
import fr.acinq.lightning.payment.Bolt11Invoice
import fr.acinq.lightning.utils.UUID
import io.ktor.client.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
@ -30,7 +31,7 @@ import kotlinx.serialization.json.Json
fun main(args: Array<String>) =
PhoenixCli()
.subcommands(GetInfo(), GetBalance(), ListChannels(), CreateInvoice(), PayInvoice(), SendToAddress(), CloseChannel())
.subcommands(GetInfo(), GetBalance(), ListChannels(), GetOutgoingPayment(), GetIncomingPayment(), ListIncomingPayments(), CreateInvoice(), PayInvoice(), SendToAddress(), CloseChannel())
.main(args)
data class HttpConf(val baseUrl: Url, val httpClient: HttpClient)
@ -114,16 +115,61 @@ class ListChannels : CliktCommand(name = "listchannels", help = "List all channe
}
}
class GetOutgoingPayment : CliktCommand(name = "getoutgoingpayment", help = "Get outgoing payment") {
private val commonOptions by requireObject<HttpConf>()
private val uuid by option("--uuid").convert { UUID.fromString(it) }.required()
override fun run() {
runBlocking {
val res = commonOptions.httpClient.get(
url = commonOptions.baseUrl / "payments/outgoing/$uuid"
)
echo(res.bodyAsText())
}
}
}
class GetIncomingPayment : CliktCommand(name = "getincomingpayment", help = "Get incoming payment") {
private val commonOptions by requireObject<HttpConf>()
private val paymentHash by option("--paymentHash", "--h").convert { ByteVector32.fromValidHex(it) }.required()
override fun run() {
runBlocking {
val res = commonOptions.httpClient.get(
url = commonOptions.baseUrl / "payments/incoming/$paymentHash"
)
echo(res.bodyAsText())
}
}
}
class ListIncomingPayments : CliktCommand(name = "listincomingpayments", help = "List incoming payments matching the given externalId") {
private val commonOptions by requireObject<HttpConf>()
private val externalId by option("--externalId", "--eid").required()
override fun run() {
runBlocking {
val res = commonOptions.httpClient.get(
url = commonOptions.baseUrl / "payments/incoming",
) {
url {
parameters.append("externalId", externalId)
}
}
echo(res.bodyAsText())
}
}
}
class CreateInvoice : CliktCommand(name = "createinvoice", help = "Create a Lightning invoice", printHelpOnEmptyArgs = true) {
private val commonOptions by requireObject<HttpConf>()
private val amountSat by option("--amountSat").long()
private val description by option("--description", "--desc").required()
private val externalId by option("--externalId")
override fun run() {
runBlocking {
val res = commonOptions.httpClient.submitForm(
url = (commonOptions.baseUrl / "createinvoice").toString(),
formParameters = parameters {
amountSat?.let { append("amountSat", amountSat.toString()) }
amountSat?.let { append("amountSat", it.toString()) }
externalId?.let { append("externalId", it) }
append("description", description)
}
)
@ -170,7 +216,7 @@ class SendToAddress : CliktCommand(name = "sendtoaddress", help = "Send to a Bit
}
}
class CloseChannel : CliktCommand(name = "closechannel", help = "Close all channels", printHelpOnEmptyArgs = true) {
class CloseChannel : CliktCommand(name = "closechannel", help = "Close channel", printHelpOnEmptyArgs = true) {
private val commonOptions by requireObject<HttpConf>()
private val channelId by option("--channelId").convert { ByteVector32.fromValidHex(it) }.required()
private val address by option("--address").required().check { runCatching { Base58Check.decode(it) }.isSuccess || runCatching { Bech32.decodeWitnessAddress(it) }.isSuccess }

View File

@ -10,11 +10,9 @@ CREATE TABLE IF NOT EXISTS payments_metadata (
PRIMARY KEY (type, id)
);
-- queries for payments_metadata tablexxxx`
CREATE INDEX IF NOT EXISTS payments_metadata_external_id ON payments_metadata(external_id);
count:
SELECT COUNT(*) FROM payments_metadata
WHERE type = ? AND id = ?;
-- queries for payments_metadata table
insert:
INSERT INTO payments_metadata (
@ -25,5 +23,9 @@ INSERT INTO payments_metadata (
) VALUES (?, ?, ?, ?);
get:
SELECT * FROM payments_metadata
SELECT external_id, created_at FROM payments_metadata
WHERE type = ? AND id = ?;
getByExternalId:
SELECT type, id, external_id, created_at FROM payments_metadata
WHERE external_id = ?;