From 9d984591bc025ce978f0aaf120935d0e5f62b1de Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:49:30 +0100 Subject: [PATCH] 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. --- .../kotlin/fr/acinq/lightning/bin/Api.kt | 19 ++++++- .../db/payments/PaymentsMetadataQueries.kt | 15 +++++- .../lightning/bin/json/JsonSerializers.kt | 8 ++- .../fr/acinq/lightning/cli/PhoenixCli.kt | 52 +++++++++++++++++-- .../fr/acinq/phoenix/db/PaymentsMetadata.sq | 12 +++-- 5 files changed, 90 insertions(+), 16 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt index 5a4db8c..111041c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt @@ -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() 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 index 224776c..9cd19da 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/PaymentsMetadataQueries.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/PaymentsMetadataQueries.kt @@ -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> { + 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 { 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 a199355..f5d4350 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt @@ -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, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt b/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt index 6f42cd9..fbdd5ef 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt @@ -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) = 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() + 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() + 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() + 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() 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() 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 } diff --git a/src/commonMain/sqldelight/paymentsdb/fr/acinq/phoenix/db/PaymentsMetadata.sq b/src/commonMain/sqldelight/paymentsdb/fr/acinq/phoenix/db/PaymentsMetadata.sq index cf9a893..8c92cfd 100644 --- a/src/commonMain/sqldelight/paymentsdb/fr/acinq/phoenix/db/PaymentsMetadata.sq +++ b/src/commonMain/sqldelight/paymentsdb/fr/acinq/phoenix/db/PaymentsMetadata.sq @@ -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 = ?;