From 6e3011d6abeb0faec2217590103cc0ef5fd0b18b Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Tue, 30 Apr 2024 18:58:21 +0200 Subject: [PATCH] Handle new database types for offer payments --- buildSrc/src/main/kotlin/Versions.kt | 2 +- .../details/PaymentDetailsTechnicalView.kt | 66 +++++++++++++++---- .../android/utils/LegacyMigrationHelper.kt | 2 +- .../acinq/phoenix/android/utils/extensions.kt | 3 +- .../src/main/res/values/strings.xml | 5 ++ phoenix-shared/build.gradle.kts | 4 ++ .../controllers/payments/ScanController.kt | 6 +- .../db/payments/IncomingOriginType.kt | 15 +++++ .../db/payments/OutgoingDetailsType.kt | 36 ++++++---- .../fr.acinq.phoenix/utils/CsvWriter.kt | 5 +- 10 files changed, 112 insertions(+), 32 deletions(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 3cf028a18..039c6e3e3 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,5 +1,5 @@ object Versions { - const val lightningKmp = "1.6.1" + const val lightningKmp = "1.6.2-SNAPSHOT" const val secp256k1 = "0.14.0" const val torMobile = "0.2.0" diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsTechnicalView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsTechnicalView.kt index 65f56d99f..b75917266 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsTechnicalView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsTechnicalView.kt @@ -28,10 +28,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.PrivateKey import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.db.* import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.payment.Bolt12Invoice +import fr.acinq.lightning.payment.OfferPaymentMetadata import fr.acinq.lightning.utils.currentTimestampMillis import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sum @@ -133,7 +135,7 @@ private fun HeaderForOutgoing( is LightningOutgoingPayment -> when (payment.details) { is LightningOutgoingPayment.Details.Normal -> stringResource(R.string.paymentdetails_normal_outgoing) is LightningOutgoingPayment.Details.SwapOut -> stringResource(R.string.paymentdetails_swapout) - is LightningOutgoingPayment.Details.KeySend -> stringResource(R.string.paymentdetails_keysend) + is LightningOutgoingPayment.Details.Blinded -> stringResource(id = R.string.paymentdetails_offer_outgoing) } is SpliceCpfpOutgoingPayment -> stringResource(id = R.string.paymentdetails_splice_cpfp_outgoing) is InboundLiquidityOutgoingPayment -> stringResource(id = R.string.paymentdetails_inbound_liquidity) @@ -175,6 +177,7 @@ private fun HeaderForIncoming( is IncomingPayment.Origin.KeySend -> stringResource(R.string.paymentdetails_keysend) is IncomingPayment.Origin.SwapIn -> stringResource(R.string.paymentdetails_swapin) is IncomingPayment.Origin.OnChain -> stringResource(R.string.paymentdetails_swapin) + is IncomingPayment.Origin.Offer -> stringResource(id = R.string.paymentdetails_offer_incoming) } ) } @@ -300,19 +303,14 @@ private fun DetailsForLightningOutgoingPayment( // -- details of the payment when (details) { is LightningOutgoingPayment.Details.Normal -> { - when (val paymentRequest = details.paymentRequest) { - is Bolt11Invoice -> InvoiceSection(invoice = paymentRequest) - is Bolt12Invoice -> { - // TODO - } - } + Bolt11InvoiceSection(invoice = details.paymentRequest) } is LightningOutgoingPayment.Details.SwapOut -> { TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_bitcoin_address_label), value = details.address) TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_payment_hash_label), value = details.paymentHash.toHex()) } - is LightningOutgoingPayment.Details.KeySend -> { - TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_payment_hash_label), value = details.paymentHash.toHex()) + is LightningOutgoingPayment.Details.Blinded -> { + Bolt12InvoiceSection(invoice = details.paymentRequest, payerKey = details.payerKey) } } @@ -408,7 +406,7 @@ private fun DetailsForIncoming( // -- details about the origin of the payment when (val origin = payment.origin) { is IncomingPayment.Origin.Invoice -> { - InvoiceSection(invoice = origin.paymentRequest) + Bolt11InvoiceSection(invoice = origin.paymentRequest) TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_preimage_label), value = payment.preimage.toHex()) } is IncomingPayment.Origin.SwapIn -> { @@ -428,6 +426,9 @@ private fun DetailsForIncoming( } } } + is IncomingPayment.Origin.Offer -> { + Bolt12MetadataSection(metadata = origin.metadata) + } } } @@ -507,7 +508,7 @@ private fun LightningPart( } @Composable -private fun InvoiceSection( +private fun Bolt11InvoiceSection( invoice: Bolt11Invoice ) { val requestedAmount = invoice.amount @@ -530,6 +531,49 @@ private fun InvoiceSection( TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_payment_request_label), value = invoice.write()) } +@Composable +private fun Bolt12InvoiceSection( + invoice: Bolt12Invoice, + payerKey: PrivateKey, +) { + val requestedAmount = invoice.amount + if (requestedAmount != null) { + TechnicalRowAmount( + label = stringResource(id = R.string.paymentdetails_invoice_requested_label), + amount = requestedAmount, + rateThen = null + ) + } + + val description = invoice.description?.takeIf { it.isNotBlank() } + if (description != null) { + TechnicalRow(label = stringResource(id = R.string.paymentdetails_payment_request_description_label)) { + Text(text = description) + } + } + + TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_payerkey_label), value = payerKey.toHex()) + TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_payment_hash_label), value = invoice.paymentHash.toHex()) + TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_payment_request_label), value = invoice.write()) +} + +@Composable +private fun Bolt12MetadataSection( + metadata: OfferPaymentMetadata +) { + TechnicalRowAmount( + label = stringResource(id = R.string.paymentdetails_invoice_requested_label), + amount = metadata.amount, + rateThen = null + ) + TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_payment_hash_label), value = metadata.paymentHash.toHex()) + TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_preimage_label), value = metadata.preimage.toHex()) + TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_offerid_label), value = metadata.offerId.toHex()) + if (metadata is OfferPaymentMetadata.V1) { + TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_payerkey_label), value = metadata.payerKey.toHex()) + } +} + // ============== utility components for this view @Composable diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/LegacyMigrationHelper.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/LegacyMigrationHelper.kt index 63228d378..c3d1a7673 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/LegacyMigrationHelper.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/LegacyMigrationHelper.kt @@ -388,7 +388,7 @@ object LegacyMigrationHelper { } else if (paymentRequest != null) { LightningOutgoingPayment.Details.Normal(paymentRequest) } else { - LightningOutgoingPayment.Details.KeySend(preimage = Lightning.randomBytes32().sha256()) + throw RuntimeException("unhandled outgoing payment details") } val parts = listOfParts.filter { it.paymentType() == PaymentType.Standard() }.map { part -> diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions.kt index 2edd6a44b..31374e913 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions.kt @@ -123,13 +123,14 @@ fun Connection.CLOSED.isBadCertificate() = this.reason?.cause is CertificateExce fun WalletPayment.smartDescription(context: Context): String? = when (this) { is LightningOutgoingPayment -> when (val details = this.details) { is LightningOutgoingPayment.Details.Normal -> details.paymentRequest.desc - is LightningOutgoingPayment.Details.KeySend -> context.getString(R.string.paymentdetails_desc_keysend) is LightningOutgoingPayment.Details.SwapOut -> context.getString(R.string.paymentdetails_desc_swapout, details.address) + is LightningOutgoingPayment.Details.Blinded -> details.paymentRequest.description } is IncomingPayment -> when (val origin = this.origin) { is IncomingPayment.Origin.Invoice -> origin.paymentRequest.description is IncomingPayment.Origin.KeySend -> context.getString(R.string.paymentdetails_desc_keysend) is IncomingPayment.Origin.SwapIn, is IncomingPayment.Origin.OnChain -> context.getString(R.string.paymentdetails_desc_swapin) + is IncomingPayment.Origin.Offer -> context.getString(R.string.paymentdetails_desc_offer_incoming, origin.metadata.offerId.toHex()) } is SpliceOutgoingPayment -> context.getString(R.string.paymentdetails_desc_splice_out) is ChannelCloseOutgoingPayment -> context.getString(R.string.paymentdetails_desc_closing_channel) diff --git a/phoenix-android/src/main/res/values/strings.xml b/phoenix-android/src/main/res/values/strings.xml index e8e77d5ea..b0fa8a28d 100644 --- a/phoenix-android/src/main/res/values/strings.xml +++ b/phoenix-android/src/main/res/values/strings.xml @@ -368,6 +368,7 @@ Swap-out to %1$s On-chain deposit Payment to %1$s + Payment to %1$s Add a custom description to this payment Description @@ -388,6 +389,8 @@ Swap-in Bitcoin deposit Swap-out to Bitcoin address Keysend (spontaneous payment) + Offer outgoing Lightning payment + Offer incoming Lightning payment Deposit address Bitcoin address @@ -410,6 +413,8 @@ Payment Hash Invoice Preimage + Payer key + Offer ID Payment status Successful diff --git a/phoenix-shared/build.gradle.kts b/phoenix-shared/build.gradle.kts index 06ffaef00..18c5ad6bb 100644 --- a/phoenix-shared/build.gradle.kts +++ b/phoenix-shared/build.gradle.kts @@ -181,6 +181,10 @@ if (includeAndroid) { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + lint { + disable.add("Deprecation") + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/ScanController.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/ScanController.kt index b1ad04cf5..9fa498507 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/ScanController.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/ScanController.kt @@ -21,6 +21,7 @@ import fr.acinq.bitcoin.Chain import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.db.LightningOutgoingPayment +import fr.acinq.lightning.io.PayInvoice import fr.acinq.lightning.io.SendPayment import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.utils.* @@ -222,11 +223,10 @@ class AppScanController( } peer.send( - SendPayment( + PayInvoice( paymentId = paymentId, amount = amountToSend, - recipient = invoice.nodeId, - paymentRequest = invoice, + paymentDetails = LightningOutgoingPayment.Details.Normal(paymentRequest = invoice), trampolineFeesOverride = listOf(trampolineFees) ) ) diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/IncomingOriginType.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/IncomingOriginType.kt index b4c6321e8..925a93669 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/IncomingOriginType.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/IncomingOriginType.kt @@ -17,17 +17,21 @@ @file:UseSerializers( OutpointSerializer::class, ByteVector32Serializer::class, + ByteVectorSerializer::class, ) package fr.acinq.phoenix.db.payments +import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.OutPoint import fr.acinq.bitcoin.TxId import fr.acinq.lightning.db.IncomingPayment import fr.acinq.lightning.payment.Bolt11Invoice +import fr.acinq.lightning.payment.OfferPaymentMetadata import fr.acinq.phoenix.db.payments.DbTypesHelper.decodeBlob import fr.acinq.phoenix.db.serializers.v1.ByteVector32Serializer +import fr.acinq.phoenix.db.serializers.v1.ByteVectorSerializer import fr.acinq.phoenix.db.serializers.v1.OutpointSerializer import io.ktor.utils.io.charsets.* import io.ktor.utils.io.core.* @@ -40,6 +44,7 @@ enum class IncomingOriginTypeVersion { INVOICE_V0, SWAPIN_V0, ONCHAIN_V0, + OFFER_V0, } sealed class IncomingOriginData { @@ -65,6 +70,11 @@ sealed class IncomingOriginData { data class V0(@Serializable val txId: ByteVector32, val outpoints: List<@Serializable OutPoint>) : SwapIn() } + sealed class Offer : IncomingOriginData() { + @Serializable + data class V0(@Serializable val encodedMetadata: ByteVector) : Offer() + } + companion object { fun deserialize(typeVersion: IncomingOriginTypeVersion, blob: ByteArray): IncomingPayment.Origin = decodeBlob(blob) { json, format -> when (typeVersion) { @@ -74,6 +84,9 @@ sealed class IncomingOriginData { IncomingOriginTypeVersion.ONCHAIN_V0 -> format.decodeFromString(json).let { IncomingPayment.Origin.OnChain(TxId(it.txId), it.outpoints.toSet()) } + IncomingOriginTypeVersion.OFFER_V0 -> format.decodeFromString(json).let { + IncomingPayment.Origin.Offer(metadata = OfferPaymentMetadata.decode(it.encodedMetadata)) + } } } } @@ -88,4 +101,6 @@ fun IncomingPayment.Origin.mapToDb(): Pair Json.encodeToString(IncomingOriginData.SwapIn.V0(address)).toByteArray(Charsets.UTF_8) is IncomingPayment.Origin.OnChain -> IncomingOriginTypeVersion.ONCHAIN_V0 to Json.encodeToString(IncomingOriginData.OnChain.V0(txId.value, localInputs.toList())).toByteArray(Charsets.UTF_8) + is IncomingPayment.Origin.Offer -> IncomingOriginTypeVersion.OFFER_V0 to + Json.encodeToString(IncomingOriginData.Offer.V0(metadata.encode())).toByteArray(Charsets.UTF_8) } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/OutgoingDetailsType.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/OutgoingDetailsType.kt index 49be4761e..e3534a847 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/OutgoingDetailsType.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/OutgoingDetailsType.kt @@ -22,25 +22,27 @@ package fr.acinq.phoenix.db.payments import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.PrivateKey import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.db.LightningOutgoingPayment import fr.acinq.lightning.payment.Bolt11Invoice +import fr.acinq.lightning.payment.Bolt12Invoice import fr.acinq.phoenix.db.serializers.v1.ByteVector32Serializer import fr.acinq.phoenix.db.serializers.v1.SatoshiSerializer import io.ktor.utils.io.charsets.* import io.ktor.utils.io.core.* import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers +import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json - enum class OutgoingDetailsTypeVersion { NORMAL_V0, - KEYSEND_V0, SWAPOUT_V0, @Deprecated("channel close are now stored in their own table") CLOSING_V0, + BLINDED_V0, } sealed class OutgoingDetailsData { @@ -50,20 +52,19 @@ sealed class OutgoingDetailsData { data class V0(val paymentRequest: String) : Normal() } - sealed class KeySend : OutgoingDetailsData() { + sealed class SwapOut : OutgoingDetailsData() { @Serializable - data class V0(@Serializable val preimage: ByteVector32) : KeySend() + data class V0(val address: String, val paymentRequest: String, @Serializable val swapOutFee: Satoshi) : SwapOut() } - sealed class SwapOut : OutgoingDetailsData() { + sealed class Blinded : OutgoingDetailsData() { @Serializable - data class V0(val address: String, val paymentRequest: String, @Serializable val swapOutFee: Satoshi) : SwapOut() + data class V0(val paymentRequest: String, val payerKey: String) : Blinded() } @Deprecated("channel close are now stored in their own table") sealed class Closing : OutgoingDetailsData() { @Serializable - @Suppress("DEPRECATION") data class V0( @Serializable val channelId: ByteVector32, val closingAddress: String, @@ -73,18 +74,25 @@ sealed class OutgoingDetailsData { companion object { /** Deserialize the details of an outgoing payment. Return null if the details is for a legacy channel closing payment (see [deserializeLegacyClosingDetails]). */ - @Suppress("DEPRECATION") fun deserialize(typeVersion: OutgoingDetailsTypeVersion, blob: ByteArray): LightningOutgoingPayment.Details? = DbTypesHelper.decodeBlob(blob) { json, format -> when (typeVersion) { - OutgoingDetailsTypeVersion.NORMAL_V0 -> format.decodeFromString(json).let { LightningOutgoingPayment.Details.Normal(Bolt11Invoice.read(it.paymentRequest).get()) } - OutgoingDetailsTypeVersion.KEYSEND_V0 -> format.decodeFromString(json).let { LightningOutgoingPayment.Details.KeySend(it.preimage) } - OutgoingDetailsTypeVersion.SWAPOUT_V0 -> format.decodeFromString(json).let { LightningOutgoingPayment.Details.SwapOut(it.address, Bolt11Invoice.read(it.paymentRequest).get(), it.swapOutFee) } + OutgoingDetailsTypeVersion.NORMAL_V0 -> format.decodeFromString(json).let { + LightningOutgoingPayment.Details.Normal(Bolt11Invoice.read(it.paymentRequest).get()) + } + OutgoingDetailsTypeVersion.SWAPOUT_V0 -> format.decodeFromString(json).let { + LightningOutgoingPayment.Details.SwapOut(it.address, Bolt11Invoice.read(it.paymentRequest).get(), it.swapOutFee) + } OutgoingDetailsTypeVersion.CLOSING_V0 -> null + OutgoingDetailsTypeVersion.BLINDED_V0 -> format.decodeFromString(json).let { + LightningOutgoingPayment.Details.Blinded( + paymentRequest = Bolt12Invoice.fromString(it.paymentRequest).get(), + payerKey = PrivateKey.fromHex(it.payerKey), + ) + } } } /** Returns the channel closing details from a blob, for backward-compatibility purposes. */ - @Suppress("DEPRECATION") fun deserializeLegacyClosingDetails(blob: ByteArray): Closing.V0 = DbTypesHelper.decodeBlob(blob) { json, format -> format.decodeFromString(json) } @@ -94,8 +102,8 @@ sealed class OutgoingDetailsData { fun LightningOutgoingPayment.Details.mapToDb(): Pair = when (this) { is LightningOutgoingPayment.Details.Normal -> OutgoingDetailsTypeVersion.NORMAL_V0 to Json.encodeToString(OutgoingDetailsData.Normal.V0(paymentRequest.write())).toByteArray(Charsets.UTF_8) - is LightningOutgoingPayment.Details.KeySend -> OutgoingDetailsTypeVersion.KEYSEND_V0 to - Json.encodeToString(OutgoingDetailsData.KeySend.V0(preimage)).toByteArray(Charsets.UTF_8) is LightningOutgoingPayment.Details.SwapOut -> OutgoingDetailsTypeVersion.SWAPOUT_V0 to Json.encodeToString(OutgoingDetailsData.SwapOut.V0(address, paymentRequest.write(), swapOutFee)).toByteArray(Charsets.UTF_8) + is LightningOutgoingPayment.Details.Blinded -> OutgoingDetailsTypeVersion.BLINDED_V0 to + Json.encodeToString(OutgoingDetailsData.Blinded.V0(paymentRequest.write(), payerKey.toHex())).toByteArray(Charsets.UTF_8) } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/CsvWriter.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/CsvWriter.kt index a2943c4f9..7a88ff8c4 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/CsvWriter.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/CsvWriter.kt @@ -128,11 +128,14 @@ class CsvWriter { is IncomingPayment.Origin.OnChain -> { "Swap-in with inputs: ${origin.localInputs.map { it.txid.toString() } }" } + is IncomingPayment.Origin.Offer -> { + "Incoming offer ${origin.metadata.offerId}" + } } is LightningOutgoingPayment -> when (val details = payment.details) { is LightningOutgoingPayment.Details.Normal -> "Outgoing LN payment to ${details.paymentRequest.nodeId.toHex()}" - is LightningOutgoingPayment.Details.KeySend -> "Outgoing LN payment (keysend)" is LightningOutgoingPayment.Details.SwapOut -> "Swap-out to ${details.address}" + is LightningOutgoingPayment.Details.Blinded -> "Offer to ${details.payerKey.publicKey()}" } is SpliceOutgoingPayment -> "Outgoing splice to ${payment.address}" is ChannelCloseOutgoingPayment -> "Channel closing to ${payment.address}"