From 2dbdeb4a2911757a880eb5fd71ceee374fcdf7de Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:50:05 +0200 Subject: [PATCH 01/25] Update phoenix-shared to new on-the-fly - Updated the liquidity policy (see NodeParamsManager). We are using a policy that does not target additional liquidity and that does not use fee credit yet. - Removed the code updating the peer's swap feerate (it's now provided by the LSP) - Updated the received-with database object with new types: incoming LN payments may contain a funding fee ; payments may be received through a fee credit (not enabled for now). - Updated the liquidity-purchase database objects with new purchase & payment-details type. The lease data are now legacy and are removed wherever possible. Note: for the inbound liquidity db objects, we are moving away from the type_version pattern for serialisation. Instead the json data type is embedded inside the json by the kotlinx serialisation library. This makes the code less verbose. - Added a liquidity purchase wrapper for cloud data. - Updated the Notification types with new rejection options. We try to match in this update the serialisation pattern that was mentioned above. --- .../fr/acinq/phoenix/db/androidDbFactory.kt | 13 +- .../fr.acinq.phoenix.db/Notifications.sq | 4 +- .../fr.acinq.phoenix/data/Notification.kt | 18 ++- .../kotlin/fr.acinq.phoenix/db/SqliteAppDb.kt | 3 - .../fr.acinq.phoenix/db/SqlitePaymentsDb.kt | 9 +- .../db/cloud/payments/CloudData.kt | 15 ++- .../InboundLiquidityPaymentWrapper.kt | 127 +++++++++--------- .../db/notifications/NotificationDataType.kt | 77 +++++------ .../db/notifications/NotificationsQueries.kt | 112 ++++++++------- .../db/payments/DbTypesHelper.kt | 1 + .../db/payments/InboundLiquidityLeaseType.kt | 104 -------------- .../db/payments/InboundLiquidityQueries.kt | 26 +++- .../db/payments/IncomingReceivedWithType.kt | 74 +++++++--- .../payments/liquidityads/FundingFeeData.kt | 28 ++++ .../payments/liquidityads/LegacyLeaseData.kt | 62 +++++++++ .../liquidityads/PaymentDetailsData.kt | 67 +++++++++ .../db/payments/liquidityads/PurchaseData.kt | 109 +++++++++++++++ .../db/serializers/v1/TxIdSerializer.kt | 25 ++++ .../managers/NodeParamsManager.kt | 32 ++--- .../managers/NotificationsManager.kt | 17 ++- .../fr.acinq.phoenix/managers/PeerManager.kt | 10 -- .../fr.acinq.phoenix/utils/CsvWriter.kt | 7 +- .../utils/extensions/PaymentExtensions.kt | 17 +++ .../InboundLiquidityOutgoing.sq | 14 +- .../fr.acinq.phoenix.db/migrations/9.sqm | 8 ++ .../db/IncomingPaymentDbTypeVersionTest.kt | 8 +- .../phoenix/db/SqlitePaymentsDatabaseTest.kt | 19 ++- .../acinq/phoenix/db/cloud/CloudDataTest.kt | 11 +- .../fr/acinq/phoenix/utils/CsvWriterTests.kt | 12 +- .../fr/acinq/phoenix/utils/ParserTest.kt | 49 +++---- .../fr/acinq/phoenix/db/iosDbFactory.kt | 12 +- 31 files changed, 697 insertions(+), 393 deletions(-) delete mode 100644 phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/InboundLiquidityLeaseType.kt create mode 100644 phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/FundingFeeData.kt create mode 100644 phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/LegacyLeaseData.kt create mode 100644 phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/PaymentDetailsData.kt create mode 100644 phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/PurchaseData.kt create mode 100644 phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/serializers/v1/TxIdSerializer.kt create mode 100644 phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/migrations/9.sqm diff --git a/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/db/androidDbFactory.kt b/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/db/androidDbFactory.kt index 5f9f6d960..0167018d7 100644 --- a/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/db/androidDbFactory.kt +++ b/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/db/androidDbFactory.kt @@ -22,12 +22,19 @@ import fr.acinq.bitcoin.Chain import fr.acinq.phoenix.utils.PlatformContext actual fun createChannelsDbDriver(ctx: PlatformContext, chain: Chain, nodeIdHash: String): SqlDriver { - - return AndroidSqliteDriver(ChannelsDatabase.Schema, ctx.applicationContext, "channels-${chain.name.lowercase()}-$nodeIdHash.sqlite") + val chainName = when (chain) { + is Chain.Testnet3 -> "testnet" + else -> chain.name.lowercase() + } + return AndroidSqliteDriver(ChannelsDatabase.Schema, ctx.applicationContext, "channels-$chainName-$nodeIdHash.sqlite") } actual fun createPaymentsDbDriver(ctx: PlatformContext, chain: Chain, nodeIdHash: String): SqlDriver { - return AndroidSqliteDriver(PaymentsDatabase.Schema, ctx.applicationContext, "payments-${chain.name.lowercase()}-$nodeIdHash.sqlite") + val chainName = when (chain) { + is Chain.Testnet3 -> "testnet" + else -> chain.name.lowercase() + } + return AndroidSqliteDriver(PaymentsDatabase.Schema, ctx.applicationContext, "payments-$chainName-$nodeIdHash.sqlite") } actual fun createAppDbDriver(ctx: PlatformContext): SqlDriver { diff --git a/phoenix-shared/src/commonMain/appdb/fr.acinq.phoenix.db/Notifications.sq b/phoenix-shared/src/commonMain/appdb/fr.acinq.phoenix.db/Notifications.sq index b65844c00..525a5fb5e 100644 --- a/phoenix-shared/src/commonMain/appdb/fr.acinq.phoenix.db/Notifications.sq +++ b/phoenix-shared/src/commonMain/appdb/fr.acinq.phoenix.db/Notifications.sq @@ -1,5 +1,3 @@ -import fr.acinq.phoenix.db.notifications.NotificationTypeVersion; - -- This table stores notifications of all kinds. -- * id => UUID of a notification -- * type_version => string tracking the type/version of a notification @@ -8,7 +6,7 @@ import fr.acinq.phoenix.db.notifications.NotificationTypeVersion; -- * read_at => when the notification was read, in millis. Read notifications are typically not shown anymore. CREATE TABLE IF NOT EXISTS notifications ( id TEXT NOT NULL PRIMARY KEY, - type_version TEXT AS NotificationTypeVersion NOT NULL, + type_version TEXT NOT NULL, data_json BLOB NOT NULL, created_at INTEGER NOT NULL, read_at INTEGER DEFAULT NULL diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/Notification.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/Notification.kt index afb6f9265..bd675a454 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/Notification.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/Notification.kt @@ -63,7 +63,23 @@ sealed class Notification { override val source: LiquidityEvents.Source, ) : PaymentRejected() - data class ChannelsInitializing( + data class MissingOffChainAmountTooLow( + override val id: UUID, + override val createdAt: Long, + override val readAt: Long?, + override val amount: MilliSatoshi, + override val source: LiquidityEvents.Source, + ) : PaymentRejected() + + data class ChannelFundingInProgress( + override val id: UUID, + override val createdAt: Long, + override val readAt: Long?, + override val amount: MilliSatoshi, + override val source: LiquidityEvents.Source, + ) : PaymentRejected() + + data class GenericError( override val id: UUID, override val createdAt: Long, override val readAt: Long?, diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/SqliteAppDb.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/SqliteAppDb.kt index 89cecfc58..fc957fde1 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/SqliteAppDb.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/SqliteAppDb.kt @@ -26,9 +26,6 @@ class SqliteAppDb(private val driver: SqlDriver) { exchange_ratesAdapter = Exchange_rates.Adapter( typeAdapter = EnumColumnAdapter() ), - notificationsAdapter = Notifications.Adapter( - type_versionAdapter = EnumColumnAdapter() - ) ) private val priceQueries = database.exchangeRatesQueries diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/SqlitePaymentsDb.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/SqlitePaymentsDb.kt index dd327bda9..3a38b2be3 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/SqlitePaymentsDb.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/SqlitePaymentsDb.kt @@ -76,9 +76,6 @@ class SqlitePaymentsDb( channel_close_outgoing_paymentsAdapter = Channel_close_outgoing_payments.Adapter( closing_info_typeAdapter = EnumColumnAdapter() ), - inbound_liquidity_outgoing_paymentsAdapter = Inbound_liquidity_outgoing_payments.Adapter( - lease_typeAdapter = EnumColumnAdapter() - ) ) internal val inQueries = IncomingQueries(database) @@ -155,6 +152,12 @@ class SqlitePaymentsDb( } } + override suspend fun getInboundLiquidityPurchase(fundingTxId: TxId): InboundLiquidityOutgoingPayment? { + return withContext(Dispatchers.Default) { + inboundLiquidityQueries.getByTxId(fundingTxId) + } + } + override suspend fun completeOutgoingPaymentOffchain( id: UUID, finalFailure: FinalFailure, diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/cloud/payments/CloudData.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/cloud/payments/CloudData.kt index 66966d5cc..71678b417 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/cloud/payments/CloudData.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/cloud/payments/CloudData.kt @@ -1,6 +1,8 @@ package fr.acinq.phoenix.db.cloud import fr.acinq.lightning.db.* +import fr.acinq.phoenix.db.cloud.payments.InboundLiquidityLegacyWrapper +import fr.acinq.phoenix.db.cloud.payments.InboundLiquidityPaymentWrapper import kotlinx.serialization.* import kotlinx.serialization.cbor.ByteString import kotlinx.serialization.cbor.Cbor @@ -68,7 +70,9 @@ data class CloudData( @SerialName("sc") val spliceCpfp: SpliceCpfpPaymentWrapper? = null, @SerialName("il") - val inboundLiquidity: InboundLiquidityPaymentWrapper? = null, + val inboundLegacyLiquidity: InboundLiquidityLegacyWrapper? = null, + @SerialName("ip") + val inboundPurchaseLiquidity: InboundLiquidityPaymentWrapper? = null, @SerialName("v") val version: Int, @ByteString @@ -81,7 +85,8 @@ data class CloudData( spliceOutgoing = null, channelClose = null, spliceCpfp = null, - inboundLiquidity = null, + inboundLegacyLiquidity = null, + inboundPurchaseLiquidity = null, version = CloudDataVersion.V0.value, padding = ByteArray(size = 0) ) @@ -92,7 +97,8 @@ data class CloudData( spliceOutgoing = if (outgoing is SpliceOutgoingPayment) SpliceOutgoingPaymentWrapper(outgoing) else null, channelClose = if (outgoing is ChannelCloseOutgoingPayment) ChannelClosePaymentWrapper(outgoing) else null, spliceCpfp = if (outgoing is SpliceCpfpOutgoingPayment) SpliceCpfpPaymentWrapper(outgoing) else null, - inboundLiquidity = if (outgoing is InboundLiquidityOutgoingPayment) InboundLiquidityPaymentWrapper(outgoing) else null, + inboundLegacyLiquidity = null, + inboundPurchaseLiquidity = if (outgoing is InboundLiquidityOutgoingPayment) InboundLiquidityPaymentWrapper(outgoing) else null, version = CloudDataVersion.V0.value, padding = ByteArray(size = 0) ) @@ -112,7 +118,8 @@ data class CloudData( spliceOutgoing != null -> spliceOutgoing.unwrap() channelClose != null -> channelClose.unwrap() spliceCpfp != null -> spliceCpfp.unwrap() - inboundLiquidity != null -> inboundLiquidity.unwrap() + inboundLegacyLiquidity != null -> inboundLegacyLiquidity.unwrap() + inboundPurchaseLiquidity != null -> inboundPurchaseLiquidity.unwrap() else -> null } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/cloud/payments/InboundLiquidityPaymentWrapper.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/cloud/payments/InboundLiquidityPaymentWrapper.kt index 33c56ced1..5c5419c09 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/cloud/payments/InboundLiquidityPaymentWrapper.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/cloud/payments/InboundLiquidityPaymentWrapper.kt @@ -1,29 +1,30 @@ -package fr.acinq.phoenix.db.cloud +package fr.acinq.phoenix.db.cloud.payments import fr.acinq.bitcoin.TxId import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment import fr.acinq.lightning.utils.UUID -import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.utils.toByteVector32 -import fr.acinq.lightning.utils.toByteVector64 import fr.acinq.lightning.wire.LiquidityAds +import fr.acinq.phoenix.db.cloud.UUIDSerializer +import fr.acinq.phoenix.db.payments.liquidityads.PurchaseData +import fr.acinq.phoenix.db.payments.liquidityads.PurchaseData.Companion.encodeAsDb import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.cbor.ByteString -@Serializable + +/** New inbound liquidity wrapper that uses the [LiquidityAds.Purchase] object. */ +@Suppress("ArrayInDataClass") @OptIn(ExperimentalSerializationApi::class) +@Serializable data class InboundLiquidityPaymentWrapper( @Serializable(with = UUIDSerializer::class) val id: UUID, - @ByteString - val channelId: ByteArray, - @ByteString - val txId: ByteArray, + @ByteString val channelId: ByteArray, + @ByteString val txId: ByteArray, val miningFeesSat: Long, - val lease: LiquidityAdsLeaseWrapper, + val purchase: LiquidityAdsPurchaseWrapper, val createdAt: Long, val confirmedAt: Long?, val lockedAt: Long?, @@ -33,7 +34,7 @@ data class InboundLiquidityPaymentWrapper( channelId = src.channelId.toByteArray(), txId = src.txId.value.toByteArray(), miningFeesSat = src.miningFees.sat, - lease = LiquidityAdsLeaseWrapper(src.lease), + purchase = LiquidityAdsPurchaseWrapper(src.purchase), createdAt = src.createdAt, confirmedAt = src.confirmedAt, lockedAt = src.lockedAt @@ -45,80 +46,76 @@ data class InboundLiquidityPaymentWrapper( channelId = this.channelId.toByteVector32(), txId = TxId(this.txId), miningFees = this.miningFeesSat.sat, - lease = this.lease.unwrap(), + purchase = this.purchase.unwrap(), + createdAt = this.createdAt, + confirmedAt = this.confirmedAt, + lockedAt = this.lockedAt, + ) + + @Serializable + data class LiquidityAdsPurchaseWrapper(@ByteString val blob: ByteArray) { + companion object { + operator fun invoke(purchase: LiquidityAds.Purchase): LiquidityAdsPurchaseWrapper { + return LiquidityAdsPurchaseWrapper(purchase.encodeAsDb()) + } + } + fun unwrap(): LiquidityAds.Purchase { + return PurchaseData.decodeAsCanonical("", blob) + } + } +} + +/** This is the legacy wrapper for inbound liquidity, that used a Lease object to represent the liquidity purchase. Used only for deserialization now. */ +@Serializable +@Suppress("ArrayInDataClass") +@OptIn(ExperimentalSerializationApi::class) +data class InboundLiquidityLegacyWrapper( + @Serializable(with = UUIDSerializer::class) + val id: UUID, + @ByteString val channelId: ByteArray, + @ByteString val txId: ByteArray, + val miningFeesSat: Long, + val lease: LiquidityAdsLeaseWrapper, + val createdAt: Long, + val confirmedAt: Long?, + val lockedAt: Long?, +) { + @Throws(Exception::class) + fun unwrap() = InboundLiquidityOutgoingPayment( + id = this.id, + channelId = this.channelId.toByteVector32(), + txId = TxId(this.txId), + miningFees = this.miningFeesSat.sat, + purchase = this.lease.unwrap(), createdAt = this.createdAt, confirmedAt = this.confirmedAt, - lockedAt = this.lockedAt + lockedAt = this.lockedAt, ) @Serializable - @OptIn(ExperimentalSerializationApi::class) data class LiquidityAdsLeaseWrapper( val amountSat: Long, val fees: LiquidityAdsLeaseFeesWrapper, - @ByteString - val sellerSig: ByteArray, - val witness: LiquidityAdsLeaseWitnessWrapper ) { - constructor(src: LiquidityAds.Lease) : this( - amountSat = src.amount.sat, - fees = LiquidityAdsLeaseFeesWrapper(src.fees), - sellerSig = src.sellerSig.toByteArray(), - witness = LiquidityAdsLeaseWitnessWrapper(src.witness) - ) - @Throws(Exception::class) - fun unwrap() = LiquidityAds.Lease( - amount = this.amountSat.sat, - fees = this.fees.unwrap(), - sellerSig = this.sellerSig.toByteVector64(), - witness = this.witness.unwrap() - ) + fun unwrap(): LiquidityAds.Purchase{ + return LiquidityAds.Purchase.Standard( + amount = this.amountSat.sat, + fees = this.fees.unwrap().let { LiquidityAds.Fees(miningFee = it.miningFee, serviceFee = it.serviceFee) }, + paymentDetails = LiquidityAds.PaymentDetails.FromChannelBalance + ) + } } @Serializable - @OptIn(ExperimentalSerializationApi::class) data class LiquidityAdsLeaseFeesWrapper( val miningFeeSat: Long, val serviceFeeSat: Long ) { - constructor(src: LiquidityAds.LeaseFees) : this( - miningFeeSat = src.miningFee.sat, - serviceFeeSat = src.serviceFee.sat - ) - @Throws(Exception::class) - fun unwrap() = LiquidityAds.LeaseFees( + fun unwrap() = LiquidityAds.Fees( miningFee = this.miningFeeSat.sat, serviceFee = this.serviceFeeSat.sat ) } - - @Serializable - @OptIn(ExperimentalSerializationApi::class) - data class LiquidityAdsLeaseWitnessWrapper( - @ByteString - val fundingScript: ByteArray, - val leaseDuration: Int, - val leaseEnd: Int, - val maxRelayFeeProportional: Int, - val maxRelayFeeBaseMsat: Long - ) { - constructor(src: LiquidityAds.LeaseWitness) : this( - fundingScript = src.fundingScript.toByteArray(), - leaseDuration = src.leaseDuration, - leaseEnd = src.leaseEnd, - maxRelayFeeProportional = src.maxRelayFeeProportional, - maxRelayFeeBaseMsat = src.maxRelayFeeBase.msat - ) - - @Throws(Exception::class) - fun unwrap() = LiquidityAds.LeaseWitness( - fundingScript = this.fundingScript.toByteVector(), - leaseDuration = this.leaseDuration, - leaseEnd = this.leaseEnd, - maxRelayFeeProportional = this.maxRelayFeeProportional, - maxRelayFeeBase = this.maxRelayFeeBaseMsat.msat - ) - } } \ No newline at end of file diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationDataType.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationDataType.kt index 4aa1d1bde..b91cff6f5 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationDataType.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationDataType.kt @@ -32,26 +32,14 @@ import fr.acinq.phoenix.db.payments.DbTypesHelper import fr.acinq.phoenix.db.serializers.v1.ByteVector32Serializer import fr.acinq.phoenix.db.serializers.v1.MilliSatoshiSerializer import fr.acinq.phoenix.db.serializers.v1.SatoshiSerializer -import io.ktor.utils.io.charsets.* -import io.ktor.utils.io.core.* +import io.ktor.utils.io.charsets.Charsets +import io.ktor.utils.io.core.toByteArray import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -enum class NotificationTypeVersion { - PAYMENT_REJECTED_BY_USER_V0, - PAYMENT_REJECTED_TOO_EXPENSIVE_V0, - PAYMENT_REJECTED_OVER_ABSOLUTE_V0, - PAYMENT_REJECTED_OVER_RELATIVE_V0, - PAYMENT_REJECTED_DISABLED_V0, - PAYMENT_REJECTED_CHANNELS_INIT_V0, - - WATCH_TOWER_NOMINAL_V0, - WATCH_TOWER_UNKNOWN_V0, - WATCH_TOWER_REVOKED_FOUND_V0, -} - +@Serializable internal sealed class NotificationData { sealed class PaymentRejected : NotificationData() { sealed class OverAbsoluteFee : PaymentRejected() { @@ -79,16 +67,26 @@ internal sealed class NotificationData { data class V0(@Serializable val amount: MilliSatoshi, val source: LiquidityEvents.Source) : Disabled() } - sealed class ChannelsInitializing : PaymentRejected() { + sealed class ChannelFundingInProgress : PaymentRejected() { + @Serializable + data class V0(@Serializable val amount: MilliSatoshi, val source: LiquidityEvents.Source) : ChannelFundingInProgress() + } + + sealed class MissingOffchainAmountTooLow : PaymentRejected() { + @Serializable + data class V0(@Serializable val amount: MilliSatoshi, val source: LiquidityEvents.Source) : MissingOffchainAmountTooLow() + } + + sealed class GenericError : PaymentRejected() { @Serializable - data class V0(@Serializable val amount: MilliSatoshi, val source: LiquidityEvents.Source) : ChannelsInitializing() + data class V0(@Serializable val amount: MilliSatoshi, val source: LiquidityEvents.Source) : GenericError() } } sealed class WatchTowerOutcome: NotificationData() { sealed class Unknown : WatchTowerOutcome() { @Serializable - object V0: Unknown() + data object V0: Unknown() } sealed class Nominal : WatchTowerOutcome() { @Serializable @@ -101,37 +99,26 @@ internal sealed class NotificationData { } companion object { - fun deserialize(typeVersion: NotificationTypeVersion, blob: ByteArray): NotificationData? = try { - DbTypesHelper.decodeBlob(blob) { json, format -> - when (typeVersion) { - NotificationTypeVersion.PAYMENT_REJECTED_OVER_ABSOLUTE_V0 -> format.decodeFromString(json) - NotificationTypeVersion.PAYMENT_REJECTED_OVER_RELATIVE_V0 -> format.decodeFromString(json) - NotificationTypeVersion.PAYMENT_REJECTED_DISABLED_V0 -> format.decodeFromString(json) - NotificationTypeVersion.PAYMENT_REJECTED_CHANNELS_INIT_V0 -> format.decodeFromString(json) - NotificationTypeVersion.WATCH_TOWER_NOMINAL_V0 -> format.decodeFromString(json) - NotificationTypeVersion.WATCH_TOWER_UNKNOWN_V0 -> format.decodeFromString(json) - NotificationTypeVersion.WATCH_TOWER_REVOKED_FOUND_V0 -> format.decodeFromString(json) - - // obsolete types - NotificationTypeVersion.PAYMENT_REJECTED_BY_USER_V0, NotificationTypeVersion.PAYMENT_REJECTED_TOO_EXPENSIVE_V0 -> { - throw UnsupportedOperationException() - } - } - } + fun decode(blob: ByteArray): NotificationData? = try { + DbTypesHelper.decodeBlob(blob) { json, format -> format.decodeFromString(json) } } catch (e: Exception) { // notifications are not critical data, can be ignored if malformed null } + + fun Notification.encodeAsDb(): ByteArray = Json.encodeToString(this.asDb()).toByteArray(Charsets.UTF_8) + + private fun Notification.asDb(): NotificationData = when (this) { + is Notification.OverAbsoluteFee -> PaymentRejected.OverAbsoluteFee.V0(amount, source, fee, maxAbsoluteFee) + is Notification.OverRelativeFee -> PaymentRejected.OverRelativeFee.V0(amount, source, fee, maxRelativeFeeBasisPoints) + is Notification.FeePolicyDisabled -> PaymentRejected.Disabled.V0(amount, source) + is Notification.ChannelFundingInProgress -> PaymentRejected.ChannelFundingInProgress.V0(amount, source) + is Notification.MissingOffChainAmountTooLow -> PaymentRejected.MissingOffchainAmountTooLow.V0(amount, source) + is Notification.GenericError -> PaymentRejected.GenericError.V0(amount, source) + is fr.acinq.phoenix.data.WatchTowerOutcome.Nominal -> WatchTowerOutcome.Nominal.V0(channelsWatchedCount) + is fr.acinq.phoenix.data.WatchTowerOutcome.RevokedFound -> WatchTowerOutcome.RevokedFound.V0(channels) + is fr.acinq.phoenix.data.WatchTowerOutcome.Unknown -> WatchTowerOutcome.Unknown.V0 + } } } - -internal fun Notification.mapToDb(): Pair = when (this) { - is Notification.OverAbsoluteFee -> NotificationTypeVersion.PAYMENT_REJECTED_OVER_ABSOLUTE_V0 to Json.encodeToString(NotificationData.PaymentRejected.OverAbsoluteFee.V0(amount, source, fee, maxAbsoluteFee)).toByteArray(Charsets.UTF_8) - is Notification.OverRelativeFee -> NotificationTypeVersion.PAYMENT_REJECTED_OVER_RELATIVE_V0 to Json.encodeToString(NotificationData.PaymentRejected.OverRelativeFee.V0(amount, source, fee, maxRelativeFeeBasisPoints)).toByteArray(Charsets.UTF_8) - is Notification.FeePolicyDisabled -> NotificationTypeVersion.PAYMENT_REJECTED_DISABLED_V0 to Json.encodeToString(NotificationData.PaymentRejected.Disabled.V0(amount, source)).toByteArray(Charsets.UTF_8) - is Notification.ChannelsInitializing -> NotificationTypeVersion.PAYMENT_REJECTED_CHANNELS_INIT_V0 to Json.encodeToString(NotificationData.PaymentRejected.ChannelsInitializing.V0(amount, source)).toByteArray(Charsets.UTF_8) - is WatchTowerOutcome.Nominal -> NotificationTypeVersion.WATCH_TOWER_NOMINAL_V0 to Json.encodeToString(NotificationData.WatchTowerOutcome.Nominal.V0(channelsWatchedCount)).toByteArray(Charsets.UTF_8) - is WatchTowerOutcome.RevokedFound -> NotificationTypeVersion.WATCH_TOWER_REVOKED_FOUND_V0 to Json.encodeToString(NotificationData.WatchTowerOutcome.RevokedFound.V0(channels)).toByteArray(Charsets.UTF_8) - is WatchTowerOutcome.Unknown -> NotificationTypeVersion.WATCH_TOWER_UNKNOWN_V0 to Json.encodeToString(NotificationData.WatchTowerOutcome.Unknown.V0).toByteArray(Charsets.UTF_8) -} \ No newline at end of file diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationsQueries.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationsQueries.kt index c82fab40c..260c55e9b 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationsQueries.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationsQueries.kt @@ -22,7 +22,9 @@ import fr.acinq.lightning.LiquidityEvents import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.utils.currentTimestampMillis import fr.acinq.phoenix.data.Notification +import fr.acinq.phoenix.data.WatchTowerOutcome import fr.acinq.phoenix.db.AppDatabase +import fr.acinq.phoenix.db.notifications.NotificationData.Companion.encodeAsDb import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.Flow @@ -33,16 +35,25 @@ internal class NotificationsQueries(val database: AppDatabase) { fun get(id: UUID): Notification? { return queries.get(id.toString()).executeAsOneOrNull()?.let { row -> - mapToNotification(row.id, row.type_version, row.data_json, row.created_at, row.read_at) + mapToNotification(row.id, row.data_json, row.created_at, row.read_at) } } fun save(notification: Notification) { - val (typeVersion, blob) = notification.mapToDb() queries.insert( id = notification.id.toString(), - type_version = typeVersion, - data_json = blob, + type_version = when (notification) { + is Notification.OverAbsoluteFee -> "PAYMENT_REJECTED_OVER_ABSOLUTE_FEE" + is Notification.OverRelativeFee -> "PAYMENT_REJECTED_OVER_RELATIVE_FEE" + is Notification.FeePolicyDisabled -> "PAYMENT_REJECTED_POLICY_DISABLED" + is Notification.ChannelFundingInProgress -> "PAYMENT_REJECTED_CHANNEL_FUNDING_IN_PROGRESS" + is Notification.MissingOffChainAmountTooLow -> "PAYMENT_REJECTED_OFFCHAIN_AMOUNT_TOO_LOW" + is Notification.GenericError -> "PAYMENT_REJECTED_GENERIC_ERROR" + is WatchTowerOutcome.Nominal -> "WATCH_TOWER_NOMINAL" + is WatchTowerOutcome.RevokedFound -> "WATCH_TOWER_REVOKED" + is WatchTowerOutcome.Unknown -> "WATCH_TOWER_UNKNOWN" + }, + data_json = notification.encodeAsDb(), created_at = currentTimestampMillis() ) } @@ -68,7 +79,7 @@ internal class NotificationsQueries(val database: AppDatabase) { return queries.listUnread().asFlow().mapToList(Dispatchers.IO).map { val notifs = it.mapNotNull { row -> val ids = row.grouped_ids.split(";").map { UUID.fromString(it) }.toSet() - val notif = mapToNotification(row.id, row.type_version, row.data_json, row.max ?: 0, null) + val notif = mapToNotification(row.id, row.data_json, row.max ?: 0, null) if (notif != null) { ids to notif } else { @@ -101,53 +112,58 @@ internal class NotificationsQueries(val database: AppDatabase) { /** Map columns to a [Notification] object. If the [data_json] column is unreadable, return null. */ internal fun mapToNotification( id: String, - type_version: NotificationTypeVersion, data_json: ByteArray, created_at: Long, read_at: Long?, ): Notification? { - return when (val data = NotificationData.deserialize(type_version, data_json)) { - is NotificationData.PaymentRejected.OverAbsoluteFee.V0 -> { - Notification.OverAbsoluteFee( - id = UUID.fromString(id), - createdAt = created_at, - readAt = read_at, - amount = data.amount, - source = data.source, - fee = data.fee, - maxAbsoluteFee = data.maxAbsoluteFee - ) - } - is NotificationData.PaymentRejected.OverRelativeFee.V0 -> { - Notification.OverRelativeFee( - id = UUID.fromString(id), - createdAt = created_at, - readAt = read_at, - amount = data.amount, - source = data.source, - fee = data.fee, - maxRelativeFeeBasisPoints = data.maxRelativeFeeBasisPoints - ) - } - is NotificationData.PaymentRejected.Disabled.V0 -> { - Notification.FeePolicyDisabled( - id = UUID.fromString(id), - createdAt = created_at, - readAt = read_at, - amount = data.amount, - source = data.source, - ) - } - is NotificationData.PaymentRejected.ChannelsInitializing.V0 -> { - Notification.ChannelsInitializing( - id = UUID.fromString(id), - createdAt = created_at, - readAt = read_at, - amount = data.amount, - source = data.source, - ) - } - is NotificationData.WatchTowerOutcome -> null + return when (val data = NotificationData.decode(data_json)) { + is NotificationData.PaymentRejected.OverAbsoluteFee.V0 -> Notification.OverAbsoluteFee( + id = UUID.fromString(id), + createdAt = created_at, + readAt = read_at, + amount = data.amount, + source = data.source, + fee = data.fee, + maxAbsoluteFee = data.maxAbsoluteFee + ) + is NotificationData.PaymentRejected.OverRelativeFee.V0 -> Notification.OverRelativeFee( + id = UUID.fromString(id), + createdAt = created_at, + readAt = read_at, + amount = data.amount, + source = data.source, + fee = data.fee, + maxRelativeFeeBasisPoints = data.maxRelativeFeeBasisPoints + ) + is NotificationData.PaymentRejected.Disabled.V0 -> Notification.FeePolicyDisabled( + id = UUID.fromString(id), + createdAt = created_at, + readAt = read_at, + amount = data.amount, + source = data.source, + ) + is NotificationData.PaymentRejected.ChannelFundingInProgress.V0 -> Notification.ChannelFundingInProgress( + id = UUID.fromString(id), + createdAt = created_at, + readAt = read_at, + amount = data.amount, + source = data.source, + ) + is NotificationData.PaymentRejected.MissingOffchainAmountTooLow.V0 -> Notification.MissingOffChainAmountTooLow( + id = UUID.fromString(id), + createdAt = created_at, + readAt = read_at, + amount = data.amount, + source = data.source, + ) + is NotificationData.PaymentRejected.GenericError.V0 -> Notification.GenericError( + id = UUID.fromString(id), + createdAt = created_at, + readAt = read_at, + amount = data.amount, + source = data.source, + ) + is NotificationData.WatchTowerOutcome -> null // ignored null -> null } } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/DbTypesHelper.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/DbTypesHelper.kt index 98730f1bf..25f5c4a61 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/DbTypesHelper.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/DbTypesHelper.kt @@ -30,6 +30,7 @@ object DbTypesHelper { val module = SerializersModule { polymorphic(IncomingReceivedWithData.Part::class) { subclass(IncomingReceivedWithData.Part.Htlc.V0::class) + subclass(IncomingReceivedWithData.Part.Htlc.V1::class) @Suppress("DEPRECATION") subclass(IncomingReceivedWithData.Part.NewChannel.V0::class) subclass(IncomingReceivedWithData.Part.NewChannel.V1::class) diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/InboundLiquidityLeaseType.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/InboundLiquidityLeaseType.kt deleted file mode 100644 index c45249b8b..000000000 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/InboundLiquidityLeaseType.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2023 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:UseSerializers( - ByteVectorSerializer::class, - ByteVector32Serializer::class, - ByteVector64Serializer::class, - SatoshiSerializer::class, - MilliSatoshiSerializer::class -) - -package fr.acinq.phoenix.db.payments - -import fr.acinq.bitcoin.ByteVector -import fr.acinq.bitcoin.ByteVector64 -import fr.acinq.bitcoin.Satoshi -import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment -import fr.acinq.lightning.wire.LiquidityAds -import fr.acinq.phoenix.db.serializers.v1.ByteVector32Serializer -import fr.acinq.phoenix.db.serializers.v1.ByteVector64Serializer -import fr.acinq.phoenix.db.serializers.v1.ByteVectorSerializer -import fr.acinq.phoenix.db.serializers.v1.MilliSatoshiSerializer -import fr.acinq.phoenix.db.serializers.v1.SatoshiSerializer -import io.ktor.utils.io.charsets.Charsets -import io.ktor.utils.io.core.toByteArray -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -enum class InboundLiquidityLeaseTypeVersion { - LEASE_V0, -} - -sealed class InboundLiquidityLeaseData { - - @Serializable - data class V0( - val amount: Satoshi, - val miningFees: Satoshi, - val serviceFee: Satoshi, - val sellerSig: ByteVector64, - val witnessFundingScript: ByteVector, - val witnessLeaseDuration: Int, - val witnessLeaseEnd: Int, - val witnessMaxRelayFeeProportional: Int, - val witnessMaxRelayFeeBase: MilliSatoshi - ) : InboundLiquidityLeaseData() - - companion object { - /** Deserializes a json-encoded blob containing data for an [LiquidityAds.Lease] object. */ - fun deserialize( - typeVersion: InboundLiquidityLeaseTypeVersion, - blob: ByteArray, - ): LiquidityAds.Lease = DbTypesHelper.decodeBlob(blob) { json, format -> - when (typeVersion) { - InboundLiquidityLeaseTypeVersion.LEASE_V0 -> format.decodeFromString(json).let { - LiquidityAds.Lease( - amount = it.amount, - fees = LiquidityAds.LeaseFees(miningFee = it.miningFees, serviceFee = it.serviceFee), - sellerSig = it.sellerSig, - witness = LiquidityAds.LeaseWitness( - fundingScript = it.witnessFundingScript, - leaseDuration = it.witnessLeaseDuration, - leaseEnd = it.witnessLeaseEnd, - maxRelayFeeProportional = it.witnessMaxRelayFeeProportional, - maxRelayFeeBase = it.witnessMaxRelayFeeBase, - ) - ) - } - } - } - } -} - -fun InboundLiquidityOutgoingPayment.mapLeaseToDb() = InboundLiquidityLeaseTypeVersion.LEASE_V0 to - InboundLiquidityLeaseData.V0( - amount = lease.amount, - miningFees = lease.fees.miningFee, - serviceFee = lease.fees.serviceFee, - sellerSig = lease.sellerSig, - witnessFundingScript = lease.witness.fundingScript, - witnessLeaseDuration = lease.witness.leaseDuration, - witnessLeaseEnd = lease.witness.leaseEnd, - witnessMaxRelayFeeProportional = lease.witness.maxRelayFeeProportional, - witnessMaxRelayFeeBase = lease.witness.maxRelayFeeBase, - ).let { - Json.encodeToString(it).toByteArray(Charsets.UTF_8) - } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/InboundLiquidityQueries.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/InboundLiquidityQueries.kt index 869e8fe09..618376b5f 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/InboundLiquidityQueries.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/InboundLiquidityQueries.kt @@ -21,23 +21,34 @@ import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector32 +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.data.WalletPaymentId import fr.acinq.phoenix.db.PaymentsDatabase import fr.acinq.phoenix.db.didSaveWalletPayment +import fr.acinq.phoenix.db.payments.liquidityads.PurchaseData +import fr.acinq.phoenix.db.payments.liquidityads.PurchaseData.Companion.encodeAsDb class InboundLiquidityQueries(val database: PaymentsDatabase) { private val queries = database.inboundLiquidityOutgoingQueries fun add(payment: InboundLiquidityOutgoingPayment) { database.transaction { - val (leaseType, leaseData) = payment.mapLeaseToDb() queries.insert( id = payment.id.toString(), mining_fees_sat = payment.miningFees.sat, channel_id = payment.channelId.toByteArray(), tx_id = payment.txId.value.toByteArray(), - lease_type = leaseType, - lease_blob = leaseData, + lease_type = when (payment.purchase) { + is LiquidityAds.Purchase.Standard -> "STANDARD" + is LiquidityAds.Purchase.WithFeeCredit -> "WITH_FEE_CREDIT" + }, + lease_blob = payment.purchase.encodeAsDb(), + payment_details_type = when (payment.purchase.paymentDetails) { + is LiquidityAds.PaymentDetails.FromChannelBalance -> "FROM_CHANNEL_BALANCE" + is LiquidityAds.PaymentDetails.FromFutureHtlc -> "FROM_FUTURE_HTLC" + is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> "FROM_FUTURE_HTLC_WITH_PREIMAGE" + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> "FROM_CHANNEL_BALANCE_FOR_FUTURE_HTLC" + }, created_at = payment.createdAt, confirmed_at = payment.confirmedAt, locked_at = payment.lockedAt, @@ -50,6 +61,11 @@ class InboundLiquidityQueries(val database: PaymentsDatabase) { .executeAsOneOrNull() } + fun getByTxId(txId: TxId): InboundLiquidityOutgoingPayment? { + return queries.getByTxId(tx_id = txId.value.toByteArray(), mapper = Companion::mapPayment) + .executeAsOneOrNull() + } + fun setConfirmed(id: UUID, confirmedAt: Long) { database.transaction { queries.setConfirmed(confirmed_at = confirmedAt, id = id.toString()) @@ -70,7 +86,7 @@ class InboundLiquidityQueries(val database: PaymentsDatabase) { mining_fees_sat: Long, channel_id: ByteArray, tx_id: ByteArray, - lease_type: InboundLiquidityLeaseTypeVersion, + lease_type: String, lease_blob: ByteArray, created_at: Long, confirmed_at: Long?, @@ -81,7 +97,7 @@ class InboundLiquidityQueries(val database: PaymentsDatabase) { miningFees = mining_fees_sat.sat, channelId = channel_id.toByteVector32(), txId = TxId(tx_id), - lease = InboundLiquidityLeaseData.deserialize(lease_type, lease_blob), + purchase = PurchaseData.decodeAsCanonical(lease_type, lease_blob), createdAt = created_at, confirmedAt = confirmed_at, lockedAt = locked_at diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/IncomingReceivedWithType.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/IncomingReceivedWithType.kt index 6623e3df7..2ad4c3e14 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/IncomingReceivedWithType.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/IncomingReceivedWithType.kt @@ -34,6 +34,9 @@ import fr.acinq.phoenix.db.serializers.v1.ByteVector32Serializer import fr.acinq.phoenix.db.serializers.v1.MilliSatoshiSerializer import fr.acinq.phoenix.db.serializers.v1.UUIDSerializer import fr.acinq.lightning.utils.sat +import fr.acinq.phoenix.db.payments.liquidityads.FundingFeeData +import fr.acinq.phoenix.db.payments.liquidityads.FundingFeeData.Companion.asCanonical +import fr.acinq.phoenix.db.payments.liquidityads.FundingFeeData.Companion.asDb import fr.acinq.phoenix.db.serializers.v1.SatoshiSerializer import io.ktor.utils.io.charsets.* import io.ktor.utils.io.core.* @@ -80,12 +83,21 @@ sealed class IncomingReceivedWithData { @Serializable sealed class Part : IncomingReceivedWithData() { sealed class Htlc : Part() { + @Deprecated("Replaced by [Htlc.V1], which supports the liquidity ads funding fee") @Serializable data class V0( @Serializable val amount: MilliSatoshi, @Serializable val channelId: ByteVector32, val htlcId: Long ) : Htlc() + + @Serializable + data class V1( + val amountReceived: MilliSatoshi, + val channelId: ByteVector32, + val htlcId: Long, + val fundingFee: FundingFeeData?, + ) : Htlc() } sealed class NewChannel : Part() { @@ -131,6 +143,13 @@ sealed class IncomingReceivedWithData { @Serializable val lockedAt: Long?, ) : SpliceIn() } + + sealed class FeeCredit : Part() { + @Serializable + data class V0( + val amount: MilliSatoshi + ) : FeeCredit() + } } companion object { @@ -152,11 +171,11 @@ sealed class IncomingReceivedWithData { @Suppress("DEPRECATION") when (typeVersion) { IncomingReceivedWithTypeVersion.LIGHTNING_PAYMENT_V0 -> listOf( - IncomingPayment.ReceivedWith.LightningPayment(amount ?: 0.msat, ByteVector32.Zeroes, 0L) + IncomingPayment.ReceivedWith.LightningPayment(amount ?: 0.msat, ByteVector32.Zeroes, 0L, null) ) IncomingReceivedWithTypeVersion.NEW_CHANNEL_V0 -> listOf(format.decodeFromString(json).let { IncomingPayment.ReceivedWith.NewChannel( - amount = amount ?: 0.msat, + amountReceived = amount ?: 0.msat, serviceFee = it.fees, miningFee = 0.sat, channelId = it.channelId ?: ByteVector32.Zeroes, @@ -165,12 +184,13 @@ sealed class IncomingReceivedWithData { lockedAt = 0, ) }) - IncomingReceivedWithTypeVersion.MULTIPARTS_V0 -> DbTypesHelper.polymorphicFormat.decodeFromString(SetSerializer(PolymorphicSerializer(Part::class)), json).map { + IncomingReceivedWithTypeVersion.MULTIPARTS_V0 -> DbTypesHelper.polymorphicFormat.decodeFromString(SetSerializer(PolymorphicSerializer(Part::class)), json).mapNotNull { when (it) { - is Part.Htlc.V0 -> IncomingPayment.ReceivedWith.LightningPayment(it.amount, it.channelId, it.htlcId) + is Part.Htlc.V0 -> IncomingPayment.ReceivedWith.LightningPayment(it.amount, it.channelId, it.htlcId, null) + is Part.Htlc.V1 -> IncomingPayment.ReceivedWith.LightningPayment(it.amountReceived, it.channelId, it.htlcId, it.fundingFee?.asCanonical()) is Part.NewChannel.V0 -> if (originTypeVersion == IncomingOriginTypeVersion.SWAPIN_V0) { IncomingPayment.ReceivedWith.NewChannel( - amount = it.amount, + amountReceived = it.amount, serviceFee = it.fees, miningFee = 0.sat, channelId = it.channelId ?: ByteVector32.Zeroes, @@ -180,7 +200,7 @@ sealed class IncomingReceivedWithData { ) } else { IncomingPayment.ReceivedWith.NewChannel( - amount = it.amount - it.fees, + amountReceived = it.amount - it.fees, serviceFee = it.fees, miningFee = 0.sat, channelId = it.channelId ?: ByteVector32.Zeroes, @@ -191,12 +211,23 @@ sealed class IncomingReceivedWithData { } else -> null // does not apply, MULTIPARTS_V0 only uses V0 parts } - }.filterNotNull() // null elements are discarded! + } IncomingReceivedWithTypeVersion.MULTIPARTS_V1 -> DbTypesHelper.polymorphicFormat.decodeFromString(SetSerializer(PolymorphicSerializer(Part::class)), json).map { when (it) { - is Part.Htlc.V0 -> IncomingPayment.ReceivedWith.LightningPayment(it.amount, it.channelId, it.htlcId) + is Part.Htlc.V0 -> IncomingPayment.ReceivedWith.LightningPayment( + amountReceived = it.amount, + channelId = it.channelId, + htlcId = it.htlcId, + fundingFee = null + ) + is Part.Htlc.V1 -> IncomingPayment.ReceivedWith.LightningPayment( + amountReceived = it.amountReceived, + channelId = it.channelId, + htlcId = it.htlcId, + fundingFee = it.fundingFee?.asCanonical() + ) is Part.NewChannel.V0 -> IncomingPayment.ReceivedWith.NewChannel( - amount = it.amount, + amountReceived = it.amount, serviceFee = it.fees, miningFee = 0.sat, channelId = it.channelId ?: ByteVector32.Zeroes, @@ -205,7 +236,7 @@ sealed class IncomingReceivedWithData { lockedAt = 0, ) is Part.NewChannel.V1 -> IncomingPayment.ReceivedWith.NewChannel( - amount = it.amount, + amountReceived = it.amount, serviceFee = it.fees, miningFee = 0.sat, channelId = it.channelId ?: ByteVector32.Zeroes, @@ -214,7 +245,7 @@ sealed class IncomingReceivedWithData { lockedAt = 0, ) is Part.NewChannel.V2 -> IncomingPayment.ReceivedWith.NewChannel( - amount = it.amount, + amountReceived = it.amount, serviceFee = it.serviceFee, miningFee = it.miningFee, channelId = it.channelId, @@ -223,7 +254,7 @@ sealed class IncomingReceivedWithData { lockedAt = it.lockedAt, ) is Part.SpliceIn.V0 -> IncomingPayment.ReceivedWith.SpliceIn( - amount = it.amount, + amountReceived = it.amount, serviceFee = it.serviceFee, miningFee = it.miningFee, channelId = it.channelId, @@ -231,8 +262,11 @@ sealed class IncomingReceivedWithData { confirmedAt = it.confirmedAt, lockedAt = it.lockedAt, ) + is Part.FeeCredit.V0 -> IncomingPayment.ReceivedWith.AddedToFeeCredit( + amountReceived = it.amount + ) } - }.filterNotNull() // null elements are discarded! + } } } } @@ -241,9 +275,14 @@ sealed class IncomingReceivedWithData { /** Only serialize received_with into the [IncomingReceivedWithTypeVersion.MULTIPARTS_V1] type. */ fun List.mapToDb(): Pair? = map { when (it) { - is IncomingPayment.ReceivedWith.LightningPayment -> IncomingReceivedWithData.Part.Htlc.V0(it.amount, it.channelId, it.htlcId) + is IncomingPayment.ReceivedWith.LightningPayment -> IncomingReceivedWithData.Part.Htlc.V1( + amountReceived = it.amountReceived, + channelId = it.channelId, + htlcId = it.htlcId, + fundingFee = it.fundingFee?.asDb() + ) is IncomingPayment.ReceivedWith.NewChannel -> IncomingReceivedWithData.Part.NewChannel.V2( - amount = it.amount, + amount = it.amountReceived, serviceFee = it.serviceFee, miningFee = it.miningFee, channelId = it.channelId, @@ -252,7 +291,7 @@ fun List.mapToDb(): Pair IncomingReceivedWithData.Part.SpliceIn.V0( - amount = it.amount, + amount = it.amountReceived, serviceFee = it.serviceFee, miningFee = it.miningFee, channelId = it.channelId, @@ -260,6 +299,9 @@ fun List.mapToDb(): Pair IncomingReceivedWithData.Part.FeeCredit.V0( + amount = it.amountReceived + ) } }.takeIf { it.isNotEmpty() }?.toSet()?.let { IncomingReceivedWithTypeVersion.MULTIPARTS_V1 to DbTypesHelper.polymorphicFormat.encodeToString( diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/FundingFeeData.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/FundingFeeData.kt new file mode 100644 index 000000000..626853ed6 --- /dev/null +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/FundingFeeData.kt @@ -0,0 +1,28 @@ +@file:UseSerializers( + MilliSatoshiSerializer::class, + TxIdSerializer::class, +) + +package fr.acinq.phoenix.db.payments.liquidityads + +import fr.acinq.bitcoin.TxId +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.phoenix.db.serializers.v1.MilliSatoshiSerializer +import fr.acinq.phoenix.db.serializers.v1.TxIdSerializer +import fr.acinq.lightning.wire.LiquidityAds +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers + +@Serializable +sealed class FundingFeeData { + + @Serializable + data class V0(val amount: MilliSatoshi, val fundingTxId: TxId) : FundingFeeData() + + companion object { + fun FundingFeeData.asCanonical(): LiquidityAds.FundingFee = when (this) { + is V0 -> LiquidityAds.FundingFee(amount = amount, fundingTxId = fundingTxId) + } + fun LiquidityAds.FundingFee.asDb(): FundingFeeData = V0(amount = amount, fundingTxId = fundingTxId) + } +} \ No newline at end of file diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/LegacyLeaseData.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/LegacyLeaseData.kt new file mode 100644 index 000000000..f8602d565 --- /dev/null +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/LegacyLeaseData.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2023 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:UseSerializers( + ByteVectorSerializer::class, + ByteVector32Serializer::class, + ByteVector64Serializer::class, + SatoshiSerializer::class, + MilliSatoshiSerializer::class +) + +package fr.acinq.phoenix.db.payments.liquidityads + +import fr.acinq.bitcoin.ByteVector +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.ByteVector64 +import fr.acinq.bitcoin.Satoshi +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.lightning.wire.LiquidityAds +import fr.acinq.phoenix.db.serializers.v1.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers + +enum class InboundLiquidityLeaseType { + @Deprecated("obsolete with the new on-the-fly channel funding that replaces lease -> purchase") + LEASE_V0 +} + +@Suppress("DEPRECATION_WARNING") +@Deprecated("obsolete with the new on-the-fly channel funding that replaces lease with purchase") +@Serializable +data class LeaseV0( + val amount: Satoshi, + val miningFees: Satoshi, + val serviceFee: Satoshi, + val sellerSig: ByteVector64, + val witnessFundingScript: ByteVector, + val witnessLeaseDuration: Int, + val witnessLeaseEnd: Int, + val witnessMaxRelayFeeProportional: Int, + val witnessMaxRelayFeeBase: MilliSatoshi +) { + /** Maps a legacy lease data into the modern [LiquidityAds.Purchase] object using fake payment details data. */ + fun toLiquidityAdsPurchase(): LiquidityAds.Purchase = LiquidityAds.Purchase.Standard( + amount = amount, + fees = LiquidityAds.Fees(miningFee = miningFees, serviceFee = serviceFee), + paymentDetails = LiquidityAds.PaymentDetails.FromChannelBalance + ) +} \ No newline at end of file diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/PaymentDetailsData.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/PaymentDetailsData.kt new file mode 100644 index 000000000..7431c9c9b --- /dev/null +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/PaymentDetailsData.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:UseSerializers( + ByteVector32Serializer::class, +) + +package fr.acinq.phoenix.db.payments.liquidityads + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.phoenix.db.serializers.v1.ByteVector32Serializer +import fr.acinq.lightning.wire.LiquidityAds +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers + + +@Serializable +sealed class PaymentDetailsData { + sealed class ChannelBalance : PaymentDetailsData() { + @Serializable + data object V0 : ChannelBalance() + } + + sealed class FutureHtlc : PaymentDetailsData() { + @Serializable + data class V0(val paymentHashes: List) : FutureHtlc() + } + + sealed class FutureHtlcWithPreimage : PaymentDetailsData() { + @Serializable + data class V0(val preimages: List) : FutureHtlcWithPreimage() + } + + sealed class ChannelBalanceForFutureHtlc : PaymentDetailsData() { + @Serializable + data class V0(val paymentHashes: List) : ChannelBalanceForFutureHtlc() + } + + companion object { + fun PaymentDetailsData.asCanonical(): LiquidityAds.PaymentDetails = when (this) { + is ChannelBalance.V0 -> LiquidityAds.PaymentDetails.FromChannelBalance + is FutureHtlc.V0 -> LiquidityAds.PaymentDetails.FromFutureHtlc(this.paymentHashes) + is FutureHtlcWithPreimage.V0 -> LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage(this.preimages) + is ChannelBalanceForFutureHtlc.V0 -> LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(this.paymentHashes) + } + + fun LiquidityAds.PaymentDetails.asDb(): PaymentDetailsData = when (this) { + is LiquidityAds.PaymentDetails.FromChannelBalance -> ChannelBalance.V0 + is LiquidityAds.PaymentDetails.FromFutureHtlc -> FutureHtlc.V0(this.paymentHashes) + is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> FutureHtlcWithPreimage.V0(this.preimages) + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> ChannelBalanceForFutureHtlc.V0(this.paymentHashes) + } + } +} \ No newline at end of file diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/PurchaseData.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/PurchaseData.kt new file mode 100644 index 000000000..336c950d5 --- /dev/null +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/liquidityads/PurchaseData.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:UseSerializers( + SatoshiSerializer::class, + MilliSatoshiSerializer::class +) + +package fr.acinq.phoenix.db.payments.liquidityads + +import fr.acinq.bitcoin.Satoshi +import fr.acinq.lightning.MilliSatoshi +import fr.acinq.phoenix.db.payments.liquidityads.PaymentDetailsData.Companion.asCanonical +import fr.acinq.phoenix.db.payments.liquidityads.PaymentDetailsData.Companion.asDb +import fr.acinq.phoenix.db.serializers.v1.SatoshiSerializer +import fr.acinq.phoenix.db.serializers.v1.MilliSatoshiSerializer +import fr.acinq.lightning.wire.LiquidityAds +import fr.acinq.phoenix.db.payments.DbTypesHelper +import io.ktor.utils.io.charsets.Charsets +import io.ktor.utils.io.core.toByteArray +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +@Serializable +sealed class PurchaseData { + sealed class Standard : PurchaseData() { + @Serializable + data class V0( + val amount: Satoshi, + val miningFees: Satoshi, + val serviceFee: Satoshi, + val paymentDetails: PaymentDetailsData, + ) : Standard() + } + sealed class WithFeeCredit : PurchaseData() { + @Serializable + data class V0( + val amount: Satoshi, + val miningFees: Satoshi, + val serviceFee: Satoshi, + val feeCreditUsed: MilliSatoshi, + val paymentDetails: PaymentDetailsData, + ) : WithFeeCredit() + } + + companion object { + private fun PurchaseData.asCanonical(): LiquidityAds.Purchase = when (this) { + is Standard.V0 -> LiquidityAds.Purchase.Standard( + amount = amount, + fees = LiquidityAds.Fees(miningFee = miningFees, serviceFee = serviceFee), + paymentDetails = paymentDetails.asCanonical() + ) + is WithFeeCredit.V0 -> LiquidityAds.Purchase.WithFeeCredit( + amount = amount, + fees = LiquidityAds.Fees(miningFee = miningFees, serviceFee = serviceFee), + feeCreditUsed = feeCreditUsed, + paymentDetails = paymentDetails.asCanonical() + ) + } + + private fun LiquidityAds.Purchase.asDb(): PurchaseData = when (val value = this) { + is LiquidityAds.Purchase.Standard -> Standard.V0( + amount = value.amount, + miningFees = value.fees.miningFee, + serviceFee = value.fees.serviceFee, + paymentDetails = value.paymentDetails.asDb() + ) + is LiquidityAds.Purchase.WithFeeCredit -> WithFeeCredit.V0( + amount = value.amount, value.fees.miningFee, + serviceFee = value.fees.serviceFee, + paymentDetails = value.paymentDetails.asDb(), + feeCreditUsed = value.feeCreditUsed + ) + } + + /** + * Deserializes a json-encoded blob into a [LiquidityAds.Purchase] object. + * + * @param typeVersion only used for the legacy leased data, where the blob did not contain the type of the object. + */ + @Suppress("DEPRECATION") + fun decodeAsCanonical( + typeVersion: String, + blob: ByteArray, + ): LiquidityAds.Purchase = DbTypesHelper.decodeBlob(blob) { json, format -> + when (typeVersion) { + InboundLiquidityLeaseType.LEASE_V0.name -> format.decodeFromString(json).toLiquidityAdsPurchase() + else -> format.decodeFromString(json).asCanonical() + } + } + + fun LiquidityAds.Purchase.encodeAsDb(): ByteArray = Json.encodeToString(this.asDb()).toByteArray(Charsets.UTF_8) + } +} diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/serializers/v1/TxIdSerializer.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/serializers/v1/TxIdSerializer.kt new file mode 100644 index 000000000..5924f75a5 --- /dev/null +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/serializers/v1/TxIdSerializer.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.phoenix.db.serializers.v1 + +import fr.acinq.bitcoin.TxId + +object TxIdSerializer : AbstractStringSerializer( + name = "TxId", + toString = TxId::toString, + fromString = ::TxId +) diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt index a83405307..482d7a223 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt @@ -92,30 +92,14 @@ class NodeParamsManager( val trampolineNodeId = PublicKey.fromHex("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134") val trampolineNodeUri = NodeUri(id = trampolineNodeId, "13.248.222.197", 9735) const val remoteSwapInXpub = "tpubDAmCFB21J9ExKBRPDcVxSvGs9jtcf8U1wWWbS1xTYmnUsuUHPCoFdCnEGxLE3THSWcQE48GHJnyz8XPbYUivBMbLSMBifFd3G9KmafkM9og" - val defaultLiquidityPolicy = LiquidityPolicy.Auto(maxAbsoluteFee = 5_000.sat, maxRelativeFeeBasisPoints = 50_00 /* 50% */, skipAbsoluteFeeCheck = false) - val payToOpenFeeBase = 100 + val defaultLiquidityPolicy = LiquidityPolicy.Auto( + inboundLiquidityTarget = null, // auto inbound liquidity is disabled (it must be purchased manually) + maxAbsoluteFee = 5_000.sat, + maxRelativeFeeBasisPoints = 50_00 /* 50% */, + skipAbsoluteFeeCheck = false, + maxAllowedFeeCredit = 0.msat, // no fee credit + ) - fun liquidityLeaseRate(amount: Satoshi): LiquidityAds.LeaseRate { - // WARNING : THIS MUST BE KEPT IN SYNC WITH LSP OTHERWISE FUNDING REQUEST WILL BE REJECTED BY PHOENIX - val fundingWeight = if (amount <= 100_000.sat) { - 271 * 2 // 2-inputs (wpkh) / 0-change - } else if (amount <= 250_000.sat) { - 271 * 2 // 2-inputs (wpkh) / 0-change - } else if (amount <= 500_000.sat) { - 271 * 4 // 4-inputs (wpkh) / 0-change - } else if (amount <= 1_000_000.sat) { - 271 * 4 // 4-inputs (wpkh) / 0-change - } else { - 271 * 6 // 6-inputs (wpkh) / 0-change - } - return LiquidityAds.LeaseRate( - leaseDuration = 0, - fundingWeight = fundingWeight, - leaseFeeProportional = 100, // 1% - leaseFeeBase = 0.sat, - maxRelayFeeProportional = 100, - maxRelayFeeBase = 1_000.msat - ) - } + val payToOpenFeeBase = 100 } } \ No newline at end of file diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NotificationsManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NotificationsManager.kt index e13f7b53b..57bab0343 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NotificationsManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NotificationsManager.kt @@ -99,7 +99,22 @@ class NotificationsManager( amount = event.amount, source = event.source, ) - is LiquidityEvents.Rejected.Reason.ChannelInitializing -> Notification.ChannelsInitializing( + is LiquidityEvents.Rejected.Reason.ChannelFundingInProgress -> Notification.ChannelFundingInProgress( + id = UUID.randomUUID(), + createdAt = currentTimestampMillis(), + readAt = null, + amount = event.amount, + source = event.source, + ) + is LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow -> Notification.MissingOffChainAmountTooLow( + id = UUID.randomUUID(), + createdAt = currentTimestampMillis(), + readAt = null, + amount = event.amount, + source = event.source, + ) + is LiquidityEvents.Rejected.Reason.NoMatchingFundingRate, + is LiquidityEvents.Rejected.Reason.TooManyParts -> Notification.GenericError( id = UUID.randomUUID(), createdAt = currentTimestampMillis(), readAt = null, diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PeerManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PeerManager.kt index 365b02a4a..3fdcda0e4 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PeerManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PeerManager.kt @@ -196,14 +196,12 @@ class PeerManager( client = electrumClient, watcher = electrumWatcher, db = databaseManager.databases.filterNotNull().first(), - trustedSwapInTxs = startupParams.trustedSwapInTxs, socketBuilder = null, scope = MainScope() ) _peer.value = peer launch { monitorNodeEvents(nodeParams) } - launch { updatePeerSwapInFeerate(peer) } // The local channels flow must use `bootFlow` first, as `channelsFlow` is empty when the wallet starts. // `bootFlow` data come from the local database and will be overridden by fresh data once the connection @@ -250,14 +248,6 @@ class PeerManager( getPeer().nodeParams.liquidityPolicy.value = newPolicy } - /** Update the peer's swap-in feerate with values from mempool.space estimator. */ - private suspend fun updatePeerSwapInFeerate(peer: Peer) { - configurationManager.mempoolFeerate.filterNotNull().collect { feerate -> - logger.info { "using mempool.space feerate=$feerate" } - peer.swapInFeeratesFlow.value = FeeratePerKw(feerate.hour) - } - } - private suspend fun monitorNodeEvents(nodeParams: NodeParams) { nodeParams.nodeEvents.collect { event -> logger.debug { "collecting node_event=${event::class.simpleName}" } 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 0debf1375..8c021bbb6 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 @@ -1,6 +1,7 @@ package fr.acinq.phoenix.utils import fr.acinq.lightning.db.* +import fr.acinq.lightning.payment.OfferPaymentMetadata import fr.acinq.phoenix.data.WalletPaymentInfo import kotlinx.datetime.Instant @@ -124,8 +125,8 @@ 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 IncomingPayment.Origin.Offer -> when (origin.metadata) { + is OfferPaymentMetadata.V1 -> "Incoming payment to your offer" } } is LightningOutgoingPayment -> when (val details = payment.details) { @@ -136,7 +137,7 @@ class CsvWriter { is SpliceOutgoingPayment -> "Outgoing splice to ${payment.address}" is ChannelCloseOutgoingPayment -> "Channel closing to ${payment.address}" is SpliceCpfpOutgoingPayment -> "Accelerate transactions with CPFP" - is InboundLiquidityOutgoingPayment -> "+${payment.lease.amount.sat} sat inbound liquidity" + is InboundLiquidityOutgoingPayment -> "+${payment.purchase.amount.sat} sat inbound liquidity" } row += ",${processField(details)}" } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt index 3c9d7eb37..23c63fdfb 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt @@ -17,6 +17,7 @@ package fr.acinq.phoenix.utils.extensions import fr.acinq.bitcoin.PrivateKey +import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment import fr.acinq.lightning.db.IncomingPayment import fr.acinq.lightning.db.LightningOutgoingPayment @@ -25,6 +26,9 @@ import fr.acinq.lightning.db.OutgoingPayment import fr.acinq.lightning.db.WalletPayment import fr.acinq.lightning.payment.Bolt12Invoice import fr.acinq.lightning.payment.OfferPaymentMetadata +import fr.acinq.lightning.utils.getValue +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sum import fr.acinq.lightning.wire.OfferTypes /** Standardized location for extending types from: fr.acinq.lightning. */ @@ -65,6 +69,19 @@ fun WalletPayment.state(): WalletPaymentState = when (this) { } } +/** + * Incoming payments may be received (in part or entirely) as a fee credit. This happens when an on-chain operation + * would be necessary to complete the payment, but the amount received is too low to pay for this operation just yet. + * The payment is then accepted, but the amount is accrued to a fee credit. + * + * This fee credit in the wallet is not part of the wallet's balance. It and can only be spent to pay future mining + * or service fees. It serves as a buffer that allows the user to keep accepting incoming payments seamlessly. + * + * Most of the time, this value is null (i.e., the amount received goes to the balance). + */ +val IncomingPayment.amountFeeCredit : MilliSatoshi? + get() = this.received?.receivedWith?.filterIsInstance()?.map { it.amountReceived }?.sum() + fun WalletPayment.paymentHashString(): String = when (this) { is OnChainOutgoingPayment -> throw NotImplementedError("no payment hash for on-chain outgoing") is LightningOutgoingPayment -> paymentHash.toString() diff --git a/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/InboundLiquidityOutgoing.sq b/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/InboundLiquidityOutgoing.sq index aa84ff6a5..565b84644 100644 --- a/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/InboundLiquidityOutgoing.sq +++ b/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/InboundLiquidityOutgoing.sq @@ -1,5 +1,3 @@ -import fr.acinq.phoenix.db.payments.InboundLiquidityLeaseTypeVersion; - -- Stores in a flat row payments standing for an inbound liquidity request (which are done through a splice). -- The lease data are stored in a complex column, as a json-encoded blob. See InboundLiquidityLeaseType file. CREATE TABLE IF NOT EXISTS inbound_liquidity_outgoing_payments ( @@ -7,8 +5,9 @@ CREATE TABLE IF NOT EXISTS inbound_liquidity_outgoing_payments ( mining_fees_sat INTEGER NOT NULL, channel_id BLOB NOT NULL, tx_id BLOB NOT NULL, - lease_type TEXT AS InboundLiquidityLeaseTypeVersion NOT NULL, + lease_type TEXT NOT NULL, lease_blob BLOB NOT NULL, + payment_details_type TEXT DEFAULT NULL, created_at INTEGER NOT NULL, confirmed_at INTEGER DEFAULT NULL, locked_at INTEGER DEFAULT NULL @@ -16,8 +15,8 @@ CREATE TABLE IF NOT EXISTS inbound_liquidity_outgoing_payments ( insert: INSERT INTO inbound_liquidity_outgoing_payments ( - id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, created_at, confirmed_at, locked_at -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, payment_details_type, created_at, confirmed_at, locked_at +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); setConfirmed: UPDATE inbound_liquidity_outgoing_payments SET confirmed_at=? WHERE id=?; @@ -30,5 +29,10 @@ SELECT id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, created_a FROM inbound_liquidity_outgoing_payments WHERE id=?; +getByTxId: +SELECT id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, created_at, confirmed_at, locked_at +FROM inbound_liquidity_outgoing_payments +WHERE tx_id=?; + delete: DELETE FROM inbound_liquidity_outgoing_payments WHERE id=?; diff --git a/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/migrations/9.sqm b/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/migrations/9.sqm new file mode 100644 index 000000000..0b8973f65 --- /dev/null +++ b/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/migrations/9.sqm @@ -0,0 +1,8 @@ +import fr.acinq.phoenix.db.payments.InboundLiquidityLeaseTypeVersion; + +-- Migration: v9 -> v10 +-- +-- Changes: +-- * Added a new column [payment_details_type] in table [inbound_liquidity_outgoing_payments] + +ALTER TABLE inbound_liquidity_outgoing_payments ADD COLUMN payment_details_type TEXT DEFAULT NULL; diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/IncomingPaymentDbTypeVersionTest.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/IncomingPaymentDbTypeVersionTest.kt index e20110e97..6fa68f9f0 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/IncomingPaymentDbTypeVersionTest.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/IncomingPaymentDbTypeVersionTest.kt @@ -62,7 +62,7 @@ class IncomingPaymentDbTypeVersionTest { @Test @Suppress("DEPRECATION") fun incoming_receivedwith_multipart_v0_lightning() { - val receivedWith = listOf(IncomingPayment.ReceivedWith.LightningPayment(100_000.msat, ByteVector32.One, 2L)) + val receivedWith = listOf(IncomingPayment.ReceivedWith.LightningPayment(100_000.msat, ByteVector32.One, 2L, null)) val deserialized = IncomingReceivedWithData.deserialize( IncomingReceivedWithTypeVersion.MULTIPARTS_V0, receivedWith.mapToDb()!!.second, @@ -74,7 +74,7 @@ class IncomingPaymentDbTypeVersionTest { @Test fun incoming_receivedwith_multipart_v1_lightning() { - val receivedWith = listOf(IncomingPayment.ReceivedWith.LightningPayment(100_000.msat, ByteVector32.One, 2L)) + val receivedWith = listOf(IncomingPayment.ReceivedWith.LightningPayment(100_000.msat, ByteVector32.One, 2L, null)) val deserialized = IncomingReceivedWithData.deserialize( IncomingReceivedWithTypeVersion.MULTIPARTS_V1, receivedWith.mapToDb()!!.second, @@ -146,7 +146,7 @@ class IncomingPaymentDbTypeVersionTest { IncomingOriginTypeVersion.INVOICE_V0 ).first() as IncomingPayment.ReceivedWith.LightningPayment - assertEquals(999_999.msat, deserialized.amount) + assertEquals(999_999.msat, deserialized.amountReceived) assertEquals(0.msat, deserialized.fees) assertEquals(ByteVector32.Zeroes, deserialized.channelId) assertEquals(0L, deserialized.htlcId) @@ -162,7 +162,7 @@ class IncomingPaymentDbTypeVersionTest { IncomingOriginTypeVersion.SWAPIN_V0 ) .first() as IncomingPayment.ReceivedWith.NewChannel - assertEquals(123_456.msat, deserialized.amount) + assertEquals(123_456.msat, deserialized.amountReceived) assertEquals(15_000.msat, deserialized.fees) assertEquals(channelId1, deserialized.channelId) } diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/SqlitePaymentsDatabaseTest.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/SqlitePaymentsDatabaseTest.kt index aef6f06b9..37d2dc85d 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/SqlitePaymentsDatabaseTest.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/SqlitePaymentsDatabaseTest.kt @@ -21,14 +21,11 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes32 -import fr.acinq.lightning.channel.TooManyAcceptedHtlcs import fr.acinq.lightning.db.* import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.payment.FinalFailure -import fr.acinq.lightning.payment.OutgoingPaymentFailure -import fr.acinq.lightning.payment.PaymentRequest import fr.acinq.lightning.utils.* -import fr.acinq.lightning.wire.TemporaryNodeFailure +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.data.WalletPaymentFetchOptions import fr.acinq.phoenix.runTest import fr.acinq.phoenix.utils.migrations.LegacyChannelCloseHelper @@ -43,12 +40,12 @@ class SqlitePaymentsDatabaseTest { private val paymentHash1 = Crypto.sha256(preimage1).toByteVector32() private val origin1 = IncomingPayment.Origin.Invoice(createInvoice(preimage1)) private val channelId1 = randomBytes32() - private val receivedWith1 = listOf(IncomingPayment.ReceivedWith.LightningPayment(100_000.msat, channelId1, 1L)) - private val receivedWith3 = listOf(IncomingPayment.ReceivedWith.LightningPayment(150_000.msat, channelId1, 1L)) + private val receivedWith1 = listOf(IncomingPayment.ReceivedWith.LightningPayment(100_000.msat, channelId1, 1L, null)) + private val receivedWith3 = listOf(IncomingPayment.ReceivedWith.LightningPayment(150_000.msat, channelId1, 1L, fundingFee = LiquidityAds.FundingFee(2_000.msat, TxId(randomBytes32())))) private val preimage2 = randomBytes32() private val receivedWith2 = listOf( - IncomingPayment.ReceivedWith.NewChannel(amount = 1_995_000.msat, serviceFee = 5_000.msat, channelId = randomBytes32(), txId = TxId(randomBytes32()), miningFee = 100.sat, confirmedAt = 100, lockedAt = 200) + IncomingPayment.ReceivedWith.NewChannel(amountReceived = 1_995_000.msat, serviceFee = 5_000.msat, channelId = randomBytes32(), txId = TxId(randomBytes32()), miningFee = 100.sat, confirmedAt = 100, lockedAt = 200) ) val origin3 = IncomingPayment.Origin.SwapIn(address = "1PwLgmRdDjy5GAKWyp8eyAC4SFzWuboLLb") @@ -90,8 +87,8 @@ class SqlitePaymentsDatabaseTest { val origin = IncomingPayment.Origin.Invoice(createInvoice(preimage, 1_000_000_000.msat)) val channelId = randomBytes32() val txId = TxId(randomBytes32()) - val mppPart1 = IncomingPayment.ReceivedWith.NewChannel(amount = 600_000_000.msat, serviceFee = 5_000.msat, miningFee = 100.sat, channelId = channelId, txId = txId, confirmedAt = 100, lockedAt = 50) - val mppPart2 = IncomingPayment.ReceivedWith.NewChannel(amount = 400_000_000.msat, serviceFee = 5_000.msat, miningFee = 200.sat, channelId = channelId, txId = txId, confirmedAt = 115, lockedAt = 75) + val mppPart1 = IncomingPayment.ReceivedWith.NewChannel(amountReceived = 600_000_000.msat, serviceFee = 5_000.msat, miningFee = 100.sat, channelId = channelId, txId = txId, confirmedAt = 100, lockedAt = 50) + val mppPart2 = IncomingPayment.ReceivedWith.NewChannel(amountReceived = 400_000_000.msat, serviceFee = 5_000.msat, miningFee = 200.sat, channelId = channelId, txId = txId, confirmedAt = 115, lockedAt = 75) val receivedWith = listOf(mppPart1, mppPart2) db.addIncomingPayment(preimage, origin, 0) @@ -106,8 +103,8 @@ class SqlitePaymentsDatabaseTest { val origin = IncomingPayment.Origin.Invoice(createInvoice(preimage, 1_000_000_000.msat)) val channelId = randomBytes32() val txId = TxId(randomBytes32()) - val mppPart1 = IncomingPayment.ReceivedWith.NewChannel(amount = 500_000_000.msat, serviceFee = 5_000.msat, miningFee = 200.sat, channelId = channelId, txId = txId, confirmedAt = 100, lockedAt = 50) - val mppPart2 = IncomingPayment.ReceivedWith.NewChannel(amount = 500_000_000.msat, serviceFee = 5_000.msat, miningFee = 150.sat, channelId = channelId, txId = txId, confirmedAt = 115, lockedAt = 75) + val mppPart1 = IncomingPayment.ReceivedWith.NewChannel(amountReceived = 500_000_000.msat, serviceFee = 5_000.msat, miningFee = 200.sat, channelId = channelId, txId = txId, confirmedAt = 100, lockedAt = 50) + val mppPart2 = IncomingPayment.ReceivedWith.NewChannel(amountReceived = 500_000_000.msat, serviceFee = 5_000.msat, miningFee = 150.sat, channelId = channelId, txId = txId, confirmedAt = 115, lockedAt = 75) val receivedWith = listOf(mppPart1, mppPart2) db.addIncomingPayment(preimage, origin, 0) diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/cloud/CloudDataTest.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/cloud/CloudDataTest.kt index adcf64408..5ff812723 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/cloud/CloudDataTest.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/cloud/CloudDataTest.kt @@ -9,6 +9,7 @@ import fr.acinq.lightning.db.* import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.payment.FinalFailure import fr.acinq.lightning.utils.* +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.runTest import fr.acinq.secp256k1.Hex import kotlin.test.* @@ -86,10 +87,10 @@ class CloudDataTest { fun incoming__receivedWith_lightning() = runTest { val invoice = createBolt11Invoice(preimage, 250_000.msat) val receivedWith1 = IncomingPayment.ReceivedWith.LightningPayment( - amount = 100_000.msat, channelId = channelId, htlcId = 1L + amountReceived = 100_000.msat, channelId = channelId, htlcId = 1L, fundingFee = null ) val receivedWith2 = IncomingPayment.ReceivedWith.LightningPayment( - amount = 150_000.msat, channelId = channelId, htlcId = 1L + amountReceived = 150_000.msat, channelId = channelId, htlcId = 1L, fundingFee = LiquidityAds.FundingFee(amount = 1_000.msat, TxId(ByteVector32.Zeroes)) ) testRoundtrip( IncomingPayment( @@ -104,7 +105,7 @@ class CloudDataTest { fun incoming__receivedWith_newChannel() = runTest { val invoice = createBolt11Invoice(preimage, 10_000_000.msat) val receivedWith = IncomingPayment.ReceivedWith.NewChannel( - amount = 7_000_000.msat, miningFee = 2_000.sat, serviceFee = 1_000_000.msat, channelId = channelId, txId = TxId(randomBytes32()), confirmedAt = 500, lockedAt = 800 + amountReceived = 7_000_000.msat, miningFee = 2_000.sat, serviceFee = 1_000_000.msat, channelId = channelId, txId = TxId(randomBytes32()), confirmedAt = 500, lockedAt = 800 ) testRoundtrip( IncomingPayment( @@ -127,8 +128,8 @@ class CloudDataTest { val expectedChannelId = Hex.decode("e8a0e7ba91a485ed6857415cc0c60f77eda6cb1ebe1da841d42d7b4388cc2bcc").byteVector32() val expectedReceived = IncomingPayment.Received( receivedWith = listOf( - IncomingPayment.ReceivedWith.NewChannel(amount = 7_000_000.msat, miningFee = 0.sat, serviceFee = 3_000_000.msat, channelId = expectedChannelId, txId = TxId(ByteVector32.Zeroes), confirmedAt = 0, lockedAt = 0), - IncomingPayment.ReceivedWith.NewChannel(amount = 9_000_000.msat, miningFee = 0.sat, serviceFee = 6_000_000.msat, channelId = expectedChannelId, txId = TxId(ByteVector32.Zeroes), confirmedAt = 0, lockedAt = 0) + IncomingPayment.ReceivedWith.NewChannel(amountReceived = 7_000_000.msat, miningFee = 0.sat, serviceFee = 3_000_000.msat, channelId = expectedChannelId, txId = TxId(ByteVector32.Zeroes), confirmedAt = 0, lockedAt = 0), + IncomingPayment.ReceivedWith.NewChannel(amountReceived = 9_000_000.msat, miningFee = 0.sat, serviceFee = 6_000_000.msat, channelId = expectedChannelId, txId = TxId(ByteVector32.Zeroes), confirmedAt = 0, lockedAt = 0) ), receivedAt = 1658246347319 ) diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/CsvWriterTests.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/CsvWriterTests.kt index 3fabedb5c..27bcdb78d 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/CsvWriterTests.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/CsvWriterTests.kt @@ -20,6 +20,7 @@ import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.utils.currentTimestampSeconds import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.TestConstants import fr.acinq.phoenix.data.ExchangeRate import fr.acinq.phoenix.data.FiatCurrency @@ -42,7 +43,7 @@ class CsvWriterTests { received = IncomingPayment.Received( receivedWith = listOf( IncomingPayment.ReceivedWith.NewChannel( - amount = 12_000_000.msat, + amountReceived = 12_000_000.msat, serviceFee = 3_000_000.msat, miningFee = 0.sat, channelId = randomBytes32(), @@ -78,9 +79,10 @@ class CsvWriterTests { received = IncomingPayment.Received( receivedWith = listOf( IncomingPayment.ReceivedWith.LightningPayment( - amount = 2_173_929.msat, + amountReceived = 2_173_929.msat, channelId = randomBytes32(), - htlcId = 0 + htlcId = 0, + fundingFee = LiquidityAds.FundingFee(2_000.msat, TxId(randomBytes32())) ) ), receivedAt = 1675270484965 @@ -92,7 +94,7 @@ class CsvWriterTests { userNotes = null ) - val expected = "2023-02-01T16:54:44.965Z,2173929,0,0.4999 USD,0.0000 USD,Incoming LN payment,Cafécito,\r\n" + val expected = "2023-02-01T16:54:44.965Z,2173929,-2000,0.4999 USD,-0.0004 USD,Incoming LN payment,Cafécito,\r\n" val actual = CsvWriter.makeRow( info = WalletPaymentInfo(payment, metadata, null, WalletPaymentFetchOptions.All), localizedDescription = "Cafécito", @@ -238,7 +240,7 @@ class CsvWriterTests { received = IncomingPayment.Received( receivedWith = listOf( IncomingPayment.ReceivedWith.NewChannel( - amount = 12_000_000.msat, + amountReceived = 12_000_000.msat, serviceFee = 2_931_000.msat, miningFee = 69.sat, channelId = randomBytes32(), diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt index df2e1a878..8c8a96a3c 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt @@ -129,30 +129,33 @@ class ParserTest { @Test fun parse_bitcoin_uri_with_lightning_invoice() { - listOf>>( - // valid lightning invoice - "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?foo=bar&lightning=lntb15u1p05vazrpp5apz75ghtq3ynmc5qm98tsgucmsav44fyffpguhzdep2kcgkfme4sdq4xysyymr0vd4kzcmrd9hx7cqp2xqrrss9qy9qsqsp5v4hqr48qe0u7al6lxwdpmp3w6k7evjdavm0lh7arpv3qaf038s5st2d8k8vvmxyav2wkfym9jp4mk64srmswgh7l6sqtq7l4xl3nknf8snltamvpw5p3yl9nxg0ax9k0698rr94qx6unrv8yhccmh4z9ghcq77hxps" to Either.Right( - BitcoinUri( - chain = Chain.Mainnet, - address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", - script = ByteVector("0014751e76e8199196d454941c45d1b3a323f1433bd6"), - paymentRequest = Bolt11Invoice.read("lntb15u1p05vazrpp5apz75ghtq3ynmc5qm98tsgucmsav44fyffpguhzdep2kcgkfme4sdq4xysyymr0vd4kzcmrd9hx7cqp2xqrrss9qy9qsqsp5v4hqr48qe0u7al6lxwdpmp3w6k7evjdavm0lh7arpv3qaf038s5st2d8k8vvmxyav2wkfym9jp4mk64srmswgh7l6sqtq7l4xl3nknf8snltamvpw5p3yl9nxg0ax9k0698rr94qx6unrv8yhccmh4z9ghcq77hxps") - .get(), - ignoredParams = ParametersBuilder().apply { set("foo", "bar") }.build() - ) - ), - // invalid lightning invoice - "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?lightning=lntb15u1p05vazrpp" to Either.Right( - BitcoinUri(chain = Chain.Mainnet, address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", script = ByteVector("0014751e76e8199196d454941c45d1b3a323f1433bd6")) - ), - // empty lightning invoice - "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?lightning=" to Either.Right( - BitcoinUri(chain = Chain.Mainnet, address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", script = ByteVector("0014751e76e8199196d454941c45d1b3a323f1433bd6")) + assertEquals( + expected = BitcoinUri( + chain = Chain.Mainnet, + address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + script = ByteVector("0014751e76e8199196d454941c45d1b3a323f1433bd6"), + paymentRequest = Bolt11Invoice.read("lnbc10u1pn08ld4pp5x7vvjs56tsj65c5xxe43xtxc22n6umuc89hwjndkwkazduzqhsesdpcge6kuerfdenjqspsxvcxxd33vseryefqdahzqum5v93kketj9ehx2amncqzzsxqrrs0sp5g2gjnwnsmy7xfprvjsuppymeqvr0zm3tksmqtg2sqdyqmxaxasxq9qxpqysgq3rj5d9vx7vsmfe8pxzqx7jzes77sta32yp9rqx78dkh4fn8lg8mk9kzh29255qgamcdddf30pp6hptk0u432sg39h3rjxru0ec5edycpxpqmg3").get(), + ignoredParams = ParametersBuilder().apply { set("foo", "bar") }.build() ), - ).forEach { (address, expected) -> - val uri = Parser.parseBip21Uri(Chain.Mainnet, address) - assertEquals(expected, uri) - } + actual = Parser.parseBip21Uri( + chain = Chain.Mainnet, + input = "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?foo=bar&lightning=lnbc10u1pn08ld4pp5x7vvjs56tsj65c5xxe43xtxc22n6umuc89hwjndkwkazduzqhsesdpcge6kuerfdenjqspsxvcxxd33vseryefqdahzqum5v93kketj9ehx2amncqzzsxqrrs0sp5g2gjnwnsmy7xfprvjsuppymeqvr0zm3tksmqtg2sqdyqmxaxasxq9qxpqysgq3rj5d9vx7vsmfe8pxzqx7jzes77sta32yp9rqx78dkh4fn8lg8mk9kzh29255qgamcdddf30pp6hptk0u432sg39h3rjxru0ec5edycpxpqmg3" + ).right + ) + assertEquals( + expected = BitcoinUri(chain = Chain.Mainnet, address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", script = ByteVector("0014751e76e8199196d454941c45d1b3a323f1433bd6"), paymentRequest = null), + actual = Parser.parseBip21Uri( + chain = Chain.Mainnet, + input = "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?lightning=lntb15u1p05vazrpp" + ).right + ) + assertEquals( + expected = BitcoinUri(chain = Chain.Mainnet, address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", script = ByteVector("0014751e76e8199196d454941c45d1b3a323f1433bd6"), paymentRequest = null), + actual = Parser.parseBip21Uri( + chain = Chain.Mainnet, + input = "bitcoin:bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?lightning=" + ).right + ) } @Test diff --git a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/db/iosDbFactory.kt b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/db/iosDbFactory.kt index 12ec20df2..336fae9fd 100644 --- a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/db/iosDbFactory.kt +++ b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/db/iosDbFactory.kt @@ -30,7 +30,11 @@ actual fun createChannelsDbDriver( nodeIdHash: String ): SqlDriver { val schema = ChannelsDatabase.Schema - val name = "channels-${chain.name.lowercase()}-$nodeIdHash.sqlite" + val chainName = when (chain) { + is Chain.Testnet3 -> "testnet" + else -> chain.name.lowercase() + } + val name = "channels-$chainName-$nodeIdHash.sqlite" // The foreign_keys constraint needs to be set via the DatabaseConfiguration: // https://github.com/cashapp/sqldelight/issues/1356 @@ -59,7 +63,11 @@ actual fun createPaymentsDbDriver( nodeIdHash: String ): SqlDriver { val schema = PaymentsDatabase.Schema - val name = "payments-${chain.name.lowercase()}-$nodeIdHash.sqlite" + val chainName = when (chain) { + is Chain.Testnet3 -> "testnet" + else -> chain.name.lowercase() + } + val name = "payments-$chainName-$nodeIdHash.sqlite" val dbDir = getDatabaseFilesDirectoryPath(ctx) val configuration = DatabaseConfiguration( From e4b7f8b331cc512a4a2d44bdb0ae0b51aadb338a Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:32:47 +0200 Subject: [PATCH 02/25] (android) Update the payment screens for liquidity purchases With the new on-the-fly mechanism, the automated liquidity operation necesssary to receive some payments are stored as specific payments. That is, the incoming payments related to this operation do not have a fee, instead there's a payment row in the payments history specific to the liquidity purchase. The operation also has its own payment details page just like the (already existing) manual liquidity purchase. The splash screen file has been split into several files, one for each payment types. This makes the code more flexible. The liquidity policy objects set in the channels management screen have been updated to match the NodeParamsManager value. This commit is a WIP, especially for wording and localisation. --- .../android/components/BottomSheetDialog.kt | 70 ++ .../phoenix/android/components/Dialogs.kt | 4 +- .../android/components/SplashLayout.kt | 22 +- .../details/PaymentDetailsSplashView.kt | 880 ------------------ .../details/PaymentDetailsTechnicalView.kt | 74 +- .../payments/details/PaymentDetailsView.kt | 1 + .../android/payments/details/PaymentLine.kt | 2 +- .../details/splash/PaymentSplashStatus.kt | 388 ++++++++ .../details/splash/PaymentSplashView.kt | 233 +++++ .../details/splash/SplashChannelClose.kt | 86 ++ .../payments/details/splash/SplashIncoming.kt | 141 +++ .../details/splash/SplashLightningOut.kt | 237 +++++ .../details/splash/SplashLiquidityPurchase.kt | 267 ++++++ .../details/splash/SplashSpliceOut.kt | 73 ++ .../details/splash/SplashSpliceOutCpfp.kt | 73 ++ .../liquidity/RequestLiquidityView.kt | 14 +- .../liquidity/RequestLiquidityViewModel.kt | 21 +- .../android/payments/offer/SendOfferView.kt | 2 +- .../phoenix/android/services/NodeService.kt | 25 +- .../android/settings/NotificationsView.kt | 6 +- .../fees/AdvancedIncomingFeePolicy.kt | 11 +- .../settings/fees/LiquidityPolicyView.kt | 11 +- .../settings/walletinfo/SwapInWalletInfo.kt | 4 +- .../android/utils/LegacyMigrationHelper.kt | 15 +- .../android/utils/SystemNotificationHelper.kt | 4 +- .../utils/datastore/UserPrefsRepository.kt | 9 +- .../acinq/phoenix/android/utils/extensions.kt | 49 +- .../res/values-b+es+419/important_strings.xml | 1 - .../src/main/res/values-b+es+419/strings.xml | 2 +- .../main/res/values-cs/important_strings.xml | 1 - .../main/res/values-de/important_strings.xml | 1 - .../src/main/res/values-de/strings.xml | 3 +- .../main/res/values-es/important_strings.xml | 1 - .../main/res/values-fr/important_strings.xml | 1 - .../src/main/res/values-fr/strings.xml | 1 - .../res/values-pt-rBR/important_strings.xml | 1 - .../main/res/values-sk/important_strings.xml | 1 - .../src/main/res/values-sk/strings.xml | 1 - .../main/res/values-sw/important_strings.xml | 1 - .../src/main/res/values-sw/strings.xml | 1 - .../main/res/values-vi/important_strings.xml | 1 - .../src/main/res/values-vi/strings.xml | 1 - .../src/main/res/values/important_strings.xml | 6 +- .../src/main/res/values/strings.xml | 7 +- 44 files changed, 1771 insertions(+), 982 deletions(-) create mode 100644 phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/BottomSheetDialog.kt delete mode 100644 phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsSplashView.kt create mode 100644 phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashStatus.kt create mode 100644 phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt create mode 100644 phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt create mode 100644 phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt create mode 100644 phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt create mode 100644 phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt create mode 100644 phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOut.kt create mode 100644 phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOutCpfp.kt diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/BottomSheetDialog.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/BottomSheetDialog.kt new file mode 100644 index 000000000..e142c3d35 --- /dev/null +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/BottomSheetDialog.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.phoenix.android.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BottomSheetDialog( + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + skipPartiallyExpanded: Boolean = true, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + scrimAlpha: Float = 0.2f, + internalPadding: PaddingValues = PaddingValues(top = 0.dp, start = 20.dp, end = 20.dp, bottom = 64.dp), + content: @Composable ColumnScope.() -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded) + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { + // executed when user click outside the sheet, and after sheet has been hidden thru state. + onDismiss() + }, + modifier = modifier, + containerColor = MaterialTheme.colors.surface, + contentColor = MaterialTheme.colors.onSurface, + scrimColor = MaterialTheme.colors.onBackground.copy(alpha = scrimAlpha), + ) { + Column( + horizontalAlignment = horizontalAlignment, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .verticalScroll(rememberScrollState()) + .padding(internalPadding) + ) { + content() + } + } +} \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Dialogs.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Dialogs.kt index c8925eb76..23166c70b 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Dialogs.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Dialogs.kt @@ -179,7 +179,7 @@ fun RowScope.IconPopup( popupLink: Pair? = null, spaceLeft: Dp? = 8.dp, spaceRight: Dp? = null, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { var showPopup by remember { mutableStateOf(false) } spaceLeft?.let { Spacer(Modifier.requiredWidth(it)) } @@ -191,7 +191,7 @@ fun RowScope.IconPopup( padding = PaddingValues(iconPadding), modifier = modifier.requiredSize(iconSize), interactionSource = interactionSource, - onClick = { showPopup = true } + onClick = { showPopup = true }, ) if (showPopup) { PopupDialog(onDismiss = { showPopup = false }, message = popupMessage, button = popupLink?.let { (text, link) -> diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt index 7afac378b..7a47b94b0 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text @@ -34,7 +33,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -127,27 +125,29 @@ fun SplashLabelRow( ) { Row { Row( - modifier = Modifier.weight(1f).alignByBaseline(), + modifier = Modifier + .weight(1f) + .alignByBaseline(), horizontalArrangement = Arrangement.End ) { + Spacer(modifier = Modifier.weight(1f)) + if (helpMessage != null) { + IconPopup(modifier = Modifier.offset(y = (-3).dp), popupMessage = helpMessage, popupLink = helpLink, spaceLeft = 0.dp, spaceRight = 5.dp) + } Text( text = label.uppercase(), - style = MaterialTheme.typography.subtitle1.copy(fontSize = 12.sp, textAlign = TextAlign.End), + style = MaterialTheme.typography.subtitle1.copy(fontSize = 12.sp), maxLines = 2, overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f) ) - if (helpMessage != null) { - IconPopup(modifier = Modifier.offset(y = (-3).dp), popupMessage = helpMessage, popupLink = helpLink, spaceLeft = 4.dp, spaceRight = 0.dp) - } if (icon != null) { - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(3.dp)) Image( painter = painterResource(id = icon), colorFilter = ColorFilter.tint(iconTint), contentDescription = null, modifier = Modifier - .size(ButtonDefaults.IconSize) + .size(17.dp) .offset(y = (-2).dp) ) } @@ -175,7 +175,7 @@ fun SplashClickableContent( .offset(x = (-8).dp), shape = RoundedCornerShape(12.dp) ) { - Column(modifier = Modifier.padding(8.dp)) { + Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)) { content() } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsSplashView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsSplashView.kt deleted file mode 100644 index f59485151..000000000 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsSplashView.kt +++ /dev/null @@ -1,880 +0,0 @@ -/* - * Copyright 2023 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.phoenix.android.payments.details - -import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi -import androidx.compose.animation.graphics.res.animatedVectorResource -import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter -import androidx.compose.animation.graphics.vector.AnimatedImageVector -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.PublicKey -import fr.acinq.bitcoin.TxId -import fr.acinq.bitcoin.utils.Either -import fr.acinq.lightning.blockchain.electrum.ElectrumConnectionStatus -import fr.acinq.lightning.db.* -import fr.acinq.lightning.payment.FinalFailure -import fr.acinq.lightning.payment.OutgoingPaymentFailure -import fr.acinq.lightning.utils.msat -import fr.acinq.lightning.utils.sum -import fr.acinq.lightning.wire.LiquidityAds -import fr.acinq.phoenix.android.LocalBitcoinUnit -import fr.acinq.phoenix.android.R -import fr.acinq.phoenix.android.business -import fr.acinq.phoenix.android.components.* -import fr.acinq.phoenix.android.components.contact.ContactCompactView -import fr.acinq.phoenix.android.components.contact.ContactOrOfferView -import fr.acinq.phoenix.android.components.contact.OfferContactState -import fr.acinq.phoenix.android.payments.cpfp.CpfpView -import fr.acinq.phoenix.android.utils.* -import fr.acinq.phoenix.android.utils.Converter.toPrettyString -import fr.acinq.phoenix.android.utils.Converter.toRelativeDateString -import fr.acinq.phoenix.data.LnurlPayMetadata -import fr.acinq.phoenix.data.WalletPaymentId -import fr.acinq.phoenix.data.WalletPaymentInfo -import fr.acinq.phoenix.data.lnurl.LnurlPay -import fr.acinq.phoenix.utils.extensions.WalletPaymentState -import fr.acinq.phoenix.utils.extensions.minDepthForFunding -import fr.acinq.phoenix.utils.extensions.incomingOfferMetadata -import fr.acinq.phoenix.utils.extensions.outgoingInvoiceRequest -import fr.acinq.phoenix.utils.extensions.state -import io.ktor.http.Url -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - -@Composable -fun PaymentDetailsSplashView( - onBackClick: () -> Unit, - data: WalletPaymentInfo, - onDetailsClick: (WalletPaymentId) -> Unit, - onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, - fromEvent: Boolean, -) { - val payment = data.payment - SplashLayout( - header = { DefaultScreenHeader(onBackClick = onBackClick) }, - topContent = { PaymentStatus(data.payment, fromEvent, onCpfpSuccess = onBackClick) } - ) { - AmountView( - amount = when (payment) { - is InboundLiquidityOutgoingPayment -> payment.amount - is OutgoingPayment -> payment.amount - payment.fees - is IncomingPayment -> payment.amount - }, - amountTextStyle = MaterialTheme.typography.body1.copy(fontSize = 30.sp), - separatorSpace = 4.dp, - prefix = stringResource(id = if (payment is OutgoingPayment) R.string.paymentline_prefix_sent else R.string.paymentline_prefix_received) - ) - - Spacer(modifier = Modifier.height(36.dp)) - PrimarySeparator( - height = 6.dp, - color = when (payment.state()) { - WalletPaymentState.Failure -> negativeColor - WalletPaymentState.SuccessOffChain, WalletPaymentState.SuccessOnChain -> positiveColor - else -> mutedBgColor - } - ) - Spacer(modifier = Modifier.height(36.dp)) - - if (data.payment is LightningOutgoingPayment && data.metadata.lnurl != null) { - LnurlPayInfoView(data.payment as LightningOutgoingPayment, data.metadata.lnurl!!) - } - - payment.incomingOfferMetadata()?.let { meta -> - meta.payerNote?.takeIf { it.isNotBlank() }?.let { - OfferPayerNote(payerNote = it) - Spacer(modifier = Modifier.height(8.dp)) - } - OfferSentBy(payerPubkey = meta.payerKey, !meta.payerNote.isNullOrBlank()) - } - - payment.outgoingInvoiceRequest()?.payerNote?.takeIf { it.isNotBlank() }?.let { - OfferPayerNote(payerNote = it) - } - - PaymentDescriptionView(data = data, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) - PaymentDestinationView(data = data) - PaymentFeeView(payment = payment) - if (payment is InboundLiquidityOutgoingPayment) { - InboundLiquidityLeaseDetails(lease = payment.lease) - } - - if (payment is LightningOutgoingPayment) { - (payment.status as? LightningOutgoingPayment.Status.Completed.Failed)?.let { status -> - PaymentErrorView(status = status, failedParts = payment.parts.map { it.status }.filterIsInstance()) - } - } - - Spacer(modifier = Modifier.height(48.dp)) - BorderButton( - text = stringResource(id = R.string.paymentdetails_details_button), - borderColor = borderColor, - textStyle = MaterialTheme.typography.caption, - icon = R.drawable.ic_tool, - iconTint = MaterialTheme.typography.caption.color, - onClick = { onDetailsClick(data.id()) }, - ) - } -} - -@Composable -private fun PaymentStatus( - payment: WalletPayment, - fromEvent: Boolean, - onCpfpSuccess: () -> Unit, -) { - val peerManager = business.peerManager - when (payment) { - is LightningOutgoingPayment -> when (payment.status) { - is LightningOutgoingPayment.Status.Pending -> PaymentStatusIcon( - message = { Text(text = stringResource(id = R.string.paymentdetails_status_sent_pending)) }, - imageResId = R.drawable.ic_payment_details_pending_static, - isAnimated = false, - color = mutedTextColor - ) - is LightningOutgoingPayment.Status.Completed.Failed -> PaymentStatusIcon( - message = { Text(text = annotatedStringResource(id = R.string.paymentdetails_status_sent_failed), textAlign = TextAlign.Center) }, - imageResId = R.drawable.ic_payment_details_failure_static, - isAnimated = false, - color = negativeColor - ) - is LightningOutgoingPayment.Status.Completed.Succeeded -> PaymentStatusIcon( - message = { - Text(text = annotatedStringResource(id = R.string.paymentdetails_status_sent_successful, payment.completedAt?.toRelativeDateString() ?: "")) - }, - imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, - isAnimated = fromEvent, - color = positiveColor, - ) - } - is ChannelCloseOutgoingPayment -> when (payment.confirmedAt) { - null -> { - PaymentStatusIcon( - message = null, - imageResId = R.drawable.ic_payment_details_pending_onchain_static, - isAnimated = false, - color = mutedTextColor, - ) - ConfirmationView(payment.txId, payment.channelId, isConfirmed = false, canBeBumped = false, onCpfpSuccess = onCpfpSuccess) - } - else -> { - PaymentStatusIcon( - message = { - Text(text = annotatedStringResource(id = R.string.paymentdetails_status_channelclose_confirmed, payment.completedAt?.toRelativeDateString() ?: "")) - }, - imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, - isAnimated = fromEvent, - color = positiveColor, - ) - ConfirmationView(payment.txId, payment.channelId, isConfirmed = true, canBeBumped = false, onCpfpSuccess) - } - } - is SpliceOutgoingPayment -> when (payment.confirmedAt) { - null -> { - PaymentStatusIcon( - message = null, - imageResId = R.drawable.ic_payment_details_pending_onchain_static, - isAnimated = false, - color = mutedTextColor, - ) - ConfirmationView(payment.txId, payment.channelId, isConfirmed = false, canBeBumped = true, onCpfpSuccess = onCpfpSuccess) - } - else -> { - PaymentStatusIcon( - message = { - Text(text = annotatedStringResource(id = R.string.paymentdetails_status_sent_successful, payment.completedAt!!.toRelativeDateString())) - }, - imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, - isAnimated = fromEvent, - color = positiveColor, - ) - ConfirmationView(payment.txId, payment.channelId, isConfirmed = true, canBeBumped = true, onCpfpSuccess = onCpfpSuccess) - } - } - is SpliceCpfpOutgoingPayment -> when (payment.confirmedAt) { - null -> { - PaymentStatusIcon( - message = null, - imageResId = R.drawable.ic_payment_details_pending_onchain_static, - isAnimated = false, - color = mutedTextColor, - ) - ConfirmationView(payment.txId, payment.channelId, isConfirmed = false, canBeBumped = true, onCpfpSuccess = onCpfpSuccess) - } - else -> { - PaymentStatusIcon( - message = { - Text(text = annotatedStringResource(id = R.string.paymentdetails_status_sent_successful, payment.completedAt!!.toRelativeDateString())) - }, - imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, - isAnimated = fromEvent, - color = positiveColor, - ) - ConfirmationView(payment.txId, payment.channelId, isConfirmed = true, canBeBumped = true, onCpfpSuccess = onCpfpSuccess) - } - } - is IncomingPayment -> { - val received = payment.received - when { - received == null -> { - PaymentStatusIcon( - message = { Text(text = stringResource(id = R.string.paymentdetails_status_received_pending)) }, - imageResId = R.drawable.ic_payment_details_pending_static, - isAnimated = false, - color = mutedTextColor - ) - } - received.receivedWith.isEmpty() -> { - PaymentStatusIcon( - message = { Text(text = stringResource(id = R.string.paymentdetails_status_received_paytoopen_pending)) }, - isAnimated = false, - imageResId = R.drawable.ic_clock, - color = mutedTextColor, - ) - } - received.receivedWith.any { it is IncomingPayment.ReceivedWith.OnChainIncomingPayment && it.lockedAt == null } -> { - PaymentStatusIcon( - message = { - Text(text = stringResource(id = R.string.paymentdetails_status_unconfirmed)) - }, - isAnimated = false, - imageResId = R.drawable.ic_clock, - color = mutedTextColor, - ) - } - payment.completedAt == null -> { - PaymentStatusIcon( - message = { - Text(text = stringResource(id = R.string.paymentdetails_status_received_pending)) - }, - imageResId = R.drawable.ic_payment_details_pending_static, - isAnimated = false, - color = mutedTextColor - ) - } - else -> { - PaymentStatusIcon( - message = { - Text(text = annotatedStringResource(id = R.string.paymentdetails_status_received_successful, payment.completedAt!!.toRelativeDateString())) - }, - imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, - isAnimated = fromEvent, - color = positiveColor, - ) - } - } - received?.receivedWith?.filterIsInstance()?.firstOrNull()?.let { - val nodeParams = business.nodeParamsManager.nodeParams.value - val channelMinDepth by produceState(initialValue = null, key1 = Unit) { - nodeParams?.let { params -> - val channelId = payment.received?.receivedWith?.filterIsInstance()?.firstOrNull()?.channelId - value = channelId?.let { peerManager.getChannelWithCommitments(it)?.minDepthForFunding(params) } - } - } - ConfirmationView(it.txId, it.channelId, isConfirmed = it.confirmedAt != null, canBeBumped = false, onCpfpSuccess = onCpfpSuccess, channelMinDepth) - } - } - is InboundLiquidityOutgoingPayment -> when (val lockedAt = payment.lockedAt) { - null -> { - PaymentStatusIcon( - message = null, - imageResId = R.drawable.ic_payment_details_pending_onchain_static, - isAnimated = false, - color = mutedTextColor, - ) - } - else -> { - PaymentStatusIcon( - message = { - Text(text = annotatedStringResource(id = R.string.paymentdetails_status_inbound_liquidity_success, lockedAt.toRelativeDateString())) - }, - imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, - isAnimated = fromEvent, - color = positiveColor, - ) - } - } - } -} - -@OptIn(ExperimentalAnimationGraphicsApi::class) -@Composable -private fun PaymentStatusIcon( - message: (@Composable ColumnScope.() -> Unit)?, - isAnimated: Boolean, - imageResId: Int, - color: Color, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - val scope = rememberCoroutineScope() - var atEnd by remember { mutableStateOf(false) } - Image( - painter = if (isAnimated) { - rememberAnimatedVectorPainter(AnimatedImageVector.animatedVectorResource(imageResId), atEnd) - } else { - painterResource(id = imageResId) - }, - contentDescription = null, - colorFilter = ColorFilter.tint(color), - modifier = Modifier.size(80.dp) - ) - if (isAnimated) { - LaunchedEffect(key1 = Unit) { - scope.launch { - delay(150) - atEnd = true - } - } - } - message?.let { - Spacer(Modifier.height(16.dp)) - Column { it() } - } - } - -} - -@Composable -private fun LnurlPayInfoView(payment: LightningOutgoingPayment, metadata: LnurlPayMetadata) { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_lnurlpay_service)) { - SelectionContainer { - Text(text = metadata.pay.callback.host) - } - } - metadata.successAction?.let { - LnurlSuccessAction(payment = payment, action = it) - } -} - -@Composable -private fun LnurlSuccessAction(payment: LightningOutgoingPayment, action: LnurlPay.Invoice.SuccessAction) { - Spacer(modifier = Modifier.height(8.dp)) - when (action) { - is LnurlPay.Invoice.SuccessAction.Message -> { - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_lnurlpay_action_message_label)) { - SelectionContainer { - Text(text = action.message) - } - } - } - is LnurlPay.Invoice.SuccessAction.Url -> { - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_lnurlpay_action_url_label)) { - Text(text = action.description) - WebLink(text = stringResource(id = R.string.paymentdetails_lnurlpay_action_url_button), url = action.url.toString()) - } - } - is LnurlPay.Invoice.SuccessAction.Aes -> { - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_lnurlpay_action_aes_label)) { - val status = payment.status - if (status is LightningOutgoingPayment.Status.Completed.Succeeded.OffChain) { - val deciphered by produceState(initialValue = null) { - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(status.preimage.toByteArray(), "AES"), IvParameterSpec(action.iv.toByteArray())) - value = String(cipher.doFinal(action.ciphertext.toByteArray()), Charsets.UTF_8) - } - Text(text = action.description) - when (deciphered) { - null -> ProgressView(text = stringResource(id = R.string.paymentdetails_lnurlpay_action_aes_decrypting), padding = PaddingValues(0.dp)) - else -> { - val url = try { - Url(deciphered!!) - } catch (e: Exception) { - null - } - if (url != null) { - WebLink(text = stringResource(id = R.string.paymentdetails_lnurlpay_action_url_button), url = url.toString()) - } else { - SelectionContainer { - Text(text = deciphered!!) - } - } - } - } - } else { - Text(text = stringResource(id = R.string.paymentdetails_lnurlpay_action_aes_decrypting)) - } - } - } - } -} - -@Composable -private fun OfferPayerNote(payerNote: String) { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_offer_note_label)) { - Text(text = payerNote) - } -} - -@Composable -private fun OfferSentBy(payerPubkey: PublicKey?, hasPayerNote: Boolean) { - val contactsManager = business.contactsManager - val contactState = remember { mutableStateOf(OfferContactState.Init) } - LaunchedEffect(Unit) { - contactState.value = payerPubkey?.let { - contactsManager.getContactForPayerPubkey(it) - }?.let { OfferContactState.Found(it) } ?: OfferContactState.NotFound - } - - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_offer_sender_label)) { - when (val res = contactState.value) { - is OfferContactState.Init -> Text(text = stringResource(id = R.string.utils_loading_data)) - is OfferContactState.NotFound -> { - Text(text = stringResource(id = R.string.paymentdetails_offer_sender_unknown)) - if (hasPayerNote) { - Spacer(modifier = Modifier.height(4.dp)) - Text(text = stringResource(id = R.string.paymentdetails_offer_sender_unknown_details), style = MaterialTheme.typography.subtitle2) - } - } - is OfferContactState.Found -> { - ContactCompactView( - contact = res.contact, - currentOffer = null, - onContactChange = { contactState.value = if (it == null) OfferContactState.NotFound else OfferContactState.Found(it) }, - ) - } - } - } -} - -@Composable -private fun PaymentDescriptionView( - data: WalletPaymentInfo, - onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, -) { - var showEditDescriptionDialog by remember { mutableStateOf(false) } - - val peer by business.peerManager.peerState.collectAsState() - val paymentDesc = data.metadata.lnurl?.description ?: data.payment.smartDescription(LocalContext.current) - val customDesc = remember(data) { data.metadata.userDescription?.takeIf { it.isNotBlank() } } - - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_desc_label)) { - val isLegacyMigration = data.isLegacyMigration(peer) - val finalDesc = when (isLegacyMigration) { - null -> stringResource(id = R.string.paymentdetails_desc_closing_channel) // not sure yet, but we still know it's a closing - true -> stringResource(id = R.string.paymentdetails_desc_legacy_migration) - false -> paymentDesc ?: customDesc - } - - if (isLegacyMigration == false) { - SplashClickableContent(onClick = { showEditDescriptionDialog = true }) { - Text( - text = finalDesc ?: stringResource(id = R.string.paymentdetails_no_description), - style = if (finalDesc == null) MaterialTheme.typography.caption.copy(fontStyle = FontStyle.Italic) else MaterialTheme.typography.body1 - ) - Spacer(modifier = Modifier.height(8.dp)) - if (paymentDesc != null && customDesc != null) { - HSeparator(width = 50.dp) - Spacer(modifier = Modifier.height(8.dp)) - Text(text = customDesc, style = MaterialTheme.typography.body1.copy(fontStyle = FontStyle.Italic)) - Spacer(modifier = Modifier.height(8.dp)) - } - TextWithIcon( - text = stringResource( - id = when (customDesc) { - null -> R.string.paymentdetails_attach_desc_button - else -> R.string.paymentdetails_edit_desc_button - } - ), - textStyle = MaterialTheme.typography.subtitle2, - icon = R.drawable.ic_edit, - iconTint = MaterialTheme.typography.subtitle2.color, - space = 6.dp, - ) - } - } - } - - if (showEditDescriptionDialog) { - CustomNoteDialog( - initialDescription = data.metadata.userDescription, - onConfirm = { - onMetadataDescriptionUpdate(data.id(), it?.trim()?.takeIf { it.isNotBlank() }) - showEditDescriptionDialog = false - }, - onDismiss = { showEditDescriptionDialog = false } - ) - } -} - -@Composable -private fun PaymentDestinationView(data: WalletPaymentInfo) { - when (val payment = data.payment) { - is InboundLiquidityOutgoingPayment -> {} - is OnChainOutgoingPayment -> { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_destination_label), icon = R.drawable.ic_chain) { - SelectionContainer { - Text( - text = when (payment) { - is SpliceOutgoingPayment -> payment.address - is ChannelCloseOutgoingPayment -> payment.address - is SpliceCpfpOutgoingPayment -> stringResource(id = R.string.paymentdetails_destination_cpfp_value) - else -> stringResource(id = R.string.utils_unknown) - } - ) - } - } - } - is LightningOutgoingPayment -> { - val lnId = data.metadata.lnurl?.pay?.metadata?.lnid?.takeIf { it.isNotBlank() } - if (lnId != null) { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_destination_label), icon = R.drawable.ic_zap) { - SelectionContainer { - Text(text = lnId) - } - } - } - - val details = payment.details - if (details is LightningOutgoingPayment.Details.Blinded) { - val offer = details.paymentRequest.invoiceRequest.offer - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_destination_label)) { - ContactOrOfferView(offer = offer) - } - } - } - else -> Unit - } -} - -@Composable -private fun PaymentFeeView(payment: WalletPayment) { - val btcUnit = LocalBitcoinUnit.current - when { - payment is LightningOutgoingPayment && (payment.state() == WalletPaymentState.SuccessOffChain) -> { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_fees_label)) { - Text(text = payment.fees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) - } - } - payment is SpliceOutgoingPayment -> { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_fees_label)) { - Text(text = payment.fees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) - } - } - payment is ChannelCloseOutgoingPayment -> { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_fees_label)) { - Text(text = payment.fees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) - } - } - payment is SpliceCpfpOutgoingPayment -> { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_fees_label)) { - Text(text = payment.fees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) - } - } - payment is InboundLiquidityOutgoingPayment -> { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow( - label = stringResource(id = R.string.paymentdetails_liquidity_miner_fee_label), - helpMessage = stringResource(id = R.string.paymentdetails_liquidity_miner_fee_help) - ) { - Text(text = payment.miningFees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) - } - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow( - label = stringResource(id = R.string.paymentdetails_liquidity_service_fee_label), - helpMessage = stringResource(id = R.string.paymentdetails_liquidity_service_fee_help) - ) { - Text(text = payment.lease.fees.serviceFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) - } - } - payment is IncomingPayment -> { - val receivedWithNewChannel = payment.received?.receivedWith?.filterIsInstance() ?: emptyList() - val receivedWithSpliceIn = payment.received?.receivedWith?.filterIsInstance() ?: emptyList() - if ((receivedWithNewChannel + receivedWithSpliceIn).isNotEmpty()) { - val serviceFee = receivedWithNewChannel.map { it.serviceFee }.sum() + receivedWithSpliceIn.map { it.serviceFee }.sum() - val fundingFee = receivedWithNewChannel.map { it.miningFee }.sum() + receivedWithSpliceIn.map { it.miningFee }.sum() - Spacer(modifier = Modifier.height(8.dp)) - if (serviceFee > 0.msat) { - SplashLabelRow( - label = stringResource(id = R.string.paymentdetails_service_fees_label), - helpMessage = stringResource(R.string.paymentdetails_service_fees_desc) - ) { - Text(text = serviceFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW)) - } - Spacer(modifier = Modifier.height(8.dp)) - } - - SplashLabelRow( - label = stringResource(id = R.string.paymentdetails_funding_fees_label), - helpMessage = stringResource(R.string.paymentdetails_funding_fees_desc) - ) { - Text(text = fundingFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.HIDE)) - } - } - } - else -> {} - } -} - -@Composable -private fun InboundLiquidityLeaseDetails(lease: LiquidityAds.Lease) { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_liquidity_lease_duration_label)) { - Text(text = stringResource(id = R.string.paymentdetails_liquidity_lease_duration_value)) - } -} - -@Composable -private fun PaymentErrorView(status: LightningOutgoingPayment.Status.Completed.Failed, failedParts: List) { - val failure = remember(status, failedParts) { OutgoingPaymentFailure(status.reason, failedParts) } - translatePaymentError(failure).let { - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_error_label)) { - Text(text = it) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun CustomNoteDialog( - initialDescription: String?, - onConfirm: (String?) -> Unit, - onDismiss: () -> Unit -) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) - var description by rememberSaveable { mutableStateOf(initialDescription) } - - ModalBottomSheet( - sheetState = sheetState, - onDismissRequest = onDismiss, - containerColor = MaterialTheme.colors.surface, - contentColor = MaterialTheme.colors.onSurface, - scrimColor = MaterialTheme.colors.onBackground.copy(alpha = 0.1f), - ) { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(top = 0.dp, start = 24.dp, end = 24.dp, bottom = 70.dp), - ) { - Text(text = stringResource(id = R.string.paymentdetails_edit_dialog_title), style = MaterialTheme.typography.body2) - Spacer(modifier = Modifier.height(16.dp)) - TextInput( - modifier = Modifier.fillMaxWidth(), - text = description ?: "", - onTextChange = { description = it.takeIf { it.isNotBlank() } }, - minLines = 2, - maxLines = 6, - maxChars = 280, - staticLabel = stringResource(id = R.string.paymentdetails_edit_dialog_input_label) - ) - Spacer(modifier = Modifier.height(24.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Button(onClick = onDismiss, text = stringResource(id = R.string.btn_cancel), shape = CircleShape) - Button( - onClick = { onConfirm(description) }, - text = stringResource(id = R.string.btn_save), - icon = R.drawable.ic_check, - enabled = description != initialDescription, - space = 8.dp, - shape = CircleShape - ) - } - } - } -} - -@Composable -private fun ConfirmationView( - txId: TxId, - channelId: ByteVector32, - isConfirmed: Boolean, - canBeBumped: Boolean, - onCpfpSuccess: () -> Unit, - minDepth: Int? = null, // sometimes we know how many confirmations are needed -) { - val txUrl = txUrl(txId = txId) - val context = LocalContext.current - val electrumClient = business.electrumClient - var showBumpTxDialog by remember { mutableStateOf(false) } - - if (isConfirmed) { - FilledButton( - text = stringResource(id = R.string.paymentdetails_status_confirmed), - icon = R.drawable.ic_chain, - backgroundColor = Color.Transparent, - padding = PaddingValues(8.dp), - textStyle = MaterialTheme.typography.button.copy(fontSize = 14.sp), - iconTint = MaterialTheme.colors.primary, - space = 6.dp, - onClick = { openLink(context, txUrl) } - ) - } else { - - suspend fun getConfirmations(): Int { - val confirmations = electrumClient.getConfirmations(txId) - return confirmations ?: run { - delay(5_000) - getConfirmations() - } - } - - val confirmations by produceState(initialValue = null) { - electrumClient.connectionStatus.filterIsInstance().first() - val confirmations = getConfirmations() - value = confirmations - } - confirmations?.let { conf -> - if (conf == 0) { - Card( - internalPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), - onClick = if (canBeBumped) { - { showBumpTxDialog = true } - } else null, - backgroundColor = Color.Transparent, - horizontalAlignment = Alignment.CenterHorizontally - ) { - TextWithIcon( - text = stringResource(R.string.paymentdetails_status_unconfirmed_zero), - icon = if (canBeBumped) R.drawable.ic_rocket else R.drawable.ic_clock, - textStyle = MaterialTheme.typography.button.copy(fontSize = 14.sp, color = MaterialTheme.colors.primary), - iconTint = MaterialTheme.colors.primary - ) - - if (canBeBumped) { - Text( - text = stringResource(id = R.string.paymentdetails_status_unconfirmed_zero_bump), - style = MaterialTheme.typography.button.copy(fontSize = 14.sp, color = MaterialTheme.colors.primary, fontWeight = FontWeight.Bold), - ) - } - } - } else { - FilledButton( - text = when (minDepth) { - null -> stringResource(R.string.paymentdetails_status_unconfirmed_default, conf) - else -> stringResource(R.string.paymentdetails_status_unconfirmed_with_depth, conf, minDepth) - }, - icon = R.drawable.ic_chain, - onClick = { openLink(context, txUrl) }, - backgroundColor = Color.Transparent, - padding = PaddingValues(8.dp), - textStyle = MaterialTheme.typography.button.copy(fontSize = 14.sp), - iconTint = MaterialTheme.colors.primary, - space = 6.dp, - ) - } - - if (conf == 0 && showBumpTxDialog) { - BumpTransactionDialog(channelId = channelId, onSuccess = onCpfpSuccess, onDismiss = { showBumpTxDialog = false }) - } - } ?: ProgressView( - text = stringResource(id = R.string.paymentdetails_status_unconfirmed_fetching), - textStyle = MaterialTheme.typography.body1.copy(fontSize = 14.sp), - padding = PaddingValues(8.dp), - progressCircleSize = 16.dp, - ) - } -} - -@Composable -private fun BumpTransactionDialog( - channelId: ByteVector32, - onSuccess: () -> Unit, - onDismiss: () -> Unit, -) { - Dialog( - onDismiss = onDismiss, - title = stringResource(id = R.string.cpfp_title), - buttons = null, - ) { - CpfpView(channelId = channelId, onSuccess = onSuccess) - } -} - -@Composable -fun translatePaymentError(paymentFailure: OutgoingPaymentFailure): String { - val context = LocalContext.current - val errorMessage = remember(key1 = paymentFailure) { - when (val result = paymentFailure.explain()) { - is Either.Left -> { - when (val partFailure = result.value) { - is LightningOutgoingPayment.Part.Status.Failure.Uninterpretable -> partFailure.message - LightningOutgoingPayment.Part.Status.Failure.ChannelIsClosing -> context.getString(R.string.outgoing_failuremessage_channel_closing) - LightningOutgoingPayment.Part.Status.Failure.ChannelIsSplicing -> context.getString(R.string.outgoing_failuremessage_channel_splicing) - LightningOutgoingPayment.Part.Status.Failure.NotEnoughFees -> context.getString(R.string.outgoing_failuremessage_not_enough_fee) - LightningOutgoingPayment.Part.Status.Failure.NotEnoughFunds -> context.getString(R.string.outgoing_failuremessage_not_enough_balance) - LightningOutgoingPayment.Part.Status.Failure.PaymentAmountTooBig -> context.getString(R.string.outgoing_failuremessage_too_big) - LightningOutgoingPayment.Part.Status.Failure.PaymentAmountTooSmall -> context.getString(R.string.outgoing_failuremessage_too_small) - LightningOutgoingPayment.Part.Status.Failure.PaymentExpiryTooBig -> context.getString(R.string.outgoing_failuremessage_expiry_too_big) - LightningOutgoingPayment.Part.Status.Failure.RecipientRejectedPayment -> context.getString(R.string.outgoing_failuremessage_rejected_by_recipient) - LightningOutgoingPayment.Part.Status.Failure.RecipientIsOffline -> context.getString(R.string.outgoing_failuremessage_recipient_offline) - LightningOutgoingPayment.Part.Status.Failure.RecipientLiquidityIssue -> context.getString(R.string.outgoing_failuremessage_not_enough_liquidity) - LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure -> context.getString(R.string.outgoing_failuremessage_temporary_failure) - LightningOutgoingPayment.Part.Status.Failure.TooManyPendingPayments -> context.getString(R.string.outgoing_failuremessage_too_many_pending) - } - } - is Either.Right -> { - when (result.value) { - FinalFailure.InvalidPaymentId -> context.getString(R.string.outgoing_failuremessage_invalid_id) - FinalFailure.AlreadyPaid -> context.getString(R.string.outgoing_failuremessage_alreadypaid) - FinalFailure.ChannelClosing -> context.getString(R.string.outgoing_failuremessage_channel_closing) - FinalFailure.ChannelNotConnected -> context.getString(R.string.outgoing_failuremessage_not_connected) - FinalFailure.ChannelOpening -> context.getString(R.string.outgoing_failuremessage_channel_opening) - FinalFailure.FeaturesNotSupported -> context.getString(R.string.outgoing_failuremessage_unsupported_features) - FinalFailure.InsufficientBalance -> context.getString(R.string.outgoing_failuremessage_not_enough_balance) - FinalFailure.InvalidPaymentAmount -> context.getString(R.string.outgoing_failuremessage_invalid_amount) - FinalFailure.NoAvailableChannels -> context.getString(R.string.outgoing_failuremessage_no_available_channels) - FinalFailure.RecipientUnreachable -> context.getString(R.string.outgoing_failuremessage_noroutefound) - FinalFailure.RetryExhausted -> context.getString(R.string.outgoing_failuremessage_noroutefound) - FinalFailure.UnknownError -> context.getString(R.string.outgoing_failuremessage_unknown) - FinalFailure.WalletRestarted -> context.getString(R.string.outgoing_failuremessage_restarted) - } - } - } - } - return errorMessage -} 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 d08bc4d50..9ce0f9f68 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 @@ -42,6 +42,7 @@ import fr.acinq.lightning.utils.currentTimestampMillis import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sum import fr.acinq.lightning.utils.toMilliSatoshi +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.android.LocalBitcoinUnit import fr.acinq.phoenix.android.LocalFiatCurrency import fr.acinq.phoenix.android.R @@ -50,16 +51,22 @@ import fr.acinq.phoenix.android.components.AmountView import fr.acinq.phoenix.android.components.Card import fr.acinq.phoenix.android.components.CardHeader import fr.acinq.phoenix.android.components.Clickable +import fr.acinq.phoenix.android.components.InlineButton import fr.acinq.phoenix.android.components.TextWithIcon import fr.acinq.phoenix.android.components.TransactionLinkButton import fr.acinq.phoenix.android.fiatRate +import fr.acinq.phoenix.android.navController +import fr.acinq.phoenix.android.navigateToPaymentDetails import fr.acinq.phoenix.android.utils.Converter.toAbsoluteDateTimeString import fr.acinq.phoenix.android.utils.Converter.toFiat import fr.acinq.phoenix.android.utils.Converter.toPrettyString import fr.acinq.phoenix.android.utils.MSatDisplayPolicy import fr.acinq.phoenix.android.utils.copyToClipboard import fr.acinq.phoenix.data.ExchangeRate +import fr.acinq.phoenix.data.WalletPaymentId import fr.acinq.phoenix.data.WalletPaymentInfo +import fr.acinq.phoenix.data.walletPaymentId +import fr.acinq.phoenix.utils.extensions.amountFeeCredit @Composable @@ -92,6 +99,7 @@ fun PaymentDetailsTechnicalView( is IncomingPayment.ReceivedWith.LightningPayment -> ReceivedWithLightning(it, rateThen) is IncomingPayment.ReceivedWith.NewChannel -> ReceivedWithNewChannel(it, rateThen) is IncomingPayment.ReceivedWith.SpliceIn -> ReceivedWithSpliceIn(it, rateThen) + is IncomingPayment.ReceivedWith.AddedToFeeCredit -> ReceivedWithFeeCredit(it, rateThen) } } } @@ -180,7 +188,7 @@ private fun HeaderForIncoming( // -- payment type TechnicalRow(label = stringResource(id = R.string.paymentdetails_payment_type_label)) { Text( - when (payment.origin) { + text = when (payment.origin) { is IncomingPayment.Origin.Invoice -> stringResource(R.string.paymentdetails_normal_incoming) is IncomingPayment.Origin.SwapIn -> stringResource(R.string.paymentdetails_swapin) is IncomingPayment.Origin.OnChain -> stringResource(R.string.paymentdetails_swapin) @@ -233,7 +241,7 @@ private fun AmountSection( is InboundLiquidityOutgoingPayment -> { TechnicalRowAmount( label = stringResource(id = R.string.paymentdetails_liquidity_amount_label), - amount = payment.lease.amount.toMilliSatoshi(), + amount = payment.purchase.amount.toMilliSatoshi(), rateThen = rateThen, mSatDisplayPolicy = MSatDisplayPolicy.SHOW ) @@ -245,14 +253,10 @@ private fun AmountSection( ) TechnicalRowAmount( label = stringResource(id = R.string.paymentdetails_liquidity_service_fee_label), - amount = payment.lease.fees.serviceFee.toMilliSatoshi(), + amount = payment.serviceFees.toMilliSatoshi(), rateThen = rateThen, mSatDisplayPolicy = MSatDisplayPolicy.SHOW ) - TechnicalRowSelectable( - label = stringResource(id = R.string.paymentdetails_liquidity_signature_label), - value = payment.lease.sellerSig.toHex(), - ) } is OutgoingPayment -> { TechnicalRowAmount( @@ -275,6 +279,14 @@ private fun AmountSection( rateThen = rateThen, mSatDisplayPolicy = MSatDisplayPolicy.SHOW ) + payment.amountFeeCredit?.let { + TechnicalRowAmount( + label = stringResource(R.string.paymentdetails_amount_fee_credit_label), + amount = it, + rateThen = rateThen, + mSatDisplayPolicy = MSatDisplayPolicy.SHOW + ) + } val receivedWithNewChannel = payment.received?.receivedWith?.filterIsInstance() ?: emptyList() val receivedWithSpliceIn = payment.received?.receivedWith?.filterIsInstance() ?: emptyList() if ((receivedWithNewChannel + receivedWithSpliceIn).isNotEmpty()) { @@ -383,6 +395,37 @@ private fun DetailsForInboundLiquidity( label = stringResource(id = R.string.paymentdetails_channel_id_label), value = payment.channelId.toHex(), ) + TechnicalRow(label = "Purchase Type") { + Text(text = when (payment.purchase) { + is LiquidityAds.Purchase.Standard -> "Standard" + is LiquidityAds.Purchase.WithFeeCredit -> "Fee credit" + }) + } + val details = payment.purchase.paymentDetails + TechnicalRow(label = "Purchase details") { + Text(text = details.paymentType.toString()) + } + when (details) { + is LiquidityAds.PaymentDetails.FromFutureHtlc -> ListLinksOfPaymentHashes(details.paymentHashes) + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> ListLinksOfPaymentHashes(details.paymentHashes) + else -> Unit + } +} + +@Composable +private fun ListLinksOfPaymentHashes(paymentHashes: List) { + val navController = navController + TechnicalRow(label = "Triggered by payments") { + Column { + paymentHashes.forEach { + InlineButton( + text = "- ${it.toHex()}", + onClick = { navigateToPaymentDetails(navController, WalletPaymentId.IncomingPaymentId(it), isFromEvent = false) }, + maxLines = 1, + ) + } + } + } } @Composable @@ -449,7 +492,7 @@ private fun ReceivedWithLightning( Text(text = receivedWith.channelId.toHex()) } } - TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_received_label), amount = receivedWith.amount, rateThen = rateThen) + TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_received_label), amount = receivedWith.amountReceived, rateThen = rateThen) } @Composable @@ -470,7 +513,7 @@ private fun ReceivedWithNewChannel( label = stringResource(id = R.string.paymentdetails_tx_id_label), content = { TransactionLinkButton(txId = receivedWith.txId) } ) - TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_received_label), amount = receivedWith.amount, rateThen = rateThen) + TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_received_label), amount = receivedWith.amountReceived, rateThen = rateThen) } @Composable @@ -491,7 +534,18 @@ private fun ReceivedWithSpliceIn( label = stringResource(id = R.string.paymentdetails_tx_id_label), content = { TransactionLinkButton(txId = receivedWith.txId) } ) - TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_received_label), amount = receivedWith.amount, rateThen = rateThen) + TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_received_label), amount = receivedWith.amountReceived, rateThen = rateThen) +} + +@Composable +private fun ReceivedWithFeeCredit( + receivedWith: IncomingPayment.ReceivedWith.AddedToFeeCredit, + rateThen: ExchangeRate.BitcoinPriceRate? +) { + TechnicalRow(label = stringResource(id = R.string.paymentdetails_received_with_label)) { + Text(text = stringResource(id = R.string.paymentdetails_received_with_fee_credit)) + } + TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_added_to_fee_credit_label), amount = receivedWith.amountReceived, rateThen = rateThen) } @Composable diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsView.kt index d46179c85..ab4bdb395 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentDetailsView.kt @@ -36,6 +36,7 @@ import fr.acinq.phoenix.android.components.Card import fr.acinq.phoenix.android.components.DefaultScreenHeader import fr.acinq.phoenix.android.components.DefaultScreenLayout import fr.acinq.phoenix.android.components.feedback.ErrorMessage +import fr.acinq.phoenix.android.payments.details.splash.PaymentDetailsSplashView import fr.acinq.phoenix.data.WalletPaymentFetchOptions import fr.acinq.phoenix.data.WalletPaymentId import fr.acinq.phoenix.data.WalletPaymentInfo diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt index 575dc9b0b..084074887 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt @@ -181,7 +181,7 @@ private fun PaymentDescription( val metadata = paymentInfo.metadata val peer by business.peerManager.peerState.collectAsState() - val desc = when (paymentInfo.isLegacyMigration(peer)) { + val desc = when (payment.isLegacyMigration(metadata, peer)) { null -> stringResource(id = R.string.paymentdetails_desc_closing_channel) // not sure yet, but we still know it's a closing true -> stringResource(id = R.string.paymentdetails_desc_legacy_migration) false -> metadata.userDescription diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashStatus.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashStatus.kt new file mode 100644 index 000000000..0eb146a6c --- /dev/null +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashStatus.kt @@ -0,0 +1,388 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.phoenix.android.payments.details.splash + +import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.TxId +import fr.acinq.lightning.blockchain.electrum.ElectrumConnectionStatus +import fr.acinq.lightning.db.ChannelCloseOutgoingPayment +import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment +import fr.acinq.lightning.db.IncomingPayment +import fr.acinq.lightning.db.LightningOutgoingPayment +import fr.acinq.lightning.db.SpliceCpfpOutgoingPayment +import fr.acinq.lightning.db.SpliceOutgoingPayment +import fr.acinq.lightning.db.WalletPayment +import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.business +import fr.acinq.phoenix.android.components.Card +import fr.acinq.phoenix.android.components.Dialog +import fr.acinq.phoenix.android.components.FilledButton +import fr.acinq.phoenix.android.components.ProgressView +import fr.acinq.phoenix.android.components.TextWithIcon +import fr.acinq.phoenix.android.components.openLink +import fr.acinq.phoenix.android.components.txUrl +import fr.acinq.phoenix.android.payments.cpfp.CpfpView +import fr.acinq.phoenix.android.utils.Converter.toRelativeDateString +import fr.acinq.phoenix.android.utils.annotatedStringResource +import fr.acinq.phoenix.android.utils.mutedTextColor +import fr.acinq.phoenix.android.utils.negativeColor +import fr.acinq.phoenix.android.utils.positiveColor +import fr.acinq.phoenix.utils.extensions.minDepthForFunding +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlin.math.absoluteValue + +@Composable +fun PaymentStatus( + payment: WalletPayment, + fromEvent: Boolean, + onCpfpSuccess: () -> Unit, +) { + val peerManager = business.peerManager + when (payment) { + is LightningOutgoingPayment -> when (payment.status) { + is LightningOutgoingPayment.Status.Pending -> PaymentStatusIcon( + message = { Text(text = stringResource(id = R.string.paymentdetails_status_sent_pending)) }, + imageResId = R.drawable.ic_payment_details_pending_static, + isAnimated = false, + color = mutedTextColor + ) + is LightningOutgoingPayment.Status.Completed.Failed -> PaymentStatusIcon( + message = { Text(text = annotatedStringResource(id = R.string.paymentdetails_status_sent_failed), textAlign = TextAlign.Center) }, + imageResId = R.drawable.ic_payment_details_failure_static, + isAnimated = false, + color = negativeColor + ) + is LightningOutgoingPayment.Status.Completed.Succeeded -> PaymentStatusIcon( + message = { + Text(text = annotatedStringResource(id = R.string.paymentdetails_status_sent_successful, payment.completedAt?.toRelativeDateString() ?: "")) + }, + imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, + isAnimated = fromEvent, + color = positiveColor, + ) + } + is ChannelCloseOutgoingPayment -> when (payment.confirmedAt) { + null -> { + PaymentStatusIcon( + message = null, + imageResId = R.drawable.ic_payment_details_pending_onchain_static, + isAnimated = false, + color = mutedTextColor, + ) + ConfirmationView(payment.txId, payment.channelId, isConfirmed = false, canBeBumped = false, onCpfpSuccess = onCpfpSuccess) + } + else -> { + PaymentStatusIcon( + message = { + Text(text = annotatedStringResource(id = R.string.paymentdetails_status_channelclose_confirmed, payment.completedAt?.toRelativeDateString() ?: "")) + }, + imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, + isAnimated = fromEvent, + color = positiveColor, + ) + ConfirmationView(payment.txId, payment.channelId, isConfirmed = true, canBeBumped = false, onCpfpSuccess) + } + } + is SpliceOutgoingPayment -> when (payment.confirmedAt) { + null -> { + PaymentStatusIcon( + message = null, + imageResId = R.drawable.ic_payment_details_pending_onchain_static, + isAnimated = false, + color = mutedTextColor, + ) + ConfirmationView(payment.txId, payment.channelId, isConfirmed = false, canBeBumped = true, onCpfpSuccess = onCpfpSuccess) + } + else -> { + PaymentStatusIcon( + message = { + Text(text = annotatedStringResource(id = R.string.paymentdetails_status_sent_successful, payment.completedAt!!.toRelativeDateString())) + }, + imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, + isAnimated = fromEvent, + color = positiveColor, + ) + ConfirmationView(payment.txId, payment.channelId, isConfirmed = true, canBeBumped = true, onCpfpSuccess = onCpfpSuccess) + } + } + is SpliceCpfpOutgoingPayment -> when (payment.confirmedAt) { + null -> { + PaymentStatusIcon( + message = null, + imageResId = R.drawable.ic_payment_details_pending_onchain_static, + isAnimated = false, + color = mutedTextColor, + ) + ConfirmationView(payment.txId, payment.channelId, isConfirmed = false, canBeBumped = true, onCpfpSuccess = onCpfpSuccess) + } + else -> { + PaymentStatusIcon( + message = { + Text(text = annotatedStringResource(id = R.string.paymentdetails_status_sent_successful, payment.completedAt!!.toRelativeDateString())) + }, + imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, + isAnimated = fromEvent, + color = positiveColor, + ) + ConfirmationView(payment.txId, payment.channelId, isConfirmed = true, canBeBumped = true, onCpfpSuccess = onCpfpSuccess) + } + } + is IncomingPayment -> { + val received = payment.received + when { + received == null -> { + PaymentStatusIcon( + message = { Text(text = stringResource(id = R.string.paymentdetails_status_received_pending)) }, + imageResId = R.drawable.ic_payment_details_pending_static, + isAnimated = false, + color = mutedTextColor + ) + } + received.receivedWith.isEmpty() -> { + PaymentStatusIcon( + message = { Text(text = stringResource(id = R.string.paymentdetails_status_received_paytoopen_pending)) }, + isAnimated = false, + imageResId = R.drawable.ic_clock, + color = mutedTextColor, + ) + } + received.receivedWith.any { it is IncomingPayment.ReceivedWith.OnChainIncomingPayment && it.lockedAt == null } -> { + PaymentStatusIcon( + message = { + Text(text = stringResource(id = R.string.paymentdetails_status_unconfirmed)) + }, + isAnimated = false, + imageResId = R.drawable.ic_clock, + color = mutedTextColor, + ) + } + payment.completedAt == null -> { + PaymentStatusIcon( + message = { + Text(text = stringResource(id = R.string.paymentdetails_status_received_pending)) + }, + imageResId = R.drawable.ic_payment_details_pending_static, + isAnimated = false, + color = mutedTextColor + ) + } + else -> { + PaymentStatusIcon( + message = { + Text(text = annotatedStringResource(id = R.string.paymentdetails_status_received_successful, payment.completedAt!!.toRelativeDateString())) + }, + imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, + isAnimated = fromEvent, + color = positiveColor, + ) + } + } + received?.receivedWith?.filterIsInstance()?.firstOrNull()?.let { + val nodeParams = business.nodeParamsManager.nodeParams.value + val channelMinDepth by produceState(initialValue = null, key1 = Unit) { + nodeParams?.let { params -> + val channelId = payment.received?.receivedWith?.filterIsInstance()?.firstOrNull()?.channelId + value = channelId?.let { peerManager.getChannelWithCommitments(it)?.minDepthForFunding(params) } + } + } + ConfirmationView(it.txId, it.channelId, isConfirmed = it.confirmedAt != null, canBeBumped = false, onCpfpSuccess = onCpfpSuccess, channelMinDepth) + } + } + is InboundLiquidityOutgoingPayment -> SplashLiquidityStatus(payment = payment, fromEvent = fromEvent) + } +} + +@OptIn(ExperimentalAnimationGraphicsApi::class) +@Composable +fun PaymentStatusIcon( + message: (@Composable ColumnScope.() -> Unit)?, + isAnimated: Boolean, + imageResId: Int, + color: Color, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + val scope = rememberCoroutineScope() + var atEnd by remember { mutableStateOf(false) } + Image( + painter = if (isAnimated) { + rememberAnimatedVectorPainter(AnimatedImageVector.animatedVectorResource(imageResId), atEnd) + } else { + painterResource(id = imageResId) + }, + contentDescription = null, + colorFilter = ColorFilter.tint(color), + modifier = Modifier.size(80.dp) + ) + if (isAnimated) { + LaunchedEffect(key1 = Unit) { + scope.launch { + delay(150) + atEnd = true + } + } + } + message?.let { + Spacer(Modifier.height(16.dp)) + Column { it() } + } + } + +} + +@Composable +private fun ConfirmationView( + txId: TxId, + channelId: ByteVector32, + isConfirmed: Boolean, + canBeBumped: Boolean, + onCpfpSuccess: () -> Unit, + minDepth: Int? = null, // sometimes we know how many confirmations are needed +) { + val txUrl = txUrl(txId = txId) + val context = LocalContext.current + val electrumClient = business.electrumClient + var showBumpTxDialog by remember { mutableStateOf(false) } + + if (isConfirmed) { + FilledButton( + text = stringResource(id = R.string.paymentdetails_status_confirmed), + icon = R.drawable.ic_chain, + backgroundColor = Color.Transparent, + padding = PaddingValues(8.dp), + textStyle = MaterialTheme.typography.button.copy(fontSize = 14.sp), + iconTint = MaterialTheme.colors.primary, + space = 6.dp, + onClick = { openLink(context, txUrl) } + ) + } else { + + suspend fun getConfirmations(): Int { + val confirmations = electrumClient.getConfirmations(txId) + return confirmations ?: run { + delay(5_000) + getConfirmations() + } + } + + val confirmations by produceState(initialValue = null) { + electrumClient.connectionStatus.filterIsInstance().first() + val confirmations = getConfirmations() + value = confirmations + } + confirmations?.absoluteValue?.let { conf -> + if (conf == 0) { + Card( + internalPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), + onClick = if (canBeBumped) { + { showBumpTxDialog = true } + } else null, + backgroundColor = Color.Transparent, + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextWithIcon( + text = stringResource(R.string.paymentdetails_status_unconfirmed_zero), + icon = if (canBeBumped) R.drawable.ic_rocket else R.drawable.ic_clock, + textStyle = MaterialTheme.typography.button.copy(fontSize = 14.sp, color = MaterialTheme.colors.primary), + iconTint = MaterialTheme.colors.primary + ) + + if (canBeBumped) { + Text( + text = stringResource(id = R.string.paymentdetails_status_unconfirmed_zero_bump), + style = MaterialTheme.typography.button.copy(fontSize = 14.sp, color = MaterialTheme.colors.primary, fontWeight = FontWeight.Bold), + ) + } + } + } else { + FilledButton( + text = when (minDepth) { + null -> stringResource(R.string.paymentdetails_status_unconfirmed_default, conf) + else -> stringResource(R.string.paymentdetails_status_unconfirmed_with_depth, conf, minDepth) + }, + icon = R.drawable.ic_chain, + onClick = { openLink(context, txUrl) }, + backgroundColor = Color.Transparent, + padding = PaddingValues(8.dp), + textStyle = MaterialTheme.typography.button.copy(fontSize = 14.sp), + iconTint = MaterialTheme.colors.primary, + space = 6.dp, + ) + } + + if (conf == 0 && showBumpTxDialog) { + BumpTransactionDialog(channelId = channelId, onSuccess = onCpfpSuccess, onDismiss = { showBumpTxDialog = false }) + } + } ?: ProgressView( + text = stringResource(id = R.string.paymentdetails_status_unconfirmed_fetching), + textStyle = MaterialTheme.typography.body1.copy(fontSize = 14.sp), + padding = PaddingValues(8.dp), + progressCircleSize = 16.dp, + ) + } +} + +@Composable +private fun BumpTransactionDialog( + channelId: ByteVector32, + onSuccess: () -> Unit, + onDismiss: () -> Unit, +) { + Dialog( + onDismiss = onDismiss, + title = stringResource(id = R.string.cpfp_title), + buttons = null, + ) { + CpfpView(channelId = channelId, onSuccess = onSuccess) + } +} \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt new file mode 100644 index 000000000..882ef195e --- /dev/null +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt @@ -0,0 +1,233 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.phoenix.android.payments.details.splash + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import fr.acinq.lightning.db.ChannelCloseOutgoingPayment +import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment +import fr.acinq.lightning.db.IncomingPayment +import fr.acinq.lightning.db.LightningOutgoingPayment +import fr.acinq.lightning.db.OutgoingPayment +import fr.acinq.lightning.db.SpliceCpfpOutgoingPayment +import fr.acinq.lightning.db.SpliceOutgoingPayment +import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.components.AmountView +import fr.acinq.phoenix.android.components.BorderButton +import fr.acinq.phoenix.android.components.Button +import fr.acinq.phoenix.android.components.DefaultScreenHeader +import fr.acinq.phoenix.android.components.PrimarySeparator +import fr.acinq.phoenix.android.components.SplashClickableContent +import fr.acinq.phoenix.android.components.SplashLabelRow +import fr.acinq.phoenix.android.components.SplashLayout +import fr.acinq.phoenix.android.components.TextInput +import fr.acinq.phoenix.android.components.TextWithIcon +import fr.acinq.phoenix.android.utils.borderColor +import fr.acinq.phoenix.android.utils.mutedBgColor +import fr.acinq.phoenix.android.utils.negativeColor +import fr.acinq.phoenix.android.utils.positiveColor +import fr.acinq.phoenix.data.WalletPaymentId +import fr.acinq.phoenix.data.WalletPaymentInfo +import fr.acinq.phoenix.utils.extensions.WalletPaymentState +import fr.acinq.phoenix.utils.extensions.state + +@Composable +fun PaymentDetailsSplashView( + onBackClick: () -> Unit, + data: WalletPaymentInfo, + onDetailsClick: (WalletPaymentId) -> Unit, + onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, + fromEvent: Boolean, +) { + val payment = data.payment + SplashLayout( + header = { DefaultScreenHeader(onBackClick = onBackClick) }, + topContent = { PaymentStatus(data.payment, fromEvent, onCpfpSuccess = onBackClick) } + ) { + AmountView( + amount = when (payment) { + is InboundLiquidityOutgoingPayment -> payment.amount + is OutgoingPayment -> payment.amount - payment.fees + is IncomingPayment -> payment.amount + }, + amountTextStyle = MaterialTheme.typography.body1.copy(fontSize = 30.sp), + separatorSpace = 4.dp, + prefix = stringResource(id = if (payment is OutgoingPayment) R.string.paymentline_prefix_sent else R.string.paymentline_prefix_received) + + ) + + Spacer(modifier = Modifier.height(36.dp)) + PrimarySeparator( + height = 6.dp, + color = when (payment.state()) { + WalletPaymentState.Failure -> negativeColor + WalletPaymentState.SuccessOffChain, WalletPaymentState.SuccessOnChain -> positiveColor + else -> mutedBgColor + } + ) + Spacer(modifier = Modifier.height(36.dp)) + + when (val payment = data.payment) { + is IncomingPayment -> SplashIncoming(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) + is LightningOutgoingPayment -> SplashLightningOutgoing(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) + is ChannelCloseOutgoingPayment -> SplashChannelClose(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) + is SpliceCpfpOutgoingPayment -> SplashSpliceOutCpfp(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) + is SpliceOutgoingPayment -> SplashSpliceOut(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) + is InboundLiquidityOutgoingPayment -> SplashLiquidityPurchase(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) + } + + Spacer(modifier = Modifier.height(48.dp)) + BorderButton( + text = stringResource(id = R.string.paymentdetails_details_button), + borderColor = borderColor, + textStyle = MaterialTheme.typography.caption, + icon = R.drawable.ic_tool, + iconTint = MaterialTheme.typography.caption.color, + onClick = { onDetailsClick(data.id()) }, + ) + } +} + +@Composable +fun SplashDescription( + description: String?, + userDescription: String?, + paymentId: WalletPaymentId, + onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, +) { + var showEditDescriptionDialog by remember { mutableStateOf(false) } + + Spacer(modifier = Modifier.height(8.dp)) + + if (!(description.isNullOrBlank() && !userDescription.isNullOrBlank())) { + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_desc_label)) { + if (description.isNullOrBlank()) { + Text( + text = stringResource(id = R.string.paymentdetails_no_description), + style = MaterialTheme.typography.caption.copy(fontStyle = FontStyle.Italic) + ) + } else { + Text(text = description) + } + } + } + + SplashLabelRow(label = if (userDescription.isNullOrBlank()) "" else "Note") { + SplashClickableContent(onClick = { showEditDescriptionDialog = true }) { + if (!userDescription.isNullOrBlank()) { + Text(text = userDescription) + Spacer(modifier = Modifier.height(8.dp)) + } + TextWithIcon( + text = stringResource( + id = when (userDescription) { + null -> R.string.paymentdetails_attach_desc_button + else -> R.string.paymentdetails_edit_desc_button + } + ), + textStyle = MaterialTheme.typography.subtitle2, + icon = R.drawable.ic_edit, + iconTint = MaterialTheme.typography.subtitle2.color, + space = 6.dp, + ) + } + } + + if (showEditDescriptionDialog) { + CustomNoteDialog( + initialDescription = userDescription, + onConfirm = { + onMetadataDescriptionUpdate(paymentId, it?.trim()?.takeIf { it.isNotBlank() }) + showEditDescriptionDialog = false + }, + onDismiss = { showEditDescriptionDialog = false } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CustomNoteDialog( + initialDescription: String?, + onConfirm: (String?) -> Unit, + onDismiss: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) + var description by rememberSaveable { mutableStateOf(initialDescription) } + + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = onDismiss, + containerColor = MaterialTheme.colors.surface, + contentColor = MaterialTheme.colors.onSurface, + scrimColor = MaterialTheme.colors.onBackground.copy(alpha = 0.1f), + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(top = 0.dp, start = 24.dp, end = 24.dp, bottom = 70.dp), + ) { + Text(text = stringResource(id = R.string.paymentdetails_edit_dialog_title), style = MaterialTheme.typography.body2) + Spacer(modifier = Modifier.height(16.dp)) + TextInput( + modifier = Modifier.fillMaxWidth(), + text = description ?: "", + onTextChange = { description = it.takeIf { it.isNotBlank() } }, + minLines = 2, + maxLines = 6, + maxChars = 280, + staticLabel = stringResource(id = R.string.paymentdetails_edit_dialog_input_label) + ) + Spacer(modifier = Modifier.height(24.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + Button(onClick = onDismiss, text = stringResource(id = R.string.btn_cancel), shape = CircleShape) + Button( + onClick = { onConfirm(description) }, + text = stringResource(id = R.string.btn_save), + icon = R.drawable.ic_check, + enabled = description != initialDescription, + space = 8.dp, + shape = CircleShape + ) + } + } + } +} diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt new file mode 100644 index 000000000..8c9cd8a25 --- /dev/null +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.phoenix.android.payments.details.splash + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import fr.acinq.lightning.db.ChannelCloseOutgoingPayment +import fr.acinq.phoenix.android.LocalBitcoinUnit +import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.business +import fr.acinq.phoenix.android.components.SplashLabelRow +import fr.acinq.phoenix.android.utils.Converter.toPrettyString +import fr.acinq.phoenix.android.utils.MSatDisplayPolicy +import fr.acinq.phoenix.android.utils.isLegacyMigration +import fr.acinq.phoenix.android.utils.smartDescription +import fr.acinq.phoenix.data.WalletPaymentId +import fr.acinq.phoenix.data.WalletPaymentMetadata +import fr.acinq.phoenix.data.walletPaymentId + +@Composable +fun SplashChannelClose( + payment: ChannelCloseOutgoingPayment, + metadata: WalletPaymentMetadata, + onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, +) { + val context = LocalContext.current + val peer by business.peerManager.peerState.collectAsState() + + val isLegacyMigration = payment.isLegacyMigration(metadata, peer) + val description = when (isLegacyMigration) { + null -> stringResource(id = R.string.paymentdetails_desc_closing_channel) // not sure yet, but we still know it's a closing + true -> stringResource(id = R.string.paymentdetails_desc_legacy_migration) + false -> payment.smartDescription(context) + } + + SplashDescription( + description = description, + userDescription = metadata.userDescription, + paymentId = payment.walletPaymentId(), + onMetadataDescriptionUpdate = onMetadataDescriptionUpdate + ) + SplashDestination(payment, metadata) + SplashFee(payment = payment) +} + +@Composable +private fun SplashDestination(payment: ChannelCloseOutgoingPayment, metadata: WalletPaymentMetadata) { + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_destination_label), icon = R.drawable.ic_chain) { + SelectionContainer { + Text(text = payment.address) + } + } +} + +@Composable +private fun SplashFee(payment: ChannelCloseOutgoingPayment) { + val btcUnit = LocalBitcoinUnit.current + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_fees_label)) { + Text(text = payment.fees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + } +} \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt new file mode 100644 index 000000000..2bd160c65 --- /dev/null +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.phoenix.android.payments.details.splash + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import fr.acinq.bitcoin.PublicKey +import fr.acinq.lightning.db.IncomingPayment +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sum +import fr.acinq.phoenix.android.LocalBitcoinUnit +import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.business +import fr.acinq.phoenix.android.components.SplashLabelRow +import fr.acinq.phoenix.android.components.contact.ContactCompactView +import fr.acinq.phoenix.android.components.contact.OfferContactState +import fr.acinq.phoenix.android.utils.Converter.toPrettyString +import fr.acinq.phoenix.android.utils.MSatDisplayPolicy +import fr.acinq.phoenix.android.utils.smartDescription +import fr.acinq.phoenix.data.WalletPaymentId +import fr.acinq.phoenix.data.WalletPaymentMetadata +import fr.acinq.phoenix.data.walletPaymentId +import fr.acinq.phoenix.utils.extensions.incomingOfferMetadata + +@Composable +fun SplashIncoming( + payment: IncomingPayment, + metadata: WalletPaymentMetadata, + onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, +) { + val context = LocalContext.current + + payment.incomingOfferMetadata()?.let { meta -> + meta.payerNote?.takeIf { it.isNotBlank() }?.let { + OfferPayerNote(payerNote = it) + Spacer(modifier = Modifier.height(8.dp)) + } + OfferSentBy(payerPubkey = meta.payerKey, !meta.payerNote.isNullOrBlank()) + } + + SplashDescription( + description = payment.smartDescription(context = context), + userDescription = metadata.userDescription, + paymentId = payment.walletPaymentId(), + onMetadataDescriptionUpdate = onMetadataDescriptionUpdate, + ) + SplashFee(payment) +} + +@Composable +fun OfferPayerNote(payerNote: String) { + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_offer_note_label)) { + Text(text = payerNote) + } +} + +@Composable +private fun OfferSentBy(payerPubkey: PublicKey?, hasPayerNote: Boolean) { + val contactsManager = business.contactsManager + val contactState = remember { mutableStateOf(OfferContactState.Init) } + LaunchedEffect(Unit) { + contactState.value = payerPubkey?.let { + contactsManager.getContactForPayerPubkey(it) + }?.let { OfferContactState.Found(it) } ?: OfferContactState.NotFound + } + + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_offer_sender_label)) { + when (val res = contactState.value) { + is OfferContactState.Init -> Text(text = stringResource(id = R.string.utils_loading_data)) + is OfferContactState.NotFound -> { + Text(text = stringResource(id = R.string.paymentdetails_offer_sender_unknown)) + if (hasPayerNote) { + Spacer(modifier = Modifier.height(4.dp)) + Text(text = stringResource(id = R.string.paymentdetails_offer_sender_unknown_details), style = MaterialTheme.typography.subtitle2) + } + } + is OfferContactState.Found -> { + ContactCompactView( + contact = res.contact, + currentOffer = null, + onContactChange = { contactState.value = if (it == null) OfferContactState.NotFound else OfferContactState.Found(it) }, + ) + } + } + } +} + +@Composable +private fun SplashFee( + payment: IncomingPayment +) { + val btcUnit = LocalBitcoinUnit.current + val receivedWithNewChannel = payment.received?.receivedWith?.filterIsInstance() ?: emptyList() + val receivedWithSpliceIn = payment.received?.receivedWith?.filterIsInstance() ?: emptyList() + if ((receivedWithNewChannel + receivedWithSpliceIn).isNotEmpty()) { + val serviceFee = receivedWithNewChannel.map { it.serviceFee }.sum() + receivedWithSpliceIn.map { it.serviceFee }.sum() + val fundingFee = receivedWithNewChannel.map { it.miningFee }.sum() + receivedWithSpliceIn.map { it.miningFee }.sum() + Spacer(modifier = Modifier.height(8.dp)) + if (serviceFee > 0.msat) { + SplashLabelRow( + label = stringResource(id = R.string.paymentdetails_service_fees_label), + helpMessage = stringResource(R.string.paymentdetails_service_fees_desc) + ) { + Text(text = serviceFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW)) + } + Spacer(modifier = Modifier.height(8.dp)) + } + + SplashLabelRow( + label = stringResource(id = R.string.paymentdetails_funding_fees_label), + helpMessage = stringResource(R.string.paymentdetails_funding_fees_desc) + ) { + Text(text = fundingFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.HIDE)) + } + } +} \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt new file mode 100644 index 000000000..cde352f3c --- /dev/null +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.phoenix.android.payments.details.splash + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import fr.acinq.bitcoin.utils.Either +import fr.acinq.lightning.db.LightningOutgoingPayment +import fr.acinq.lightning.payment.FinalFailure +import fr.acinq.lightning.payment.OutgoingPaymentFailure +import fr.acinq.phoenix.android.LocalBitcoinUnit +import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.components.ProgressView +import fr.acinq.phoenix.android.components.SplashLabelRow +import fr.acinq.phoenix.android.components.WebLink +import fr.acinq.phoenix.android.components.contact.ContactOrOfferView +import fr.acinq.phoenix.android.utils.Converter.toPrettyString +import fr.acinq.phoenix.android.utils.MSatDisplayPolicy +import fr.acinq.phoenix.android.utils.smartDescription +import fr.acinq.phoenix.data.LnurlPayMetadata +import fr.acinq.phoenix.data.WalletPaymentId +import fr.acinq.phoenix.data.WalletPaymentMetadata +import fr.acinq.phoenix.data.lnurl.LnurlPay +import fr.acinq.phoenix.data.walletPaymentId +import fr.acinq.phoenix.utils.extensions.WalletPaymentState +import fr.acinq.phoenix.utils.extensions.outgoingInvoiceRequest +import fr.acinq.phoenix.utils.extensions.state +import io.ktor.http.Url +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +@Composable +fun SplashLightningOutgoing( + payment: LightningOutgoingPayment, + metadata: WalletPaymentMetadata, + onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, +) { + val context = LocalContext.current + + metadata.lnurl?.let { lnurlMeta -> + LnurlPayInfoView(payment, lnurlMeta) + } + + payment.outgoingInvoiceRequest()?.payerNote?.takeIf { it.isNotBlank() }?.let { + OfferPayerNote(payerNote = it) + } + + SplashDescription( + description = payment.smartDescription(context), + userDescription = metadata.userDescription, + paymentId = payment.walletPaymentId(), + onMetadataDescriptionUpdate = onMetadataDescriptionUpdate + ) + SplashDestination(payment, metadata) + SplashFee(payment = payment) + + (payment.status as? LightningOutgoingPayment.Status.Completed.Failed)?.let { status -> + PaymentErrorView(status = status, failedParts = payment.parts.map { it.status }.filterIsInstance()) + } +} + +@Composable +private fun SplashDestination(payment: LightningOutgoingPayment, metadata: WalletPaymentMetadata) { + val lnId = metadata.lnurl?.pay?.metadata?.lnid?.takeIf { it.isNotBlank() } + if (lnId != null) { + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_destination_label), icon = R.drawable.ic_zap) { + SelectionContainer { + Text(text = lnId) + } + } + } + val details = payment.details + if (details is LightningOutgoingPayment.Details.Blinded) { + val offer = details.paymentRequest.invoiceRequest.offer + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_destination_label)) { + ContactOrOfferView(offer = offer) + } + } +} + +@Composable +private fun SplashFee(payment: LightningOutgoingPayment) { + val btcUnit = LocalBitcoinUnit.current + if (payment.state() == WalletPaymentState.SuccessOffChain) { + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_fees_label)) { + Text(text = payment.fees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + } + } +} + +@Composable +private fun LnurlPayInfoView(payment: LightningOutgoingPayment, metadata: LnurlPayMetadata) { + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_lnurlpay_service)) { + SelectionContainer { + Text(text = metadata.pay.callback.host) + } + } + metadata.successAction?.let { + LnurlSuccessAction(payment = payment, action = it) + } +} + +@Composable +private fun LnurlSuccessAction(payment: LightningOutgoingPayment, action: LnurlPay.Invoice.SuccessAction) { + Spacer(modifier = Modifier.height(8.dp)) + when (action) { + is LnurlPay.Invoice.SuccessAction.Message -> { + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_lnurlpay_action_message_label)) { + SelectionContainer { + Text(text = action.message) + } + } + } + is LnurlPay.Invoice.SuccessAction.Url -> { + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_lnurlpay_action_url_label)) { + Text(text = action.description) + WebLink(text = stringResource(id = R.string.paymentdetails_lnurlpay_action_url_button), url = action.url.toString()) + } + } + is LnurlPay.Invoice.SuccessAction.Aes -> { + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_lnurlpay_action_aes_label)) { + val status = payment.status + if (status is LightningOutgoingPayment.Status.Completed.Succeeded.OffChain) { + val deciphered by produceState(initialValue = null) { + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(status.preimage.toByteArray(), "AES"), IvParameterSpec(action.iv.toByteArray())) + value = String(cipher.doFinal(action.ciphertext.toByteArray()), Charsets.UTF_8) + } + Text(text = action.description) + when (deciphered) { + null -> ProgressView(text = stringResource(id = R.string.paymentdetails_lnurlpay_action_aes_decrypting), padding = PaddingValues(0.dp)) + else -> { + val url = try { + Url(deciphered!!) + } catch (e: Exception) { + null + } + if (url != null) { + WebLink(text = stringResource(id = R.string.paymentdetails_lnurlpay_action_url_button), url = url.toString()) + } else { + SelectionContainer { + Text(text = deciphered!!) + } + } + } + } + } else { + Text(text = stringResource(id = R.string.paymentdetails_lnurlpay_action_aes_decrypting)) + } + } + } + } +} + +@Composable +private fun PaymentErrorView(status: LightningOutgoingPayment.Status.Completed.Failed, failedParts: List) { + val failure = remember(status, failedParts) { OutgoingPaymentFailure(status.reason, failedParts) } + translatePaymentError(failure).let { + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_error_label)) { + Text(text = it) + } + } +} + +@Composable +fun translatePaymentError(paymentFailure: OutgoingPaymentFailure): String { + val context = LocalContext.current + val errorMessage = remember(key1 = paymentFailure) { + when (val result = paymentFailure.explain()) { + is Either.Left -> { + when (val partFailure = result.value) { + is LightningOutgoingPayment.Part.Status.Failure.Uninterpretable -> partFailure.message + LightningOutgoingPayment.Part.Status.Failure.ChannelIsClosing -> context.getString(R.string.outgoing_failuremessage_channel_closing) + LightningOutgoingPayment.Part.Status.Failure.ChannelIsSplicing -> context.getString(R.string.outgoing_failuremessage_channel_splicing) + LightningOutgoingPayment.Part.Status.Failure.NotEnoughFees -> context.getString(R.string.outgoing_failuremessage_not_enough_fee) + LightningOutgoingPayment.Part.Status.Failure.NotEnoughFunds -> context.getString(R.string.outgoing_failuremessage_not_enough_balance) + LightningOutgoingPayment.Part.Status.Failure.PaymentAmountTooBig -> context.getString(R.string.outgoing_failuremessage_too_big) + LightningOutgoingPayment.Part.Status.Failure.PaymentAmountTooSmall -> context.getString(R.string.outgoing_failuremessage_too_small) + LightningOutgoingPayment.Part.Status.Failure.PaymentExpiryTooBig -> context.getString(R.string.outgoing_failuremessage_expiry_too_big) + LightningOutgoingPayment.Part.Status.Failure.RecipientRejectedPayment -> context.getString(R.string.outgoing_failuremessage_rejected_by_recipient) + LightningOutgoingPayment.Part.Status.Failure.RecipientIsOffline -> context.getString(R.string.outgoing_failuremessage_recipient_offline) + LightningOutgoingPayment.Part.Status.Failure.RecipientLiquidityIssue -> context.getString(R.string.outgoing_failuremessage_not_enough_liquidity) + LightningOutgoingPayment.Part.Status.Failure.TemporaryRemoteFailure -> context.getString(R.string.outgoing_failuremessage_temporary_failure) + LightningOutgoingPayment.Part.Status.Failure.TooManyPendingPayments -> context.getString(R.string.outgoing_failuremessage_too_many_pending) + } + } + is Either.Right -> { + when (result.value) { + FinalFailure.InvalidPaymentId -> context.getString(R.string.outgoing_failuremessage_invalid_id) + FinalFailure.AlreadyPaid -> context.getString(R.string.outgoing_failuremessage_alreadypaid) + FinalFailure.ChannelClosing -> context.getString(R.string.outgoing_failuremessage_channel_closing) + FinalFailure.ChannelNotConnected -> context.getString(R.string.outgoing_failuremessage_not_connected) + FinalFailure.ChannelOpening -> context.getString(R.string.outgoing_failuremessage_channel_opening) + FinalFailure.FeaturesNotSupported -> context.getString(R.string.outgoing_failuremessage_unsupported_features) + FinalFailure.InsufficientBalance -> context.getString(R.string.outgoing_failuremessage_not_enough_balance) + FinalFailure.InvalidPaymentAmount -> context.getString(R.string.outgoing_failuremessage_invalid_amount) + FinalFailure.NoAvailableChannels -> context.getString(R.string.outgoing_failuremessage_no_available_channels) + FinalFailure.RecipientUnreachable -> context.getString(R.string.outgoing_failuremessage_noroutefound) + FinalFailure.RetryExhausted -> context.getString(R.string.outgoing_failuremessage_noroutefound) + FinalFailure.UnknownError -> context.getString(R.string.outgoing_failuremessage_unknown) + FinalFailure.WalletRestarted -> context.getString(R.string.outgoing_failuremessage_restarted) + } + } + } + } + return errorMessage +} \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt new file mode 100644 index 000000000..c882b9a30 --- /dev/null +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt @@ -0,0 +1,267 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.phoenix.android.payments.details.splash + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment +import fr.acinq.lightning.wire.LiquidityAds +import fr.acinq.phoenix.android.LocalBitcoinUnit +import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.Screen +import fr.acinq.phoenix.android.business +import fr.acinq.phoenix.android.components.BorderButton +import fr.acinq.phoenix.android.components.BottomSheetDialog +import fr.acinq.phoenix.android.components.Clickable +import fr.acinq.phoenix.android.components.SplashLabelRow +import fr.acinq.phoenix.android.components.TextWithIcon +import fr.acinq.phoenix.android.navController +import fr.acinq.phoenix.android.navigateToPaymentDetails +import fr.acinq.phoenix.android.payments.details.PaymentLine +import fr.acinq.phoenix.android.payments.details.PaymentLineLoading +import fr.acinq.phoenix.android.utils.Converter.toPrettyString +import fr.acinq.phoenix.android.utils.Converter.toRelativeDateString +import fr.acinq.phoenix.android.utils.MSatDisplayPolicy +import fr.acinq.phoenix.android.utils.annotatedStringResource +import fr.acinq.phoenix.android.utils.mutedTextColor +import fr.acinq.phoenix.android.utils.positiveColor +import fr.acinq.phoenix.data.WalletPaymentFetchOptions +import fr.acinq.phoenix.data.WalletPaymentId +import fr.acinq.phoenix.data.WalletPaymentInfo +import fr.acinq.phoenix.data.WalletPaymentMetadata + +@Composable +fun SplashLiquidityPurchase( + payment: InboundLiquidityOutgoingPayment, + metadata: WalletPaymentMetadata, + onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, +) { + SplashPurchase(purchase = payment.purchase) + Spacer(modifier = Modifier.height(12.dp)) + SplashFee(payment = payment) + + // FIXME: dangerous!! + // In general, FromChannelBalance only happens for manual purchases OR automated swap-ins with additional liquidity. + // However, swap-ins do not **yet** request additional liquidity, so **for now** we can make a safe approximation. + // Eventually, once swap-ins are upgraded to request liquidity, this will have to be fixed, . + if (payment.purchase.paymentDetails !is LiquidityAds.PaymentDetails.FromChannelBalance) { + AutoLiquidityDetails(purchase = payment.purchase) + } +} + +@Composable +fun SplashLiquidityStatus(payment: InboundLiquidityOutgoingPayment, fromEvent: Boolean) { + when (val lockedAt = payment.lockedAt) { + null -> { + PaymentStatusIcon( + message = null, + imageResId = R.drawable.ic_payment_details_pending_onchain_static, + isAnimated = false, + color = mutedTextColor, + ) + } + else -> { + PaymentStatusIcon( + message = { + if (payment.purchase.paymentDetails is LiquidityAds.PaymentDetails.FromChannelBalance) { + Text(text = annotatedStringResource(id = R.string.paymentdetails_status_inbound_liquidity_success, lockedAt.toRelativeDateString())) + } else { + Text(text = annotatedStringResource(id = R.string.paymentdetails_status_inbound_liquidity_auto_success, lockedAt.toRelativeDateString())) + } + }, + imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, + isAnimated = fromEvent, + color = positiveColor, + ) + } + } +} + + +@Composable +private fun SplashFee( + payment: InboundLiquidityOutgoingPayment +) { + val btcUnit = LocalBitcoinUnit.current + SplashLabelRow( + label = stringResource(id = R.string.paymentdetails_liquidity_miner_fee_label), + helpMessage = stringResource(id = R.string.paymentdetails_liquidity_miner_fee_help) + ) { + Text(text = payment.miningFees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + } + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow( + label = stringResource(id = R.string.paymentdetails_liquidity_service_fee_label), + helpMessage = stringResource(id = R.string.paymentdetails_liquidity_service_fee_help) + ) { + Text(text = payment.serviceFees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + if (payment.purchase is LiquidityAds.Purchase.WithFeeCredit) { + Text(text = "Paid with fee credit") + } + } +} + +@Composable +private fun SplashPurchase( + purchase: LiquidityAds.Purchase +) { + val btcUnit = LocalBitcoinUnit.current + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = "Liquidity") { + Text(text = purchase.amount.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun AutoLiquidityDetails( + purchase: LiquidityAds.Purchase +) { + val navController = navController + var showPaymentsDialog by remember { mutableStateOf(false) } + + Spacer(modifier = Modifier.height(32.dp)) + BorderButton( + text = "What is this?", + icon = R.drawable.ic_help_circle, + onClick = { showPaymentsDialog = true }, + maxLines = 1, + ) + + if (showPaymentsDialog) { + BottomSheetDialog(onDismiss = { showPaymentsDialog = false }, modifier = Modifier.fillMaxHeight(.6f), internalPadding = PaddingValues(bottom = 32.dp)) { + val pagerState = rememberPagerState(pageCount = { 2 }) + HorizontalPager( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + state = pagerState, + verticalAlignment = Alignment.Top, + beyondBoundsPageCount = 1 + ) { index -> + when (index) { + 0 -> { + Column { + Text( + text = "Why did this payment happen?", + style = MaterialTheme.typography.h4, + modifier = Modifier.padding(horizontal = 24.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Your Lightning channel had to be resized, which is an on-chain operation incurring fees.", modifier = Modifier.padding(horizontal = 24.dp)) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "This operation was necessary to accommodate new incoming payments.", modifier = Modifier.padding(horizontal = 24.dp)) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = "Swipe right to see these payments.", + style = MaterialTheme.typography.caption.copy(fontSize = 14.sp), + modifier = Modifier.padding(horizontal = 24.dp) + ) + + Spacer(modifier = Modifier.height(32.dp)) + Text( + text = "How to optimise channels resizing?", + style = MaterialTheme.typography.h4, + modifier = Modifier.padding(horizontal = 24.dp), + ) + Spacer(modifier = Modifier.height(4.dp)) + Clickable(onClick = { navController.navigate(Screen.LiquidityPolicy.route) }, modifier = Modifier.padding(horizontal = 12.dp), shape = RoundedCornerShape(10.dp)) { + Column(modifier = Modifier + .fillMaxWidth() + .padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally) { + TextWithIcon( + text = "Configure automated management", + icon = R.drawable.ic_settings, + ) + Spacer(modifier = Modifier.height(2.dp)) + Text(text = "Cap fees, or disable them altogether", style = MaterialTheme.typography.subtitle2) + } + } + Clickable(onClick = { navController.navigate(Screen.LiquidityRequest.route) }, modifier = Modifier.padding(horizontal = 12.dp), shape = RoundedCornerShape(10.dp)) { + Column(modifier = Modifier + .fillMaxWidth() + .padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally) { + TextWithIcon( + text = "Purchase liquidity in advance", + icon = R.drawable.ic_idea, + ) + Spacer(modifier = Modifier.height(2.dp)) + Text(text = "Requires some planning, but is most optimal", style = MaterialTheme.typography.subtitle2) + } + } + } + } + 1 -> { + Column(modifier = Modifier.fillMaxSize()) { + Text(text = "Operation triggered by...", style = MaterialTheme.typography.h4, modifier = Modifier.padding(horizontal = 16.dp)) + Spacer(modifier = Modifier.height(8.dp)) + val paymentHashes = when (val details = purchase.paymentDetails) { + is LiquidityAds.PaymentDetails.FromFutureHtlc -> details.paymentHashes + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> details.paymentHashes + else -> emptyList() + } + TriggeredBy(paymentHashes = paymentHashes) + } + } + } + } + } + } +} + +@Composable +private fun TriggeredBy(paymentHashes: List) { + val context = LocalContext.current + val navController = navController + val paymentsManager = business.paymentsManager + paymentHashes.forEach { paymentHash -> + val id = remember(paymentHash) { WalletPaymentId.IncomingPaymentId(paymentHash) } + val paymentInfo by produceState(initialValue = null) { + value = paymentsManager.getPayment(id = id, options = WalletPaymentFetchOptions.None) + } + + paymentInfo?.let { + PaymentLine(paymentInfo = it, contactInfo = null, onPaymentClick = { navigateToPaymentDetails(navController, id, isFromEvent = false) }) + } ?: PaymentLineLoading(paymentId = id, onPaymentClick = { navigateToPaymentDetails(navController, id, isFromEvent = false) }) + } +} diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOut.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOut.kt new file mode 100644 index 000000000..8fd05bfb4 --- /dev/null +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOut.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.phoenix.android.payments.details.splash + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import fr.acinq.lightning.db.SpliceOutgoingPayment +import fr.acinq.phoenix.android.LocalBitcoinUnit +import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.components.SplashLabelRow +import fr.acinq.phoenix.android.utils.Converter.toPrettyString +import fr.acinq.phoenix.android.utils.MSatDisplayPolicy +import fr.acinq.phoenix.android.utils.smartDescription +import fr.acinq.phoenix.data.WalletPaymentId +import fr.acinq.phoenix.data.WalletPaymentMetadata +import fr.acinq.phoenix.data.walletPaymentId + +@Composable +fun SplashSpliceOut( + payment: SpliceOutgoingPayment, + metadata: WalletPaymentMetadata, + onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, +) { + val context = LocalContext.current + SplashDescription( + description = payment.smartDescription(context), + userDescription = metadata.userDescription, + paymentId = payment.walletPaymentId(), + onMetadataDescriptionUpdate = onMetadataDescriptionUpdate + ) + SplashDestination(payment) + SplashFee(payment = payment) +} + +@Composable +private fun SplashDestination(payment: SpliceOutgoingPayment) { + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_destination_label), icon = R.drawable.ic_chain) { + SelectionContainer { + Text(text = payment.address) + } + } +} + +@Composable +private fun SplashFee(payment: SpliceOutgoingPayment) { + val btcUnit = LocalBitcoinUnit.current + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_fees_label)) { + Text(text = payment.fees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + } +} diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOutCpfp.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOutCpfp.kt new file mode 100644 index 000000000..237047044 --- /dev/null +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOutCpfp.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.phoenix.android.payments.details.splash + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import fr.acinq.lightning.db.SpliceCpfpOutgoingPayment +import fr.acinq.phoenix.android.LocalBitcoinUnit +import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.components.SplashLabelRow +import fr.acinq.phoenix.android.utils.Converter.toPrettyString +import fr.acinq.phoenix.android.utils.MSatDisplayPolicy +import fr.acinq.phoenix.android.utils.smartDescription +import fr.acinq.phoenix.data.WalletPaymentId +import fr.acinq.phoenix.data.WalletPaymentMetadata +import fr.acinq.phoenix.data.walletPaymentId + +@Composable +fun SplashSpliceOutCpfp( + payment: SpliceCpfpOutgoingPayment, + metadata: WalletPaymentMetadata, + onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, +) { + val context = LocalContext.current + SplashDescription( + description = payment.smartDescription(context), + userDescription = metadata.userDescription, + paymentId = payment.walletPaymentId(), + onMetadataDescriptionUpdate = onMetadataDescriptionUpdate + ) + SplashDestination() + SplashFee(payment = payment) +} + +@Composable +private fun SplashDestination() { + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_destination_label), icon = R.drawable.ic_chain) { + SelectionContainer { + Text(text = stringResource(id = R.string.paymentdetails_destination_cpfp_value)) + } + } +} + +@Composable +private fun SplashFee(payment: SpliceCpfpOutgoingPayment) { + val btcUnit = LocalBitcoinUnit.current + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_fees_label)) { + Text(text = payment.fees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + } +} diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt index b02dc3c1f..51599da92 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt @@ -237,7 +237,7 @@ private fun RequestLiquidityBottomSection( ErrorMessage(header = stringResource(id = R.string.validation_invalid_amount)) } else { ReviewLiquidityRequest( - onConfirm = { vm.requestInboundLiquidity(amount = state.amount, feerate = state.actualFeerate) } + onConfirm = { vm.requestInboundLiquidity(amount = state.amount, feerate = state.actualFeerate, fundingRate = state.fundingRate) } ) } } @@ -245,7 +245,7 @@ private fun RequestLiquidityBottomSection( ProgressView(text = stringResource(id = R.string.liquidityads_requesting_spinner)) } is RequestLiquidityState.Complete.Success -> { - LeaseSuccessDetails(liquidityDetails = state.response) + LiquiditySuccessDetails(liquidityDetails = state.response) } is RequestLiquidityState.Error.NoChannelsAvailable -> { ErrorMessage( @@ -253,6 +253,12 @@ private fun RequestLiquidityBottomSection( details = stringResource(id = R.string.liquidityads_error_channels_unavailable) ) } + is RequestLiquidityState.Error.InvalidFundingAmount -> { + ErrorMessage( + header = stringResource(id = R.string.liquidityads_error_header), + details = "Invalid amount requested. Please try again." + ) + } is RequestLiquidityState.Error.Thrown -> { ErrorMessage( header = stringResource(id = R.string.liquidityads_error_header), @@ -375,10 +381,10 @@ private fun ReviewLiquidityRequest( } @Composable -private fun LeaseSuccessDetails(liquidityDetails: ChannelCommand.Commitment.Splice.Response.Created) { +private fun LiquiditySuccessDetails(liquidityDetails: ChannelCommand.Commitment.Splice.Response.Created) { SuccessMessage( header = stringResource(id = R.string.liquidityads_success), - details = liquidityDetails.liquidityLease?.amount?.let { + details = liquidityDetails.liquidityPurchase?.amount?.let { stringResource(id = R.string.liquidityads_success_amount, it.toPrettyString(unit = LocalBitcoinUnit.current, withUnit = true)) }, alignment = Alignment.CenterHorizontally, diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityViewModel.kt index 175f83ef9..0cb7a4607 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityViewModel.kt @@ -24,8 +24,8 @@ import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelCommand import fr.acinq.lightning.channel.ChannelManagementFees +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.managers.AppConfigurationManager -import fr.acinq.phoenix.managers.NodeParamsManager import fr.acinq.phoenix.managers.PeerManager import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers @@ -38,7 +38,7 @@ import org.slf4j.LoggerFactory sealed class RequestLiquidityState { object Init: RequestLiquidityState() object Estimating: RequestLiquidityState() - data class Estimation(val amount: Satoshi, val fees: ChannelManagementFees, val actualFeerate: FeeratePerKw): RequestLiquidityState() + data class Estimation(val amount: Satoshi, val fees: ChannelManagementFees, val actualFeerate: FeeratePerKw, val fundingRate: LiquidityAds.FundingRate): RequestLiquidityState() object Requesting: RequestLiquidityState() sealed class Complete: RequestLiquidityState() { abstract val response: ChannelCommand.Commitment.Splice.Response @@ -47,7 +47,8 @@ sealed class RequestLiquidityState { } sealed class Error: RequestLiquidityState() { data class Thrown(val cause: Throwable): Error() - object NoChannelsAvailable: Error() + data object NoChannelsAvailable: Error() + data object InvalidFundingAmount: Error() } } @@ -65,23 +66,29 @@ class RequestLiquidityViewModel(val peerManager: PeerManager, val appConfigManag }) { val peer = peerManager.getPeer() val feerate = appConfigManager.mempoolFeerate.filterNotNull().first().hour + val fundingRate = peer.remoteFundingRates.filterNotNull().first().findRate(amount) + if (fundingRate == null) { + state.value = RequestLiquidityState.Error.InvalidFundingAmount + return@launch + } + peer.estimateFeeForInboundLiquidity( amount = amount, targetFeerate = FeeratePerKw(feerate), - leaseRate = NodeParamsManager.liquidityLeaseRate(amount), + fundingRate = fundingRate, ).let { response -> state.value = when (response) { null -> RequestLiquidityState.Error.NoChannelsAvailable else -> { val (actualFeerate, fees) = response - RequestLiquidityState.Estimation(amount, fees, actualFeerate) + RequestLiquidityState.Estimation(amount, fees, actualFeerate, fundingRate) } } } } } - fun requestInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw) { + fun requestInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw, fundingRate: LiquidityAds.FundingRate) { if (state.value is RequestLiquidityState.Requesting) return state.value = RequestLiquidityState.Requesting viewModelScope.launch(Dispatchers.Default + CoroutineExceptionHandler { _, e -> @@ -92,7 +99,7 @@ class RequestLiquidityViewModel(val peerManager: PeerManager, val appConfigManag peer.requestInboundLiquidity( amount = amount, feerate = feerate, - leaseRate = NodeParamsManager.liquidityLeaseRate(amount), + fundingRate = fundingRate, ).let { response -> state.value = when (response) { null -> RequestLiquidityState.Error.NoChannelsAvailable diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/offer/SendOfferView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/offer/SendOfferView.kt index 25ea4a60b..e722dafdc 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/offer/SendOfferView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/offer/SendOfferView.kt @@ -64,7 +64,7 @@ import fr.acinq.phoenix.android.components.SplashLayout import fr.acinq.phoenix.android.components.TextInput import fr.acinq.phoenix.android.components.contact.ContactOrOfferView import fr.acinq.phoenix.android.components.feedback.ErrorMessage -import fr.acinq.phoenix.android.payments.details.translatePaymentError +import fr.acinq.phoenix.android.payments.details.splash.translatePaymentError import fr.acinq.phoenix.android.userPrefs import fr.acinq.phoenix.android.utils.Converter.toPrettyString diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt index 9795b4986..e75bb913b 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt @@ -21,7 +21,7 @@ import com.google.firebase.messaging.FirebaseMessaging import fr.acinq.bitcoin.TxId import fr.acinq.lightning.LiquidityEvents import fr.acinq.lightning.MilliSatoshi -import fr.acinq.lightning.io.PaymentReceived +import fr.acinq.lightning.PaymentEvents import fr.acinq.lightning.utils.Connection import fr.acinq.lightning.utils.currentTimestampMillis import fr.acinq.phoenix.PhoenixBusiness @@ -257,7 +257,7 @@ class NodeService : Service() { val trustedSwapInTxs = LegacyPrefsDatastore.getMigrationTrustedSwapInTxs(applicationContext).first() val preferredFiatCurrency = userPrefs.getFiatCurrency.first() - monitorPaymentsJob = serviceScope.launch { monitorPaymentsWhenHeadless(business.peerManager, business.currencyManager, userPrefs) } + monitorPaymentsJob = serviceScope.launch { monitorPaymentsWhenHeadless(business.nodeParamsManager, business.currencyManager, userPrefs) } monitorNodeEventsJob = serviceScope.launch { monitorNodeEvents(business.peerManager, business.nodeParamsManager) } monitorFcmTokenJob = serviceScope.launch { monitorFcmToken(business) } monitorInFlightPaymentsJob = serviceScope.launch { monitorInFlightPayments(business.peerManager) } @@ -327,8 +327,12 @@ class NodeService : Service() { is LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee -> { SystemNotificationHelper.notifyPaymentRejectedOverRelative(applicationContext, event.source, event.amount, event.fee, reason.maxRelativeFeeBasisPoints, nextTimeout?.second) } - LiquidityEvents.Rejected.Reason.ChannelInitializing -> { - SystemNotificationHelper.notifyPaymentRejectedChannelsInitializing(applicationContext, event.source, event.amount, nextTimeout?.second) + // Temporary errors + is LiquidityEvents.Rejected.Reason.ChannelFundingInProgress, + is LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow, + is LiquidityEvents.Rejected.Reason.NoMatchingFundingRate, + is LiquidityEvents.Rejected.Reason.TooManyParts -> { + SystemNotificationHelper.notifyPaymentRejectedFundingError(applicationContext, event.source, event.amount) } } } @@ -337,17 +341,18 @@ class NodeService : Service() { } } - private suspend fun monitorPaymentsWhenHeadless(peerManager: PeerManager, currencyManager: CurrencyManager, userPrefs: UserPrefsRepository) { - peerManager.getPeer().eventsFlow.collect { event -> + private suspend fun monitorPaymentsWhenHeadless(nodeParamsManager: NodeParamsManager, currencyManager: CurrencyManager, userPrefs: UserPrefsRepository) { + + nodeParamsManager.nodeParams.filterNotNull().first().nodeEvents.collect { event -> when (event) { - is PaymentReceived -> { + is PaymentEvents.PaymentReceived -> { if (isHeadless) { - receivedInBackground.add(event.received.amount) + receivedInBackground.add(event.amount) SystemNotificationHelper.notifyPaymentsReceived( context = applicationContext, userPrefs = userPrefs, - paymentHash = event.incomingPayment.paymentHash, - amount = event.received.amount, + paymentHash = event.paymentHash, + amount = event.amount, rates = currencyManager.ratesFlow.value, isHeadless = isHeadless && receivedInBackground.size == 1 ) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/NotificationsView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/NotificationsView.kt index ee7b80484..25004ed6b 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/NotificationsView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/NotificationsView.kt @@ -268,14 +268,14 @@ private fun PaymentNotification( notification.fee.toPrettyString(btcUnit, withUnit = true), notification.maxAbsoluteFee.toPrettyString(btcUnit, withUnit = true), ) - is Notification.OverRelativeFee -> stringResource( id = R.string.inappnotif_payment_rejected_over_relative, notification.fee.toPrettyString(btcUnit, withUnit = true), DecimalFormat("0.##").format(notification.maxRelativeFeeBasisPoints.toDouble() / 100), ) - - is Notification.ChannelsInitializing -> stringResource(id = R.string.inappnotif_payment_rejected_channel_initializing) + is Notification.GenericError -> "An error has occurred. Please try again." + is Notification.ChannelFundingInProgress -> "A funding is in progress. Try again later." + is Notification.MissingOffChainAmountTooLow -> "The amount is too low." }, bottomText = when (notification) { is Notification.OverAbsoluteFee, is Notification.OverRelativeFee, is Notification.FeePolicyDisabled -> { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/fees/AdvancedIncomingFeePolicy.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/fees/AdvancedIncomingFeePolicy.kt index 9aea96eef..bab90ef09 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/fees/AdvancedIncomingFeePolicy.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/fees/AdvancedIncomingFeePolicy.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import fr.acinq.lightning.payment.LiquidityPolicy +import fr.acinq.lightning.utils.msat import fr.acinq.phoenix.android.R import fr.acinq.phoenix.android.business import fr.acinq.phoenix.android.components.Button @@ -111,7 +112,15 @@ fun AdvancedIncomingFeePolicy( } Card { - val newPolicy = maxRelativeFeeBasisPoints?.let { LiquidityPolicy.Auto(maxRelativeFeeBasisPoints = it, maxAbsoluteFee = maxAbsoluteFee, skipAbsoluteFeeCheck = skipAbsoluteFeeCheck) } + val newPolicy = maxRelativeFeeBasisPoints?.let { + LiquidityPolicy.Auto( + inboundLiquidityTarget = null, + maxRelativeFeeBasisPoints = it, + maxAbsoluteFee = maxAbsoluteFee, + skipAbsoluteFeeCheck = skipAbsoluteFeeCheck, + maxAllowedFeeCredit = 0.msat, + ) + } val isEnabled = newPolicy != null && liquidityPolicyPrefs != newPolicy Button( text = stringResource(id = R.string.liquiditypolicy_save_button), diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/fees/LiquidityPolicyView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/fees/LiquidityPolicyView.kt index 302d0086a..c74b511c4 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/fees/LiquidityPolicyView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/fees/LiquidityPolicyView.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.payment.LiquidityPolicy +import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.phoenix.android.LocalFiatCurrency import fr.acinq.phoenix.android.R @@ -132,7 +133,15 @@ fun LiquidityPolicyView( val skipAbsoluteFeeCheck = if (liquidityPolicyPrefs is LiquidityPolicy.Auto) liquidityPolicyPrefs.skipAbsoluteFeeCheck else false val newPolicy = when { isPolicyDisabled -> LiquidityPolicy.Disable - else -> maxAbsoluteFee?.let { LiquidityPolicy.Auto(maxRelativeFeeBasisPoints = maxPropFeePrefs, maxAbsoluteFee = it, skipAbsoluteFeeCheck = skipAbsoluteFeeCheck) } + else -> maxAbsoluteFee?.let { + LiquidityPolicy.Auto( + inboundLiquidityTarget = null, + maxRelativeFeeBasisPoints = maxPropFeePrefs, + maxAbsoluteFee = it, + skipAbsoluteFeeCheck = skipAbsoluteFeeCheck, + maxAllowedFeeCredit = 0.msat, + ) + } } val isEnabled = newPolicy != null && liquidityPolicyPrefs != newPolicy Button( diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInWalletInfo.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInWalletInfo.kt index e7c59b83c..f910b2188 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInWalletInfo.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInWalletInfo.kt @@ -237,7 +237,9 @@ private fun ReadyForSwapView( DecimalFormat("0.##").format(lastSwapFailedNotification.maxRelativeFeeBasisPoints.toDouble() / 100), ) is Notification.FeePolicyDisabled -> stringResource(id = R.string.walletinfo_onchain_swapin_last_attempt_disabled) - is Notification.ChannelsInitializing -> stringResource(id = R.string.walletinfo_onchain_swapin_last_attempt_channels_init) + is Notification.ChannelFundingInProgress -> stringResource(id = R.string.walletinfo_onchain_swapin_last_attempt_funding_in_progress) + is Notification.MissingOffChainAmountTooLow -> stringResource(id = R.string.walletinfo_onchain_swapin_last_attempt_amount_too_low) + is Notification.GenericError -> stringResource(id = R.string.walletinfo_onchain_swapin_last_attempt_generic) }, ) } 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 b271c424c..82ebe8cea 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 @@ -46,6 +46,7 @@ import fr.acinq.phoenix.data.BitcoinUnit import fr.acinq.phoenix.data.FiatCurrency import fr.acinq.phoenix.data.WalletPaymentId import fr.acinq.phoenix.data.WalletPaymentInfo +import fr.acinq.phoenix.data.WalletPaymentMetadata import fr.acinq.phoenix.data.lnurl.LnurlAuth import fr.acinq.phoenix.legacy.db.* import fr.acinq.phoenix.legacy.utils.Prefs @@ -305,7 +306,7 @@ object LegacyMigrationHelper { // use the PayToOpen metadata to know how the payment was received val receivedWith = if (payToOpenMeta != null || payment.paymentType() == PaymentType.SwapIn()) { IncomingPayment.ReceivedWith.NewChannel( - amount = status.amount().toLong().msat, + amountReceived = status.amount().toLong().msat, serviceFee = payToOpenMeta?.fee_sat?.sat?.toMilliSatoshi() ?: 0.msat, miningFee = 0.sat, channelId = ByteVector32.Zeroes, @@ -315,9 +316,10 @@ object LegacyMigrationHelper { ) } else { IncomingPayment.ReceivedWith.LightningPayment( - amount = status.amount().toLong().msat, + amountReceived = status.amount().toLong().msat, channelId = ByteVector32.Zeroes, - htlcId = 0L + htlcId = 0L, + fundingFee = null, ) } @@ -494,12 +496,11 @@ object LegacyMigrationHelper { } /** Returns true if the payment is a channel-close made by the legacy app to the node's swap-in address. Uses the [LegacyMigrationHelper.migrationDescFlag] metadata flag. */ -fun WalletPaymentInfo.isLegacyMigration(peer: Peer?): Boolean? { - val p = payment +fun WalletPayment.isLegacyMigration(metadata: WalletPaymentMetadata, peer: Peer?): Boolean? { return when { - p !is ChannelCloseOutgoingPayment -> false + this !is ChannelCloseOutgoingPayment -> false peer == null -> null - p.address == peer.phoenixSwapInWallet.legacySwapInAddress && metadata.userDescription == LegacyMigrationHelper.migrationDescFlag -> true + this.address == peer.phoenixSwapInWallet.legacySwapInAddress && metadata.userDescription == LegacyMigrationHelper.migrationDescFlag -> true else -> false } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt index c4bd04a40..e59388a0e 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt @@ -151,12 +151,12 @@ object SystemNotificationHelper { ) } - fun notifyPaymentRejectedChannelsInitializing(context: Context, source: LiquidityEvents.Source, amountIncoming: MilliSatoshi, nextTimeoutRemainingBlocks: Int?): Notification { + fun notifyPaymentRejectedFundingError(context: Context, source: LiquidityEvents.Source, amountIncoming: MilliSatoshi): Notification { return notifyPaymentFailed( context = context, title = context.getString(if (source == LiquidityEvents.Source.OnChainWallet) R.string.notif_rejected_deposit_title else R.string.notif_rejected_payment_title, amountIncoming.toPrettyString(BitcoinUnit.Sat, withUnit = true)), - message = context.getString(R.string.notif_rejected_channels_initializing), + message = context.getString(R.string.notif_rejected_generic_error), deepLink = if (source == LiquidityEvents.Source.OnChainWallet) "phoenix:swapinwallet" else "phoenix:liquiditypolicy", ) } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/datastore/UserPrefsRepository.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/datastore/UserPrefsRepository.kt index 842436df3..e82e737d3 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/datastore/UserPrefsRepository.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/datastore/UserPrefsRepository.kt @@ -25,6 +25,7 @@ import fr.acinq.lightning.TrampolineFees import fr.acinq.lightning.io.TcpSocket import fr.acinq.lightning.payment.LiquidityPolicy import fr.acinq.lightning.utils.ServerAddress +import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.phoenix.android.utils.UserTheme import fr.acinq.phoenix.data.BitcoinUnit @@ -214,7 +215,13 @@ class UserPrefsRepository(private val data: DataStore) { try { it[LIQUIDITY_POLICY]?.let { policy -> when (val res = json.decodeFromString(policy)) { - is InternalLiquidityPolicy.Auto -> LiquidityPolicy.Auto(res.maxAbsoluteFee, res.maxRelativeFeeBasisPoints, res.skipAbsoluteFeeCheck) + is InternalLiquidityPolicy.Auto -> LiquidityPolicy.Auto( + inboundLiquidityTarget = null, + maxAbsoluteFee = res.maxAbsoluteFee, + maxRelativeFeeBasisPoints = res.maxRelativeFeeBasisPoints, + skipAbsoluteFeeCheck = res.skipAbsoluteFeeCheck, + maxAllowedFeeCredit = 0.msat, + ) is InternalLiquidityPolicy.Disable -> LiquidityPolicy.Disable } } 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 d5ea31466..ca81a3e57 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 @@ -17,22 +17,18 @@ package fr.acinq.phoenix.android.utils import android.content.* -import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.core.content.FileProvider import fr.acinq.lightning.db.* import fr.acinq.lightning.utils.Connection -import fr.acinq.lightning.utils.currentTimestampMillis +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.android.* import fr.acinq.phoenix.android.R -import fr.acinq.phoenix.android.utils.Converter.toPrettyString import fr.acinq.phoenix.data.BitcoinUnit import fr.acinq.phoenix.data.FiatCurrency import fr.acinq.phoenix.utils.extensions.desc -import java.io.File import java.security.cert.CertificateException import java.util.* import kotlin.contracts.ExperimentalContracts @@ -126,6 +122,27 @@ fun UserTheme.label(): String { fun Connection.CLOSED.isBadCertificate() = this.reason?.cause is CertificateException +fun LightningOutgoingPayment.smartDescription(context: Context): String? = when (val details = this.details) { + is LightningOutgoingPayment.Details.Normal -> details.paymentRequest.desc + is LightningOutgoingPayment.Details.SwapOut -> context.getString(R.string.paymentdetails_desc_swapout, details.address) + is LightningOutgoingPayment.Details.Blinded -> details.paymentRequest.description +}?.takeIf { it.isNotBlank() } +fun SpliceOutgoingPayment.smartDescription(context: Context): String = context.getString(R.string.paymentdetails_desc_splice_out) +fun SpliceCpfpOutgoingPayment.smartDescription(context: Context): String = context.getString(R.string.paymentdetails_desc_cpfp) +fun ChannelCloseOutgoingPayment.smartDescription(context: Context): String = context.getString(R.string.paymentdetails_desc_closing_channel) +fun InboundLiquidityOutgoingPayment.smartDescription(context: Context): String = when (purchase.paymentDetails) { + // manual inbound liquidity + LiquidityAds.PaymentDetails.FromChannelBalance -> "Manual liquidity" // context.getString(R.string.paymentdetails_desc_inbound_liquidity, purchase.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)) + // pay-to-open/pay-to-splice + else -> "Automated liquidity" +} + +fun IncomingPayment.smartDescription(context: Context) : String? = when (val origin = this.origin) { + is IncomingPayment.Origin.Invoice -> origin.paymentRequest.description + is IncomingPayment.Origin.SwapIn, is IncomingPayment.Origin.OnChain -> context.getString(R.string.paymentdetails_desc_swapin) + is IncomingPayment.Origin.Offer -> null +}?.takeIf { it.isNotBlank() } + /** * Returns a trimmed, localized description of the payment, based on the type and information available. May be null! * @@ -133,18 +150,10 @@ fun Connection.CLOSED.isBadCertificate() = this.reason?.cause is CertificateExce * payment with an invoice do have a description baked in, and that's what is returned. */ 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.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.SwapIn, is IncomingPayment.Origin.OnChain -> context.getString(R.string.paymentdetails_desc_swapin) - is IncomingPayment.Origin.Offer -> null - } - is SpliceOutgoingPayment -> context.getString(R.string.paymentdetails_desc_splice_out) - is ChannelCloseOutgoingPayment -> context.getString(R.string.paymentdetails_desc_closing_channel) - is SpliceCpfpOutgoingPayment -> context.getString(R.string.paymentdetails_desc_cpfp) - is InboundLiquidityOutgoingPayment -> context.getString(R.string.paymentdetails_desc_inbound_liquidity, lease.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)) -}?.takeIf { it.isNotBlank() } \ No newline at end of file + is LightningOutgoingPayment -> smartDescription(context) + is IncomingPayment -> smartDescription(context) + is SpliceOutgoingPayment -> smartDescription(context) + is ChannelCloseOutgoingPayment -> smartDescription(context) + is SpliceCpfpOutgoingPayment -> smartDescription(context) + is InboundLiquidityOutgoingPayment -> smartDescription(context) +} \ No newline at end of file diff --git a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml index 5e546b468..1e58b9b5c 100644 --- a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml @@ -41,7 +41,6 @@ La comisión fue %1$s, pero el límite máximo se fijó en %2$s. Este depósito vencerá el %3$s. La comisión fue %1$s, que es más del %2$s%% del importe recibido. Toca para obtener más información. La comisión fue %1$s, que es más del %2$s%% del importe recibido. Este depósito vencerá el %3$s. - Los canales se estaban inicializando, por lo que no se pudo aceptar el pago. Intenta de nuevo más tarde. Inicia Phoenix Es posible que algunos de los canales se hayan cerrado. diff --git a/phoenix-android/src/main/res/values-b+es+419/strings.xml b/phoenix-android/src/main/res/values-b+es+419/strings.xml index f737205ab..cfcaeb56b 100644 --- a/phoenix-android/src/main/res/values-b+es+419/strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/strings.xml @@ -330,7 +330,7 @@ Mensaje Desencriptando mensaje… - Desc. + Descripción Enviado a Mineros de Bitcoin Comisiones diff --git a/phoenix-android/src/main/res/values-cs/important_strings.xml b/phoenix-android/src/main/res/values-cs/important_strings.xml index 9f4a465c7..11856103e 100644 --- a/phoenix-android/src/main/res/values-cs/important_strings.xml +++ b/phoenix-android/src/main/res/values-cs/important_strings.xml @@ -44,7 +44,6 @@ Poplatek činil %1$s, ale váš maximální poplatek byl nastaven na %2$s. Platnost této zálohy vyprší %3$s. Poplatek činil %1$s, což je více než %2$s%% z přijaté částky. Můžete to upravit v nastavení. Poplatek činil %1$s, což je více než %2$s%% z přijaté částky. Platnost této zálohy vyprší %3$s. - Vaše kanály se inicializovaly a nemohly tuto platbu přijmout. Zkuste to později. Spusťte prosím Phoenix Některý z vašich kanálů mohl být uzavřen. diff --git a/phoenix-android/src/main/res/values-de/important_strings.xml b/phoenix-android/src/main/res/values-de/important_strings.xml index 08c413c06..40c28e957 100644 --- a/phoenix-android/src/main/res/values-de/important_strings.xml +++ b/phoenix-android/src/main/res/values-de/important_strings.xml @@ -41,7 +41,6 @@ Die Gebühr wäre %1$s, aber Ihr Gebührenlimit beträgt %2$s. Diese Einzahlung verfällt am %3$s. Die Gebühr wäre %1$s, was mehr als %2$s%% des zu empfangenden Betrags ist. Sie können dies in den Einstellungen anpassen. Die Gebühr wäre %1$s, was mehr als %2$s%% des zu empfangenden Betrags ist. Diese Einzahlung verfällt am %3$s. - Ihre Kanäle werden gerade initialisiert und konnten die Zahlung nicht empfangen. Versuchen Sie es später noch mal. Bitte öffnen Sie Phoenix Einige Ihrer Kanäle wurden möglicherweise geschlossen. diff --git a/phoenix-android/src/main/res/values-de/strings.xml b/phoenix-android/src/main/res/values-de/strings.xml index 55efb98da..ee56cb041 100644 --- a/phoenix-android/src/main/res/values-de/strings.xml +++ b/phoenix-android/src/main/res/values-de/strings.xml @@ -337,7 +337,7 @@ Nachricht Nachricht entschlüsseln.. - Desc. + Beschreibung Gesendet an Bitcoin-Miner Gebühren @@ -382,7 +382,6 @@ - #%1$s: Geforderter Betrag - Unterschrift Schließungs-Typ Einvernehmlich diff --git a/phoenix-android/src/main/res/values-es/important_strings.xml b/phoenix-android/src/main/res/values-es/important_strings.xml index b77e6643a..4f9c70115 100644 --- a/phoenix-android/src/main/res/values-es/important_strings.xml +++ b/phoenix-android/src/main/res/values-es/important_strings.xml @@ -44,7 +44,6 @@ La tasa era %1$s, pero tu tarifa máxima estaba fijada en %2$s. Este depósito expirará el %3$s. La tasa fue de %1$s, que es más del %2$s%% del importe recibido. Puedes modificarlo en la configuración. La tasa fue de %1$s, que es más del %2$s%% del importe recibido. Este depósito expirará el %3$s. - Sus canales se estaban inicializando y no pudieron aceptar ese pago. Vuelva a intentarlo más tarde. Por favor, inicie Phoenix Es posible que algunos de sus canales hayan cerrado. diff --git a/phoenix-android/src/main/res/values-fr/important_strings.xml b/phoenix-android/src/main/res/values-fr/important_strings.xml index 52c827bbe..3ee5a3cac 100644 --- a/phoenix-android/src/main/res/values-fr/important_strings.xml +++ b/phoenix-android/src/main/res/values-fr/important_strings.xml @@ -44,7 +44,6 @@ Les frais étaient de %1$s, mais votre max est de %2$s. Le dépôt expirera le %3$s. Les frais étaient de %1$s et dépassent %2$s%% du montant. Cette configuration peut être changée. Les frais étaient de %1$s et dépassent %2$s%% du montant. Le dépôt expirera le %3$s. - Vos canaux de paiement étaient en initialisation, et n\'ont pu accepter ce paiement. Veuillez démarrer Phoenix Certains de vos canaux pourraient avoir fermé. diff --git a/phoenix-android/src/main/res/values-fr/strings.xml b/phoenix-android/src/main/res/values-fr/strings.xml index 9856c974b..6e9f52a50 100644 --- a/phoenix-android/src/main/res/values-fr/strings.xml +++ b/phoenix-android/src/main/res/values-fr/strings.xml @@ -380,7 +380,6 @@ - #%1$s: Liquidité demandée - Signature Type de clôture Mutuelle diff --git a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml index 75c5c5ed4..379065e28 100644 --- a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml +++ b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml @@ -44,7 +44,6 @@ A taxa foi de %1$s, mas sua taxa máxima foi definida como %2$s. Esse depósito expirará em %3$s. A taxa foi de %1$s, que é mais do que %2$s%% do valor recebido. Você pode ajustar isso nas configurações. A taxa foi de %1$s, que é mais do que %2$s%% do valor recebido. Esse depósito expirará em %3$s. - Seus canais estavam sendo inicializados e não puderam aceitar esse pagamento. Tente novamente mais tarde. Por favor, inicie o Phoenix Alguns de seus canais podem ter sido fechados. diff --git a/phoenix-android/src/main/res/values-sk/important_strings.xml b/phoenix-android/src/main/res/values-sk/important_strings.xml index 0b61efaa3..53497fc86 100644 --- a/phoenix-android/src/main/res/values-sk/important_strings.xml +++ b/phoenix-android/src/main/res/values-sk/important_strings.xml @@ -44,7 +44,6 @@ Poplatok bol %1$s, ale váš maximálny poplatok bol nastavený na %2$s. Platnosť tohto vkladu vyprší %3$s. Poplatok bol %1$s, čo je viac než %2$s%% z prijatej sumy. Kliknite pre podrobnosti. Poplatok bol %1$s, čo je viac než %2$s%% z prijatej sumy. Platnosť tohto vkladu vyprší %3$s. - Vaše kanály sa inicializovali a nemohli prijať túto platbu. Skúste to neskôr. Spustite prosím Phoenix Niektorý z vašich kanálov mohol byť uzavretý. diff --git a/phoenix-android/src/main/res/values-sk/strings.xml b/phoenix-android/src/main/res/values-sk/strings.xml index 4c65ccc00..98502402c 100644 --- a/phoenix-android/src/main/res/values-sk/strings.xml +++ b/phoenix-android/src/main/res/values-sk/strings.xml @@ -404,7 +404,6 @@ - #%1$s: Požadovaná suma - Podpis Typ uzavretia Vzájomné diff --git a/phoenix-android/src/main/res/values-sw/important_strings.xml b/phoenix-android/src/main/res/values-sw/important_strings.xml index 16b45bbdc..72d18613a 100644 --- a/phoenix-android/src/main/res/values-sw/important_strings.xml +++ b/phoenix-android/src/main/res/values-sw/important_strings.xml @@ -48,7 +48,6 @@ Ada ilikuwa %1$s, lakini ada yako ya juu ilikuwa imewekwa kwa %2$s. Amana hii itaisha muda wake ifikapo %3$s. Ada ilikuwa %1$s ambayo ni zaidi ya %2$s%% ya kiasi kilichopokelewa. Bonyeza kwa maelezo zaidi. Ada ilikuwa %1$s ambayo ni zaidi ya %2$s%% ya kiasi kilichopokelewa. Amana hii itaisha muda wake ifikapo %3$s. - Chaneli zako zilikuwa zinaanzishwa na hazikuweza kukubali malipo hayo. Jaribu tena baadaye. Tafadhali anzisha Phoenix Baadhi ya chaneli zako zinaweza kuwa zimefungwa. diff --git a/phoenix-android/src/main/res/values-sw/strings.xml b/phoenix-android/src/main/res/values-sw/strings.xml index 8fdbcbc72..70b4890c2 100644 --- a/phoenix-android/src/main/res/values-sw/strings.xml +++ b/phoenix-android/src/main/res/values-sw/strings.xml @@ -423,7 +423,6 @@ - #%1$s: Kiasi kilichoombwa - Sahihi Aina ya kufunga Kushirikiana diff --git a/phoenix-android/src/main/res/values-vi/important_strings.xml b/phoenix-android/src/main/res/values-vi/important_strings.xml index a8a7d5224..b5a0c5784 100644 --- a/phoenix-android/src/main/res/values-vi/important_strings.xml +++ b/phoenix-android/src/main/res/values-vi/important_strings.xml @@ -51,7 +51,6 @@ Khoản phí là %1$s, nhưng phí tối đa của bạn được đặt là %2$s. Khoản tiền cọc này sẽ hết hạn vào %3$s. Khoản phí là %1$s và cao hơn %2$s%% so với khoản nhận được. Nhấn để biết thêm chi tiết. Khoản phí là %1$s và cao hơn %2$s%% so với khoản tiền sẽ nhận được. Khoản tiền này sẽ hết hạn vào %3$s. - Kênh của bạn đang được khởi tạo và không thể nhận khoản thanh toán này. Vui lòng thử lại sau. Xin hãy khởi động Phoenix. Một vài kênh của bạn có thể đã đóng. diff --git a/phoenix-android/src/main/res/values-vi/strings.xml b/phoenix-android/src/main/res/values-vi/strings.xml index cbd184fc9..36a841419 100644 --- a/phoenix-android/src/main/res/values-vi/strings.xml +++ b/phoenix-android/src/main/res/values-vi/strings.xml @@ -383,7 +383,6 @@ - #%1$s: Số tiền yêu cầu - Chữ ký Hình thức đóng Đồng thuận diff --git a/phoenix-android/src/main/res/values/important_strings.xml b/phoenix-android/src/main/res/values/important_strings.xml index a67f3efc9..253a0e171 100644 --- a/phoenix-android/src/main/res/values/important_strings.xml +++ b/phoenix-android/src/main/res/values/important_strings.xml @@ -48,7 +48,7 @@ The fee was %1$s, but your max fee was set to %2$s. This deposit will expire by %3$s. The fee was %1$s which is more than %2$s%% of the amount received. Tap for details. The fee was %1$s which is more than %2$s%% of the amount received. This deposit will expire by %3$s. - Your channels were initializing and could not accept that payment. Try again later. + An error occurred during the funding. Please try again later. Please start Phoenix Some of your channels may have closed. @@ -313,7 +313,9 @@ A swap attempt failed %1$s Channels management was disabled. - Channels were still initializing. + A funding is in progress. + The amount was too low. + An error has occurred. Try again later. This swap will expire in a day! This swap will expire in %1$s days. diff --git a/phoenix-android/src/main/res/values/strings.xml b/phoenix-android/src/main/res/values/strings.xml index 8b4251ed2..697f827c6 100644 --- a/phoenix-android/src/main/res/values/strings.xml +++ b/phoenix-android/src/main/res/values/strings.xml @@ -349,6 +349,7 @@ Could not find payment details LIQUIDITY ADDED %1$s + CHANNEL RESIZED %1$s COMPLETE %1$s SENT %1$s Pending… @@ -378,7 +379,7 @@ Message Decrypting message… - Desc. + Description Sent to Bitcoin miners Fees @@ -425,7 +426,6 @@ - #%1$s: Amount requested - Signature Closing type Mutual @@ -459,6 +459,7 @@ Splice-in (adding to existing channel) New channel (automatically created) Lightning payment + Fee credit Channel id Transaction @@ -470,6 +471,8 @@ Amount requested Amount sent (fees included) Amount received + Fee credit accrued + Amount added to fee credit ≈ %1$s (now) ≈ %1$s (then) From 702c612e52ceda1d719f2e74c189ea039c970e2d Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Thu, 26 Sep 2024 19:40:25 +0200 Subject: [PATCH 03/25] Update to lightning-kmp snapshot (wip) Depends on lightning-kmp master minus kotlin-2.0 --- buildSrc/src/main/kotlin/Versions.kt | 2 +- .../android/payments/cpfp/CpfpViewModel.kt | 7 +++-- .../details/PaymentDetailsTechnicalView.kt | 2 +- .../details/splash/SplashLiquidityPurchase.kt | 2 +- .../liquidity/RequestLiquidityView.kt | 3 +- .../liquidity/RequestLiquidityViewModel.kt | 11 +++---- .../payments/spliceout/SpliceOutView.kt | 29 ++++++++++--------- .../payments/spliceout/SpliceOutViewModel.kt | 15 +++++----- .../managers/AppConfigurationManager.kt | 3 +- .../managers/NodeParamsManager.kt | 2 +- .../utils/BlockchainExplorer.kt | 12 +++++--- .../extensions/PaymentRequestExtensions.kt | 2 +- 12 files changed, 51 insertions(+), 39 deletions(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 5e1c26379..8987bbd52 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.7.3" + const val lightningKmp = "1.7.4-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/cpfp/CpfpViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/cpfp/CpfpViewModel.kt index f238df347..dcc28826e 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/cpfp/CpfpViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/cpfp/CpfpViewModel.kt @@ -27,6 +27,7 @@ import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelCommand +import fr.acinq.lightning.channel.ChannelFundingResponse import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.phoenix.managers.PeerManager @@ -42,7 +43,7 @@ sealed class CpfpState { data class Executing(val actualFeerate: FeeratePerKw) : CpfpState() sealed class Complete : CpfpState() { object Success: Complete() - data class Failed(val failure: ChannelCommand.Commitment.Splice.Response.Failure): Complete() + data class Failed(val failure: ChannelFundingResponse.Failure): Complete() } sealed class Error: CpfpState() { data class Thrown(val e: Throwable): Error() @@ -102,11 +103,11 @@ class CpfpViewModel(val peerManager: PeerManager) : ViewModel() { log.info("failed to execute cpfp splice: assuming no channels") state = CpfpState.Error.NoChannels } - is ChannelCommand.Commitment.Splice.Response.Created -> { + is ChannelFundingResponse.Success -> { log.info("successfully executed cpfp splice: $res") state = CpfpState.Complete.Success } - is ChannelCommand.Commitment.Splice.Response.Failure -> { + is ChannelFundingResponse.Failure -> { log.info("failed to execute cpfp splice: $res") state = CpfpState.Complete.Failed(res) } 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 9ce0f9f68..52c83fcaf 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 @@ -253,7 +253,7 @@ private fun AmountSection( ) TechnicalRowAmount( label = stringResource(id = R.string.paymentdetails_liquidity_service_fee_label), - amount = payment.serviceFees.toMilliSatoshi(), + amount = payment.purchase.fees.serviceFee.toMilliSatoshi(), rateThen = rateThen, mSatDisplayPolicy = MSatDisplayPolicy.SHOW ) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt index c882b9a30..6980cb375 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt @@ -134,7 +134,7 @@ private fun SplashFee( label = stringResource(id = R.string.paymentdetails_liquidity_service_fee_label), helpMessage = stringResource(id = R.string.paymentdetails_liquidity_service_fee_help) ) { - Text(text = payment.serviceFees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + Text(text = payment.purchase.fees.serviceFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) if (payment.purchase is LiquidityAds.Purchase.WithFeeCredit) { Text(text = "Paid with fee credit") } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt index 51599da92..c7e679ff1 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt @@ -53,6 +53,7 @@ import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelCommand +import fr.acinq.lightning.channel.ChannelFundingResponse import fr.acinq.lightning.channel.ChannelManagementFees import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.sum @@ -381,7 +382,7 @@ private fun ReviewLiquidityRequest( } @Composable -private fun LiquiditySuccessDetails(liquidityDetails: ChannelCommand.Commitment.Splice.Response.Created) { +private fun LiquiditySuccessDetails(liquidityDetails: ChannelFundingResponse.Success) { SuccessMessage( header = stringResource(id = R.string.liquidityads_success), details = liquidityDetails.liquidityPurchase?.amount?.let { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityViewModel.kt index 0cb7a4607..14b926a85 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityViewModel.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.viewModelScope import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelCommand +import fr.acinq.lightning.channel.ChannelFundingResponse import fr.acinq.lightning.channel.ChannelManagementFees import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.managers.AppConfigurationManager @@ -41,9 +42,9 @@ sealed class RequestLiquidityState { data class Estimation(val amount: Satoshi, val fees: ChannelManagementFees, val actualFeerate: FeeratePerKw, val fundingRate: LiquidityAds.FundingRate): RequestLiquidityState() object Requesting: RequestLiquidityState() sealed class Complete: RequestLiquidityState() { - abstract val response: ChannelCommand.Commitment.Splice.Response - data class Success(override val response: ChannelCommand.Commitment.Splice.Response.Created): Complete() - data class Failed(override val response: ChannelCommand.Commitment.Splice.Response.Failure): Complete() + abstract val response: ChannelFundingResponse + data class Success(override val response: ChannelFundingResponse.Success): Complete() + data class Failed(override val response: ChannelFundingResponse.Failure): Complete() } sealed class Error: RequestLiquidityState() { data class Thrown(val cause: Throwable): Error() @@ -103,8 +104,8 @@ class RequestLiquidityViewModel(val peerManager: PeerManager, val appConfigManag ).let { response -> state.value = when (response) { null -> RequestLiquidityState.Error.NoChannelsAvailable - is ChannelCommand.Commitment.Splice.Response.Failure -> RequestLiquidityState.Complete.Failed(response) - is ChannelCommand.Commitment.Splice.Response.Created -> RequestLiquidityState.Complete.Success(response) + is ChannelFundingResponse.Failure -> RequestLiquidityState.Complete.Failed(response) + is ChannelFundingResponse.Success -> RequestLiquidityState.Complete.Success(response) } } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutView.kt index 87d9334fc..94004ca59 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutView.kt @@ -43,6 +43,7 @@ import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelCommand +import fr.acinq.lightning.channel.ChannelFundingResponse import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toMilliSatoshi import fr.acinq.phoenix.android.LocalBitcoinUnit @@ -417,17 +418,19 @@ private fun ReviewSpliceOutAndConfirm( } @Composable -fun spliceFailureDetails(spliceFailure: ChannelCommand.Commitment.Splice.Response.Failure): String = when (spliceFailure) { - is ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer -> stringResource(id = R.string.splice_error_aborted_by_peer, spliceFailure.reason) - is ChannelCommand.Commitment.Splice.Response.Failure.CannotCreateCommitTx -> stringResource(id = R.string.splice_error_cannot_create_commit) - is ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice -> stringResource(id = R.string.splice_error_concurrent_remote) - is ChannelCommand.Commitment.Splice.Response.Failure.ChannelNotQuiescent -> stringResource(id = R.string.splice_error_channel_not_quiescent) - is ChannelCommand.Commitment.Splice.Response.Failure.Disconnected -> stringResource(id = R.string.splice_error_disconnected) - is ChannelCommand.Commitment.Splice.Response.Failure.FundingFailure -> stringResource(id = R.string.splice_error_funding_error, spliceFailure.reason.javaClass.simpleName) - is ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds -> stringResource(id = R.string.splice_error_insufficient_funds) - is ChannelCommand.Commitment.Splice.Response.Failure.CannotStartSession -> stringResource(id = R.string.splice_error_cannot_start_session) - is ChannelCommand.Commitment.Splice.Response.Failure.InteractiveTxSessionFailed -> stringResource(id = R.string.splice_error_interactive_session, spliceFailure.reason.javaClass.simpleName) - is ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript -> stringResource(id = R.string.splice_error_invalid_pubkey) - is ChannelCommand.Commitment.Splice.Response.Failure.SpliceAlreadyInProgress -> stringResource(id = R.string.splice_error_splice_in_progress) - is ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds -> stringResource(id = R.string.splice_error_invalid_liquidity_ads, spliceFailure.reason.details()) +fun spliceFailureDetails(spliceFailure: ChannelFundingResponse.Failure): String = when (spliceFailure) { + is ChannelFundingResponse.Failure.AbortedByPeer -> stringResource(id = R.string.splice_error_aborted_by_peer, spliceFailure.reason) + is ChannelFundingResponse.Failure.CannotCreateCommitTx -> stringResource(id = R.string.splice_error_cannot_create_commit) + is ChannelFundingResponse.Failure.ConcurrentRemoteSplice -> stringResource(id = R.string.splice_error_concurrent_remote) + is ChannelFundingResponse.Failure.ChannelNotQuiescent -> stringResource(id = R.string.splice_error_channel_not_quiescent) + is ChannelFundingResponse.Failure.Disconnected -> stringResource(id = R.string.splice_error_disconnected) + is ChannelFundingResponse.Failure.FundingFailure -> stringResource(id = R.string.splice_error_funding_error, spliceFailure.reason.javaClass.simpleName) + is ChannelFundingResponse.Failure.InsufficientFunds -> stringResource(id = R.string.splice_error_insufficient_funds) + is ChannelFundingResponse.Failure.CannotStartSession -> stringResource(id = R.string.splice_error_cannot_start_session) + is ChannelFundingResponse.Failure.InteractiveTxSessionFailed -> stringResource(id = R.string.splice_error_interactive_session, spliceFailure.reason.javaClass.simpleName) + is ChannelFundingResponse.Failure.InvalidSpliceOutPubKeyScript -> stringResource(id = R.string.splice_error_invalid_pubkey) + is ChannelFundingResponse.Failure.SpliceAlreadyInProgress -> stringResource(id = R.string.splice_error_splice_in_progress) + is ChannelFundingResponse.Failure.InvalidLiquidityAds -> stringResource(id = R.string.splice_error_invalid_liquidity_ads, spliceFailure.reason.details()) + is ChannelFundingResponse.Failure.InvalidChannelParameters -> TODO() + is ChannelFundingResponse.Failure.UnexpectedMessage -> TODO() } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutViewModel.kt index 452b7d42f..b4d95caf3 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutViewModel.kt @@ -27,6 +27,7 @@ import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelCommand +import fr.acinq.lightning.channel.ChannelFundingResponse import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.phoenix.managers.PeerManager @@ -45,9 +46,9 @@ sealed class SpliceOutState { sealed class Complete: SpliceOutState() { abstract val userAmount: Satoshi abstract val feerate: FeeratePerKw - abstract val result: ChannelCommand.Commitment.Splice.Response - data class Success(override val userAmount: Satoshi, override val feerate: FeeratePerKw, override val result: ChannelCommand.Commitment.Splice.Response.Created): Complete() - data class Failure(override val userAmount: Satoshi, override val feerate: FeeratePerKw, override val result: ChannelCommand.Commitment.Splice.Response.Failure): Complete() + abstract val result: ChannelFundingResponse + data class Success(override val userAmount: Satoshi, override val feerate: FeeratePerKw, override val result: ChannelFundingResponse.Success): Complete() + data class Failure(override val userAmount: Satoshi, override val feerate: FeeratePerKw, override val result: ChannelFundingResponse.Failure): Complete() } sealed class Error: SpliceOutState() { data class Thrown(val e: Throwable): Error() @@ -70,7 +71,7 @@ class SpliceOutViewModel(private val peerManager: PeerManager, private val chain state = SpliceOutState.Error.Thrown(e) }) { state = SpliceOutState.Preparing(userAmount = amount, feeratePerByte = feeratePerByte) - log.debug("preparing splice-out for amount=$amount feerate=${feeratePerByte}sat/vb address=$address") + log.debug("preparing splice-out for amount={} feerate={}sat/vb address={}", amount, feeratePerByte, address) val userFeerate = FeeratePerKw(FeeratePerByte(feeratePerByte)) val scriptPubKey = Parser.addressToPublicKeyScriptOrNull(chain, address)!! val res = peerManager.getPeer().estimateFeeForSpliceOut( @@ -100,7 +101,7 @@ class SpliceOutViewModel(private val peerManager: PeerManager, private val chain ) { if (state is SpliceOutState.ReadyToSend) { state = SpliceOutState.Executing(amount, feerate) - log.debug("executing splice-out with for=$amount feerate=${feerate}sat/vb address=$address") + log.debug("executing splice-out with for={} feerate={}sat/vb address={}", amount, feerate, address) viewModelScope.launch(Dispatchers.Default + CoroutineExceptionHandler { _, e -> log.error("error when executing splice-out: ", e) state = SpliceOutState.Error.Thrown(e) @@ -111,11 +112,11 @@ class SpliceOutViewModel(private val peerManager: PeerManager, private val chain feerate = feerate, ) when (response) { - is ChannelCommand.Commitment.Splice.Response.Created -> { + is ChannelFundingResponse.Success -> { log.info("successfully executed splice-out: $response") state = SpliceOutState.Complete.Success(amount, feerate, response) } - is ChannelCommand.Commitment.Splice.Response.Failure -> { + is ChannelFundingResponse.Failure -> { log.info("failed to execute splice-out: $response") state = SpliceOutState.Complete.Failure(amount, feerate, response) } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt index 92e189ded..9db9ad87c 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt @@ -218,7 +218,8 @@ class AppConfigurationManager( fun randomElectrumServer() = when (chain) { Chain.Mainnet -> mainnetElectrumServers.random() - Chain.Testnet -> testnetElectrumServers.random() + Chain.Testnet3 -> testnetElectrumServers.random() + Chain.Testnet4 -> TODO() Chain.Signet -> TODO() Chain.Regtest -> platformElectrumRegtestConf() } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt index 482d7a223..90e46464d 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NodeParamsManager.kt @@ -88,7 +88,7 @@ class NodeParamsManager( } companion object { - val chain = Chain.Testnet + val chain = Chain.Testnet3 val trampolineNodeId = PublicKey.fromHex("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134") val trampolineNodeUri = NodeUri(id = trampolineNodeId, "13.248.222.197", 9735) const val remoteSwapInXpub = "tpubDAmCFB21J9ExKBRPDcVxSvGs9jtcf8U1wWWbS1xTYmnUsuUHPCoFdCnEGxLE3THSWcQE48GHJnyz8XPbYUivBMbLSMBifFd3G9KmafkM9og" diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/BlockchainExplorer.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/BlockchainExplorer.kt index 4c441e9c8..dde985b3d 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/BlockchainExplorer.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/BlockchainExplorer.kt @@ -16,7 +16,8 @@ class BlockchainExplorer(private val chain: Chain) { Website.MempoolSpace -> { when (chain) { Chain.Mainnet -> "${website.base}/tx/$txId" - Chain.Testnet -> "${website.base}/testnet/tx/$txId" + Chain.Testnet3 -> "${website.base}/testnet3/tx/$txId" + Chain.Testnet4 -> "${website.base}/testnet4/tx/$txId" Chain.Signet -> "${website.base}/signet/tx/$txId" Chain.Regtest -> "${website.base}/_REGTEST_/tx/$txId" } @@ -24,7 +25,8 @@ class BlockchainExplorer(private val chain: Chain) { Website.BlockstreamInfo -> { when (chain) { Chain.Mainnet -> "${website.base}/tx/$txId" - Chain.Testnet -> "${website.base}/testnet/tx/$txId" + Chain.Testnet3 -> "${website.base}/testnet3/tx/$txId" + Chain.Testnet4 -> "${website.base}/testnet4/tx/$txId" Chain.Signet -> "${website.base}/signet/tx/$txId" Chain.Regtest -> "${website.base}/_REGTEST_/tx/$txId" } @@ -37,7 +39,8 @@ class BlockchainExplorer(private val chain: Chain) { Website.MempoolSpace -> { when (chain) { Chain.Mainnet -> "${website.base}/address/$addr" - Chain.Testnet -> "${website.base}/testnet/address/$addr" + Chain.Testnet3 -> "${website.base}/testnet3/address/$addr" + Chain.Testnet4 -> "${website.base}/testnet4/address/$addr" Chain.Signet -> "${website.base}/signet/address/$addr" Chain.Regtest -> "${website.base}/_REGTEST_/address/$addr" } @@ -45,7 +48,8 @@ class BlockchainExplorer(private val chain: Chain) { Website.BlockstreamInfo -> { when (chain) { Chain.Mainnet -> "${website.base}/address/$addr" - Chain.Testnet -> "${website.base}/testnet/address/$addr" + Chain.Testnet3 -> "${website.base}/testnet3/address/$addr" + Chain.Testnet4 -> "${website.base}/testnet4/address/$addr" Chain.Signet -> "${website.base}/signet/address/$addr" Chain.Regtest -> "${website.base}/_REGTEST_/address/$addr" } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentRequestExtensions.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentRequestExtensions.kt index d191caa0e..e29b00379 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentRequestExtensions.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentRequestExtensions.kt @@ -36,7 +36,7 @@ val PaymentRequest.chain: Chain is Bolt11Invoice -> { when (prefix) { "lnbc" -> Chain.Mainnet - "lntb" -> Chain.Testnet + "lntb" -> Chain.Testnet3 "lnbcrt" -> Chain.Regtest else -> throw IllegalArgumentException("unhandled invoice prefix=$prefix") } From d50426876994c5f863bd06eb00e94ad441674bf5 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:33:02 +0200 Subject: [PATCH 04/25] Add method to get purchases related to an incoming payment This is useful to track the fees incurred by an incoming payments because of a liquidity purchase. Also fixed payments history ordering. The sort logic is now: - created_at for outgoing payments - received_at for incoming payments Also improved descriptions for CSV writer to be more neutral and more informative. --- .../managers/PaymentsManager.kt | 14 +++++++++++++ .../fr.acinq.phoenix/utils/CsvWriter.kt | 10 ++++------ .../utils/extensions/PaymentExtensions.kt | 9 +++++++++ .../fr.acinq.phoenix.db/AggregatedQueries.sq | 20 ++++++++++++------- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PaymentsManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PaymentsManager.kt index d5cd14fab..03242393b 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PaymentsManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PaymentsManager.kt @@ -153,6 +153,20 @@ class PaymentsManager( return paymentsDb().listPaymentsForTxId(txId) } + /** + * Returns the inbound liquidity purchase relevant to a given transaction id. + * + * It is used to track the fees that may have been incurred by an incoming payment because of low liquidity. To do + * that, we use the transaction id attached to the incoming payment, and find any purchase that matches. + * + * This allows us to display the fees of a liquidity purchase inside an incoming payment details screen. + */ + suspend fun getLiquidityPurchaseForTxId( + txId: TxId + ): InboundLiquidityOutgoingPayment? { + return paymentsDb().getInboundLiquidityPurchase(txId) + } + suspend fun getPayment( id: WalletPaymentId, options: WalletPaymentFetchOptions 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 8c021bbb6..f770a7be3 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 @@ -122,22 +122,20 @@ class CsvWriter { is IncomingPayment -> when (val origin = payment.origin) { is IncomingPayment.Origin.Invoice -> "Incoming LN payment" is IncomingPayment.Origin.SwapIn -> "Swap-in to ${origin.address ?: "N/A"}" - is IncomingPayment.Origin.OnChain -> { - "Swap-in with inputs: ${origin.localInputs.map { it.txid.toString() } }" - } + is IncomingPayment.Origin.OnChain -> "On-chain deposit" is IncomingPayment.Origin.Offer -> when (origin.metadata) { is OfferPaymentMetadata.V1 -> "Incoming payment to your offer" } } is LightningOutgoingPayment -> when (val details = payment.details) { is LightningOutgoingPayment.Details.Normal -> "Outgoing LN payment to ${details.paymentRequest.nodeId.toHex()}" - is LightningOutgoingPayment.Details.SwapOut -> "Swap-out to ${details.address}" - is LightningOutgoingPayment.Details.Blinded -> "Offer to ${details.payerKey.publicKey()}" + is LightningOutgoingPayment.Details.SwapOut -> "Outgoing Swap to ${details.address}" + is LightningOutgoingPayment.Details.Blinded -> "Outgoing LN payment to ${details.paymentRequest.invoiceRequest.offer.encode()}" } is SpliceOutgoingPayment -> "Outgoing splice to ${payment.address}" is ChannelCloseOutgoingPayment -> "Channel closing to ${payment.address}" is SpliceCpfpOutgoingPayment -> "Accelerate transactions with CPFP" - is InboundLiquidityOutgoingPayment -> "+${payment.purchase.amount.sat} sat inbound liquidity" + is InboundLiquidityOutgoingPayment -> "Inbound liquidity purchase (+${payment.purchase.amount.sat} sat)" } row += ",${processField(details)}" } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt index 23c63fdfb..ad74f425e 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt @@ -29,7 +29,9 @@ import fr.acinq.lightning.payment.OfferPaymentMetadata import fr.acinq.lightning.utils.getValue import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sum +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.lightning.wire.OfferTypes +import fr.acinq.phoenix.data.WalletPaymentId /** Standardized location for extending types from: fr.acinq.lightning. */ enum class WalletPaymentState { SuccessOnChain, SuccessOffChain, PendingOnChain, PendingOffChain, Failure } @@ -99,3 +101,10 @@ fun WalletPayment.errorMessage(): String? = when (this) { fun WalletPayment.incomingOfferMetadata(): OfferPaymentMetadata.V1? = ((this as? IncomingPayment)?.origin as? IncomingPayment.Origin.Offer)?.metadata as? OfferPaymentMetadata.V1 fun WalletPayment.outgoingInvoiceRequest(): OfferTypes.InvoiceRequest? = ((this as? LightningOutgoingPayment)?.details as? LightningOutgoingPayment.Details.Blinded)?.paymentRequest?.invoiceRequest + +/** Returns a list of the ids of the payments that triggered this liquidity purchase. May be empty, for example if this is a manual purchase. */ +fun InboundLiquidityOutgoingPayment.relatedPaymentIds() : List = when (val details = purchase.paymentDetails) { + is LiquidityAds.PaymentDetails.FromFutureHtlc -> details.paymentHashes + is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> details.paymentHashes + else -> emptyList() +}.map { WalletPaymentId.IncomingPaymentId(it) } diff --git a/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/AggregatedQueries.sq b/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/AggregatedQueries.sq index 6f8ba0b1a..fd680c4e9 100644 --- a/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/AggregatedQueries.sq +++ b/phoenix-shared/src/commonMain/paymentsdb/fr.acinq.phoenix.db/AggregatedQueries.sq @@ -14,42 +14,48 @@ FROM ( 2 AS type, id AS id, created_at AS created_at, - completed_at AS completed_at + completed_at AS completed_at, + created_at AS order_ts FROM outgoing_payments UNION ALL SELECT 3 AS type, id AS id, created_at AS created_at, - confirmed_at AS completed_at + confirmed_at AS completed_at, + created_at AS order_ts FROM splice_outgoing_payments UNION ALL SELECT 4 AS type, id AS id, created_at AS created_at, - confirmed_at AS completed_at + confirmed_at AS completed_at, + created_at AS order_ts FROM channel_close_outgoing_payments UNION ALL SELECT 5 AS type, id AS id, created_at AS created_at, - confirmed_at AS completed_at + confirmed_at AS completed_at, + created_at AS order_ts FROM splice_cpfp_outgoing_payments UNION ALL SELECT 6 AS type, id AS id, created_at AS created_at, - locked_at AS completed_at + locked_at AS completed_at, + created_at AS order_ts FROM inbound_liquidity_outgoing_payments UNION ALL SELECT 1 AS type, lower(hex(payment_hash)) AS id, created_at AS created_at, - received_at AS completed_at + received_at AS completed_at, + received_at AS order_ts FROM incoming_payments WHERE incoming_payments.received_at IS NOT NULL AND incoming_payments.received_with_blob IS NOT NULL @@ -57,7 +63,7 @@ UNION ALL LEFT OUTER JOIN payments_metadata ON payments_metadata.type = combined_payments.type AND payments_metadata.id = combined_payments.id -ORDER BY combined_payments.created_at DESC +ORDER BY COALESCE(combined_payments.order_ts, combined_payments.created_at) DESC LIMIT :limit OFFSET :offset; listAllPaymentsCount: From ce3618e99f4025c09ca6cb2ae4788e507c959b96 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:39:39 +0200 Subject: [PATCH 05/25] (android) Use lightning-kmp method to determine whether liquidity fee is shown or not See https://github.com/ACINQ/lightning-kmp/pull/706 Methods feePaidFromChannelBalance and feePaidFromFutureHtlc have been added to check whether an inbound liquidity purchase is paid from balance or future htlc. This is used to know when to display a liquidity fee or not, and to get the liquidity fee for an incoming payment. Also fixed some UI issues with descriptions and improved txid and channel id buttons in technical screen. --- .../phoenix/android/components/Buttons.kt | 9 +- .../details/PaymentDetailsTechnicalView.kt | 116 ++++++++++-------- .../android/payments/details/PaymentLine.kt | 11 +- .../details/splash/PaymentSplashView.kt | 50 ++++---- .../details/splash/SplashChannelClose.kt | 4 +- .../payments/details/splash/SplashIncoming.kt | 44 ++++--- .../details/splash/SplashLightningOut.kt | 4 +- .../details/splash/SplashLiquidityPurchase.kt | 92 +++++++++----- .../details/splash/SplashSpliceOut.kt | 3 +- .../details/splash/SplashSpliceOutCpfp.kt | 3 +- .../payments/history/CsvExportViewModel.kt | 5 +- .../settings/channels/ChannelDetailsView.kt | 4 +- .../settings/walletinfo/SwapInRefundView.kt | 8 +- .../acinq/phoenix/android/utils/extensions.kt | 60 ++++++--- 14 files changed, 248 insertions(+), 165 deletions(-) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Buttons.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Buttons.kt index e5ad26176..abd4de427 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Buttons.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/Buttons.kt @@ -336,11 +336,13 @@ fun Button( } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun Clickable( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, + onLongClick: (() -> Unit)? = null, textStyle: TextStyle = MaterialTheme.typography.button, backgroundColor: Color = Color.Unspecified, // transparent by default! shape: Shape = RectangleShape, @@ -360,8 +362,11 @@ fun Clickable( elevation = 0.dp, modifier = modifier .clip(shape) - .clickable( + .combinedClickable( onClick = onClick, + onLongClick = onLongClick, + onLongClickLabel = null, + onDoubleClick = null, enabled = enabled, role = Role.Button, onClickLabel = clickDescription, @@ -422,7 +427,7 @@ fun AddressLinkButton( } @Composable -fun TransactionLinkButton( +fun InlineTransactionLink( modifier: Modifier = Modifier, txId: TxId, ) { 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 52c83fcaf..99914036b 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 @@ -33,6 +33,7 @@ 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.bitcoin.TxId import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.db.* import fr.acinq.lightning.payment.Bolt11Invoice @@ -46,6 +47,7 @@ import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.android.LocalBitcoinUnit import fr.acinq.phoenix.android.LocalFiatCurrency import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.Screen import fr.acinq.phoenix.android.business import fr.acinq.phoenix.android.components.AmountView import fr.acinq.phoenix.android.components.Card @@ -53,7 +55,9 @@ import fr.acinq.phoenix.android.components.CardHeader import fr.acinq.phoenix.android.components.Clickable import fr.acinq.phoenix.android.components.InlineButton import fr.acinq.phoenix.android.components.TextWithIcon -import fr.acinq.phoenix.android.components.TransactionLinkButton +import fr.acinq.phoenix.android.components.InlineTransactionLink +import fr.acinq.phoenix.android.components.openLink +import fr.acinq.phoenix.android.components.txUrl import fr.acinq.phoenix.android.fiatRate import fr.acinq.phoenix.android.navController import fr.acinq.phoenix.android.navigateToPaymentDetails @@ -62,10 +66,10 @@ import fr.acinq.phoenix.android.utils.Converter.toFiat import fr.acinq.phoenix.android.utils.Converter.toPrettyString import fr.acinq.phoenix.android.utils.MSatDisplayPolicy import fr.acinq.phoenix.android.utils.copyToClipboard +import fr.acinq.phoenix.android.utils.mutedBgColor import fr.acinq.phoenix.data.ExchangeRate import fr.acinq.phoenix.data.WalletPaymentId import fr.acinq.phoenix.data.WalletPaymentInfo -import fr.acinq.phoenix.data.walletPaymentId import fr.acinq.phoenix.utils.extensions.amountFeeCredit @@ -349,18 +353,12 @@ private fun DetailsForLightningOutgoingPayment( private fun DetailsForChannelClose( payment: ChannelCloseOutgoingPayment ) { - TechnicalRowSelectable( - label = stringResource(id = R.string.paymentdetails_channel_id_label), - value = payment.channelId.toHex() - ) + ChannelIdRow(payment.channelId) TechnicalRowSelectable( label = stringResource(id = R.string.paymentdetails_bitcoin_address_label), value = payment.address ) - TechnicalRow( - label = stringResource(id = R.string.paymentdetails_tx_id_label), - content = { TransactionLinkButton(txId = payment.txId) } - ) + TransactionRow(payment.txId) TechnicalRowSelectable( label = stringResource(id = R.string.paymentdetails_closing_type_label), value = when (payment.closingType) { @@ -377,24 +375,15 @@ private fun DetailsForChannelClose( private fun DetailsForCpfp( payment: SpliceCpfpOutgoingPayment ) { - TechnicalRow( - label = stringResource(id = R.string.paymentdetails_tx_id_label), - content = { TransactionLinkButton(txId = payment.txId) } - ) + TransactionRow(payment.txId) } @Composable private fun DetailsForInboundLiquidity( payment: InboundLiquidityOutgoingPayment ) { - TechnicalRow( - label = stringResource(id = R.string.paymentdetails_tx_id_label), - content = { TransactionLinkButton(txId = payment.txId) } - ) - TechnicalRowSelectable( - label = stringResource(id = R.string.paymentdetails_channel_id_label), - value = payment.channelId.toHex(), - ) + TransactionRow(payment.txId) + ChannelIdRow(channelId = payment.channelId) TechnicalRow(label = "Purchase Type") { Text(text = when (payment.purchase) { is LiquidityAds.Purchase.Standard -> "Standard" @@ -432,19 +421,12 @@ private fun ListLinksOfPaymentHashes(paymentHashes: List) { private fun DetailsForSpliceOut( payment: SpliceOutgoingPayment ) { - TechnicalRowSelectable( - label = stringResource(id = R.string.paymentdetails_splice_out_channel_label), - value = payment.channelId.toHex() - ) + ChannelIdRow(channelId = payment.channelId, label = stringResource(id = R.string.paymentdetails_splice_out_channel_label)) TechnicalRowSelectable( label = stringResource(id = R.string.paymentdetails_bitcoin_address_label), value = payment.address ) - TechnicalRow( - label = stringResource(id = R.string.paymentdetails_tx_id_label), - content = { TransactionLinkButton(txId = payment.txId) } - ) - + TransactionRow(payment.txId) } @Composable @@ -468,7 +450,7 @@ private fun DetailsForIncoming( Row { Text(text = stringResource(id = R.string.paymentdetails_dualswapin_tx_value, index + 1)) Spacer(modifier = Modifier.width(4.dp)) - TransactionLinkButton(txId = outpoint.txid) + InlineTransactionLink(txId = outpoint.txid) } } } @@ -488,9 +470,7 @@ private fun ReceivedWithLightning( Text(text = stringResource(id = R.string.paymentdetails_received_with_lightning)) } if (receivedWith.channelId != ByteVector32.Zeroes) { - TechnicalRow(label = stringResource(id = R.string.paymentdetails_channel_id_label)) { - Text(text = receivedWith.channelId.toHex()) - } + ChannelIdRow(receivedWith.channelId) } TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_received_label), amount = receivedWith.amountReceived, rateThen = rateThen) } @@ -505,14 +485,9 @@ private fun ReceivedWithNewChannel( } val channelId = receivedWith.channelId if (channelId != ByteVector32.Zeroes) { // backward compat - TechnicalRow(label = stringResource(id = R.string.paymentdetails_channel_id_label)) { - Text(text = channelId.toHex()) - } + ChannelIdRow(channelId) } - TechnicalRow( - label = stringResource(id = R.string.paymentdetails_tx_id_label), - content = { TransactionLinkButton(txId = receivedWith.txId) } - ) + TransactionRow(receivedWith.txId) TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_received_label), amount = receivedWith.amountReceived, rateThen = rateThen) } @@ -526,14 +501,9 @@ private fun ReceivedWithSpliceIn( } val channelId = receivedWith.channelId if (channelId != ByteVector32.Zeroes) { // backward compat - TechnicalRow(label = stringResource(id = R.string.paymentdetails_channel_id_label)) { - Text(text = channelId.toHex()) - } + ChannelIdRow(channelId) } - TechnicalRow( - label = stringResource(id = R.string.paymentdetails_tx_id_label), - content = { TransactionLinkButton(txId = receivedWith.txId) } - ) + TransactionRow(receivedWith.txId) TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_received_label), amount = receivedWith.amountReceived, rateThen = rateThen) } @@ -738,3 +708,51 @@ private fun TechnicalRowWithCopy(label: String, value: String) { } } } + +@Composable +private fun TechnicalRowClickable( + label: String, + onClick: () -> Unit, + onLongClick: (() -> Unit)? = null, + content: @Composable () -> Unit, +) { + TechnicalRow(label = label) { + Clickable( + onClick = onClick, + onLongClick = onLongClick, + modifier = Modifier.fillMaxWidth().offset(x = (-8).dp), + shape = RoundedCornerShape(12.dp), + backgroundColor = mutedBgColor, + ) { + Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)) { + content() + } + } + } +} + +@Composable +private fun TransactionRow(txId: TxId) { + val context = LocalContext.current + val link = txUrl(txId = txId) + TechnicalRowClickable( + label = stringResource(id = R.string.paymentdetails_tx_id_label), + onClick = { openLink(context, link) }, + onLongClick = { copyToClipboard(context, txId.toString()) } + ) { + TextWithIcon(text = txId.toString(), icon = R.drawable.ic_external_link, maxLines = 1, textOverflow = TextOverflow.Ellipsis, space = 4.dp) + } +} + +@Composable +private fun ChannelIdRow(channelId: ByteVector32, label: String = stringResource(id = R.string.paymentdetails_channel_id_label)) { + val context = LocalContext.current + val navController = navController + TechnicalRowClickable( + label = label, + onClick = { navController.navigate("${Screen.ChannelDetails.route}?id=${channelId.toHex()}") }, + onLongClick = { copyToClipboard(context, channelId.toHex()) } + ) { + TextWithIcon(text = channelId.toHex(), icon = R.drawable.ic_zap, maxLines = 1, textOverflow = TextOverflow.Ellipsis, space = 4.dp) + } +} diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt index 084074887..3013d6bbd 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt @@ -34,24 +34,24 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment import fr.acinq.lightning.db.IncomingPayment import fr.acinq.lightning.db.OutgoingPayment import fr.acinq.lightning.db.SpliceCpfpOutgoingPayment import fr.acinq.lightning.db.SpliceOutgoingPayment import fr.acinq.lightning.db.WalletPayment +import fr.acinq.lightning.utils.sat import fr.acinq.phoenix.android.R import fr.acinq.phoenix.android.business import fr.acinq.phoenix.android.components.AmountView @@ -142,7 +142,9 @@ fun PaymentLine( Row { PaymentDescription(paymentInfo = paymentInfo, contactInfo = contactInfo, modifier = Modifier.weight(1.0f)) Spacer(modifier = Modifier.width(16.dp)) - if (payment.state() != WalletPaymentState.Failure) { + val showPayment = payment.state() != WalletPaymentState.Failure + && !(payment is InboundLiquidityOutgoingPayment && payment.feePaidFromChannelBalance.total == 0.sat) + if (showPayment) { val isOutgoing = payment is OutgoingPayment if (isAmountRedacted) { Text(text = "****") @@ -176,7 +178,6 @@ private fun PaymentDescription( contactInfo: ContactInfo?, modifier: Modifier = Modifier ) { - val context = LocalContext.current val payment = paymentInfo.payment val metadata = paymentInfo.metadata val peer by business.peerManager.peerState.collectAsState() @@ -190,7 +191,7 @@ private fun PaymentDescription( if (contactInfo != null) offerMetadata.payerNote else null } ?: payment.outgoingInvoiceRequest()?.payerNote - ?: payment.smartDescription(context) + ?: payment.smartDescription() } Text( diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt index 882ef195e..34571fae0 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt @@ -49,6 +49,7 @@ import fr.acinq.lightning.db.LightningOutgoingPayment import fr.acinq.lightning.db.OutgoingPayment import fr.acinq.lightning.db.SpliceCpfpOutgoingPayment import fr.acinq.lightning.db.SpliceOutgoingPayment +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.android.R import fr.acinq.phoenix.android.components.AmountView import fr.acinq.phoenix.android.components.BorderButton @@ -82,30 +83,32 @@ fun PaymentDetailsSplashView( header = { DefaultScreenHeader(onBackClick = onBackClick) }, topContent = { PaymentStatus(data.payment, fromEvent, onCpfpSuccess = onBackClick) } ) { - AmountView( - amount = when (payment) { - is InboundLiquidityOutgoingPayment -> payment.amount - is OutgoingPayment -> payment.amount - payment.fees - is IncomingPayment -> payment.amount - }, - amountTextStyle = MaterialTheme.typography.body1.copy(fontSize = 30.sp), - separatorSpace = 4.dp, - prefix = stringResource(id = if (payment is OutgoingPayment) R.string.paymentline_prefix_sent else R.string.paymentline_prefix_received) - - ) - - Spacer(modifier = Modifier.height(36.dp)) - PrimarySeparator( - height = 6.dp, - color = when (payment.state()) { - WalletPaymentState.Failure -> negativeColor - WalletPaymentState.SuccessOffChain, WalletPaymentState.SuccessOnChain -> positiveColor - else -> mutedBgColor - } - ) + if (payment is InboundLiquidityOutgoingPayment && payment.purchase.paymentDetails is LiquidityAds.PaymentDetails.FromFutureHtlc) { + Unit + } else { + AmountView( + amount = when (payment) { + is InboundLiquidityOutgoingPayment -> payment.amount + is OutgoingPayment -> payment.amount - payment.fees + is IncomingPayment -> payment.amount + }, + amountTextStyle = MaterialTheme.typography.body1.copy(fontSize = 30.sp), + separatorSpace = 4.dp, + prefix = stringResource(id = if (payment is OutgoingPayment) R.string.paymentline_prefix_sent else R.string.paymentline_prefix_received) + ) + Spacer(modifier = Modifier.height(36.dp)) + PrimarySeparator( + height = 6.dp, + color = when (payment.state()) { + WalletPaymentState.Failure -> negativeColor + WalletPaymentState.SuccessOffChain, WalletPaymentState.SuccessOnChain -> positiveColor + else -> mutedBgColor + } + ) + } Spacer(modifier = Modifier.height(36.dp)) - when (val payment = data.payment) { + when (payment) { is IncomingPayment -> SplashIncoming(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) is LightningOutgoingPayment -> SplashLightningOutgoing(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) is ChannelCloseOutgoingPayment -> SplashChannelClose(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) @@ -136,7 +139,6 @@ fun SplashDescription( var showEditDescriptionDialog by remember { mutableStateOf(false) } Spacer(modifier = Modifier.height(8.dp)) - if (!(description.isNullOrBlank() && !userDescription.isNullOrBlank())) { SplashLabelRow(label = stringResource(id = R.string.paymentdetails_desc_label)) { if (description.isNullOrBlank()) { @@ -149,7 +151,7 @@ fun SplashDescription( } } } - + Spacer(modifier = Modifier.height(5.dp)) SplashLabelRow(label = if (userDescription.isNullOrBlank()) "" else "Note") { SplashClickableContent(onClick = { showEditDescriptionDialog = true }) { if (!userDescription.isNullOrBlank()) { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt index 8c9cd8a25..a9fa90d9e 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import fr.acinq.lightning.db.ChannelCloseOutgoingPayment @@ -46,14 +45,13 @@ fun SplashChannelClose( metadata: WalletPaymentMetadata, onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, ) { - val context = LocalContext.current val peer by business.peerManager.peerState.collectAsState() val isLegacyMigration = payment.isLegacyMigration(metadata, peer) val description = when (isLegacyMigration) { null -> stringResource(id = R.string.paymentdetails_desc_closing_channel) // not sure yet, but we still know it's a closing true -> stringResource(id = R.string.paymentdetails_desc_legacy_migration) - false -> payment.smartDescription(context) + false -> payment.smartDescription() } SplashDescription( diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt index 2bd160c65..4a7a926ce 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt @@ -22,16 +22,19 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import fr.acinq.bitcoin.PublicKey import fr.acinq.lightning.db.IncomingPayment import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.sum +import fr.acinq.lightning.utils.toMilliSatoshi import fr.acinq.phoenix.android.LocalBitcoinUnit import fr.acinq.phoenix.android.R import fr.acinq.phoenix.android.business @@ -52,8 +55,6 @@ fun SplashIncoming( metadata: WalletPaymentMetadata, onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, ) { - val context = LocalContext.current - payment.incomingOfferMetadata()?.let { meta -> meta.payerNote?.takeIf { it.isNotBlank() }?.let { OfferPayerNote(payerNote = it) @@ -63,7 +64,7 @@ fun SplashIncoming( } SplashDescription( - description = payment.smartDescription(context = context), + description = payment.smartDescription(), userDescription = metadata.userDescription, paymentId = payment.walletPaymentId(), onMetadataDescriptionUpdate = onMetadataDescriptionUpdate, @@ -115,11 +116,24 @@ private fun SplashFee( payment: IncomingPayment ) { val btcUnit = LocalBitcoinUnit.current - val receivedWithNewChannel = payment.received?.receivedWith?.filterIsInstance() ?: emptyList() - val receivedWithSpliceIn = payment.received?.receivedWith?.filterIsInstance() ?: emptyList() - if ((receivedWithNewChannel + receivedWithSpliceIn).isNotEmpty()) { - val serviceFee = receivedWithNewChannel.map { it.serviceFee }.sum() + receivedWithSpliceIn.map { it.serviceFee }.sum() - val fundingFee = receivedWithNewChannel.map { it.miningFee }.sum() + receivedWithSpliceIn.map { it.miningFee }.sum() + val receivedWithOnChain = remember(payment) { payment.received?.receivedWith?.filterIsInstance() ?: emptyList() } + val receivedWithLightning = remember(payment) { payment.received?.receivedWith?.filterIsInstance() ?: emptyList() } + + if (receivedWithOnChain.isNotEmpty() || receivedWithLightning.isNotEmpty()) { + + val paymentsManager = business.paymentsManager + val txIds = remember(receivedWithLightning) { receivedWithLightning.mapNotNull { it.fundingFee?.fundingTxId } } + val relatedLiquidityPayments by produceState(initialValue = emptyList()) { + value = txIds.mapNotNull { paymentsManager.getLiquidityPurchaseForTxId(it) } + } + + val serviceFee = remember(receivedWithOnChain, relatedLiquidityPayments) { + receivedWithOnChain.map { it.serviceFee }.sum() + relatedLiquidityPayments.map { it.feePaidFromFutureHtlc.serviceFee.toMilliSatoshi() }.sum() + } + val miningFee = remember(receivedWithOnChain, relatedLiquidityPayments) { + receivedWithOnChain.map { it.miningFee }.sum() + relatedLiquidityPayments.map { it.feePaidFromFutureHtlc.miningFee }.sum() + } + Spacer(modifier = Modifier.height(8.dp)) if (serviceFee > 0.msat) { SplashLabelRow( @@ -131,11 +145,13 @@ private fun SplashFee( Spacer(modifier = Modifier.height(8.dp)) } - SplashLabelRow( - label = stringResource(id = R.string.paymentdetails_funding_fees_label), - helpMessage = stringResource(R.string.paymentdetails_funding_fees_desc) - ) { - Text(text = fundingFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.HIDE)) + if (miningFee > 0.sat) { + SplashLabelRow( + label = stringResource(id = R.string.paymentdetails_funding_fees_label), + helpMessage = stringResource(R.string.paymentdetails_funding_fees_desc) + ) { + Text(text = miningFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.HIDE)) + } } } } \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt index cde352f3c..30811e798 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt @@ -61,8 +61,6 @@ fun SplashLightningOutgoing( metadata: WalletPaymentMetadata, onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, ) { - val context = LocalContext.current - metadata.lnurl?.let { lnurlMeta -> LnurlPayInfoView(payment, lnurlMeta) } @@ -72,7 +70,7 @@ fun SplashLightningOutgoing( } SplashDescription( - description = payment.smartDescription(context), + description = payment.smartDescription(), userDescription = metadata.userDescription, paymentId = payment.walletPaymentId(), onMetadataDescriptionUpdate = onMetadataDescriptionUpdate diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt index 6980cb375..2dde1e294 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt @@ -36,15 +36,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import fr.acinq.bitcoin.ByteVector32 import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment +import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.android.LocalBitcoinUnit import fr.acinq.phoenix.android.R @@ -53,6 +52,7 @@ import fr.acinq.phoenix.android.business import fr.acinq.phoenix.android.components.BorderButton import fr.acinq.phoenix.android.components.BottomSheetDialog import fr.acinq.phoenix.android.components.Clickable +import fr.acinq.phoenix.android.components.SplashClickableContent import fr.acinq.phoenix.android.components.SplashLabelRow import fr.acinq.phoenix.android.components.TextWithIcon import fr.acinq.phoenix.android.navController @@ -69,6 +69,8 @@ import fr.acinq.phoenix.data.WalletPaymentFetchOptions import fr.acinq.phoenix.data.WalletPaymentId import fr.acinq.phoenix.data.WalletPaymentInfo import fr.acinq.phoenix.data.WalletPaymentMetadata +import fr.acinq.phoenix.utils.extensions.relatedPaymentIds +import kotlinx.coroutines.launch @Composable fun SplashLiquidityPurchase( @@ -85,7 +87,7 @@ fun SplashLiquidityPurchase( // However, swap-ins do not **yet** request additional liquidity, so **for now** we can make a safe approximation. // Eventually, once swap-ins are upgraded to request liquidity, this will have to be fixed, . if (payment.purchase.paymentDetails !is LiquidityAds.PaymentDetails.FromChannelBalance) { - AutoLiquidityDetails(purchase = payment.purchase) + AutoLiquidityDetails(payment) } } @@ -123,20 +125,37 @@ private fun SplashFee( payment: InboundLiquidityOutgoingPayment ) { val btcUnit = LocalBitcoinUnit.current - SplashLabelRow( - label = stringResource(id = R.string.paymentdetails_liquidity_miner_fee_label), - helpMessage = stringResource(id = R.string.paymentdetails_liquidity_miner_fee_help) - ) { - Text(text = payment.miningFees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) - } - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow( - label = stringResource(id = R.string.paymentdetails_liquidity_service_fee_label), - helpMessage = stringResource(id = R.string.paymentdetails_liquidity_service_fee_help) - ) { - Text(text = payment.purchase.fees.serviceFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) - if (payment.purchase is LiquidityAds.Purchase.WithFeeCredit) { - Text(text = "Paid with fee credit") + + // if the fee paid from channel balance is 0, it means this is a liquidity purchase for a new channel whose fee are paid + // by a future htlc. In this case, for UX reasons, we don't show the fees here but instead link to the payment whose htlcs + // paid the fees. + if (payment.feePaidFromChannelBalance.total == 0.sat) { + SplashLabelRow(label = "Fees") { + val navController = navController + payment.relatedPaymentIds().forEach { + SplashClickableContent(onClick = { navigateToPaymentDetails(navController, it, isFromEvent = false) }) { + TextWithIcon(text = "See related payment", icon = R.drawable.ic_arrow_next) + } + } + } + } else { + val miningFee = payment.feePaidFromChannelBalance.miningFee + val serviceFee = payment.feePaidFromChannelBalance.serviceFee + SplashLabelRow( + label = stringResource(id = R.string.paymentdetails_liquidity_miner_fee_label), + helpMessage = stringResource(id = R.string.paymentdetails_liquidity_miner_fee_help) + ) { + Text(text = miningFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + } + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow( + label = stringResource(id = R.string.paymentdetails_liquidity_service_fee_label), + helpMessage = stringResource(id = R.string.paymentdetails_liquidity_service_fee_help) + ) { + Text(text = serviceFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + if (payment.purchase is LiquidityAds.Purchase.WithFeeCredit) { + Text(text = "Paid with fee credit") + } } } } @@ -155,7 +174,7 @@ private fun SplashPurchase( @OptIn(ExperimentalFoundationApi::class) @Composable private fun AutoLiquidityDetails( - purchase: LiquidityAds.Purchase + payment: InboundLiquidityOutgoingPayment ) { val navController = navController var showPaymentsDialog by remember { mutableStateOf(false) } @@ -192,11 +211,23 @@ private fun AutoLiquidityDetails( Spacer(modifier = Modifier.height(8.dp)) Text(text = "This operation was necessary to accommodate new incoming payments.", modifier = Modifier.padding(horizontal = 24.dp)) Spacer(modifier = Modifier.height(12.dp)) - Text( - text = "Swipe right to see these payments.", - style = MaterialTheme.typography.caption.copy(fontSize = 14.sp), - modifier = Modifier.padding(horizontal = 24.dp) - ) + Spacer(modifier = Modifier.height(4.dp)) + val scope = rememberCoroutineScope() + Clickable(onClick = { scope.launch { pagerState.animateScrollToPage(1) } }, modifier = Modifier + .padding(horizontal = 12.dp) + .align(Alignment.CenterHorizontally), shape = RoundedCornerShape(10.dp)) { + Column(modifier = Modifier.padding(12.dp)) { + TextWithIcon( + text = "See related payments", + icon = R.drawable.ic_arrow_next, + ) + } + } +// Text( +// text = "Swipe right to see these payments.", +// style = MaterialTheme.typography.caption.copy(fontSize = 14.sp), +// modifier = Modifier.padding(horizontal = 24.dp) +// ) Spacer(modifier = Modifier.height(32.dp)) Text( @@ -235,12 +266,7 @@ private fun AutoLiquidityDetails( Column(modifier = Modifier.fillMaxSize()) { Text(text = "Operation triggered by...", style = MaterialTheme.typography.h4, modifier = Modifier.padding(horizontal = 16.dp)) Spacer(modifier = Modifier.height(8.dp)) - val paymentHashes = when (val details = purchase.paymentDetails) { - is LiquidityAds.PaymentDetails.FromFutureHtlc -> details.paymentHashes - is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> details.paymentHashes - else -> emptyList() - } - TriggeredBy(paymentHashes = paymentHashes) + TriggeredBy(ids = payment.relatedPaymentIds()) } } } @@ -250,12 +276,10 @@ private fun AutoLiquidityDetails( } @Composable -private fun TriggeredBy(paymentHashes: List) { - val context = LocalContext.current +private fun TriggeredBy(ids: List) { val navController = navController val paymentsManager = business.paymentsManager - paymentHashes.forEach { paymentHash -> - val id = remember(paymentHash) { WalletPaymentId.IncomingPaymentId(paymentHash) } + ids.forEach { id -> val paymentInfo by produceState(initialValue = null) { value = paymentsManager.getPayment(id = id, options = WalletPaymentFetchOptions.None) } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOut.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOut.kt index 8fd05bfb4..bbe8b431c 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOut.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOut.kt @@ -42,9 +42,8 @@ fun SplashSpliceOut( metadata: WalletPaymentMetadata, onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, ) { - val context = LocalContext.current SplashDescription( - description = payment.smartDescription(context), + description = payment.smartDescription(), userDescription = metadata.userDescription, paymentId = payment.walletPaymentId(), onMetadataDescriptionUpdate = onMetadataDescriptionUpdate diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOutCpfp.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOutCpfp.kt index 237047044..d34fa57d4 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOutCpfp.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashSpliceOutCpfp.kt @@ -42,9 +42,8 @@ fun SplashSpliceOutCpfp( metadata: WalletPaymentMetadata, onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, ) { - val context = LocalContext.current SplashDescription( - description = payment.smartDescription(context), + description = payment.smartDescription(), userDescription = metadata.userDescription, paymentId = payment.walletPaymentId(), onMetadataDescriptionUpdate = onMetadataDescriptionUpdate diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/CsvExportViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/CsvExportViewModel.kt index df6c5e878..99176b661 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/CsvExportViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/CsvExportViewModel.kt @@ -29,9 +29,8 @@ import androidx.lifecycle.viewModelScope import fr.acinq.lightning.utils.currentTimestampMillis import fr.acinq.phoenix.android.BuildConfig import fr.acinq.phoenix.android.utils.Converter.toAbsoluteDateTimeString -import fr.acinq.phoenix.android.utils.smartDescription +import fr.acinq.phoenix.android.utils.basicDescription import fr.acinq.phoenix.data.WalletPaymentFetchOptions -import fr.acinq.phoenix.db.SqlitePaymentsDb import fr.acinq.phoenix.managers.DatabaseManager import fr.acinq.phoenix.managers.PaymentsFetcher import fr.acinq.phoenix.managers.PeerManager @@ -109,7 +108,7 @@ class CsvExportViewModel( ).map { paymentRow -> paymentsFetcher.getPayment(paymentRow, WalletPaymentFetchOptions.All)?.let { info -> val descriptions = listOf( - info.payment.smartDescription(context), + info.payment.basicDescription(), info.metadata.userDescription, info.metadata.lnurl?.pay?.metadata?.longDesc ).mapNotNull { it.takeIf { !it.isNullOrBlank() } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ChannelDetailsView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ChannelDetailsView.kt index 0a644d62a..4fd54aee2 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ChannelDetailsView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ChannelDetailsView.kt @@ -68,7 +68,7 @@ import fr.acinq.phoenix.android.components.PhoenixIcon import fr.acinq.phoenix.android.components.ProgressView import fr.acinq.phoenix.android.components.settings.Setting import fr.acinq.phoenix.android.components.settings.SettingWithCopy -import fr.acinq.phoenix.android.components.TransactionLinkButton +import fr.acinq.phoenix.android.components.InlineTransactionLink import fr.acinq.phoenix.android.navController import fr.acinq.phoenix.android.navigateToPaymentDetails import fr.acinq.phoenix.android.utils.Converter.toPrettyString @@ -188,7 +188,7 @@ private fun CommitmentDetailsView( Row { Text(text = stringResource(id = R.string.channeldetails_commitment_funding_tx_id), modifier = Modifier.alignByBaseline()) Spacer(modifier = Modifier.width(4.dp)) - TransactionLinkButton(txId = commitment.fundingTxId, modifier = Modifier.alignByBaseline()) + InlineTransactionLink(txId = commitment.fundingTxId, modifier = Modifier.alignByBaseline()) } Row { Text(text = stringResource(id = R.string.channeldetails_commitment_balance)) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInRefundView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInRefundView.kt index 0d4971a8e..121aa910b 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInRefundView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInRefundView.kt @@ -23,12 +23,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -49,7 +47,6 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import com.journeyapps.barcodescanner.DecoratedBarcodeView import fr.acinq.bitcoin.Satoshi @@ -71,7 +68,7 @@ import fr.acinq.phoenix.android.components.FeerateSlider import fr.acinq.phoenix.android.components.ProgressView import fr.acinq.phoenix.android.components.SplashLabelRow import fr.acinq.phoenix.android.components.TextInput -import fr.acinq.phoenix.android.components.TransactionLinkButton +import fr.acinq.phoenix.android.components.InlineTransactionLink import fr.acinq.phoenix.android.components.feedback.ErrorMessage import fr.acinq.phoenix.android.components.feedback.SuccessMessage import fr.acinq.phoenix.android.fiatRate @@ -79,7 +76,6 @@ import fr.acinq.phoenix.android.payments.CameraPermissionsView import fr.acinq.phoenix.android.payments.ScannerView import fr.acinq.phoenix.android.utils.Converter.toPrettyString import fr.acinq.phoenix.android.utils.annotatedStringResource -import fr.acinq.phoenix.android.utils.copyToClipboard import fr.acinq.phoenix.managers.PeerManager import fr.acinq.phoenix.utils.Parser @@ -276,7 +272,7 @@ private fun AvailableForRefundView( Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { Text(text = stringResource(id = R.string.swapinrefund_success_details)) Spacer(modifier = Modifier.height(12.dp)) - TransactionLinkButton(txId = currentState.tx.txid) + InlineTransactionLink(txId = currentState.tx.txid) } } } 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 ca81a3e57..eb07ea33f 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 @@ -122,24 +122,34 @@ fun UserTheme.label(): String { fun Connection.CLOSED.isBadCertificate() = this.reason?.cause is CertificateException -fun LightningOutgoingPayment.smartDescription(context: Context): String? = when (val details = this.details) { +@Composable +fun LightningOutgoingPayment.smartDescription(): String? = when (val details = this.details) { is LightningOutgoingPayment.Details.Normal -> details.paymentRequest.desc - is LightningOutgoingPayment.Details.SwapOut -> context.getString(R.string.paymentdetails_desc_swapout, details.address) + is LightningOutgoingPayment.Details.SwapOut -> stringResource(id = R.string.paymentdetails_desc_swapout, details.address) is LightningOutgoingPayment.Details.Blinded -> details.paymentRequest.description }?.takeIf { it.isNotBlank() } -fun SpliceOutgoingPayment.smartDescription(context: Context): String = context.getString(R.string.paymentdetails_desc_splice_out) -fun SpliceCpfpOutgoingPayment.smartDescription(context: Context): String = context.getString(R.string.paymentdetails_desc_cpfp) -fun ChannelCloseOutgoingPayment.smartDescription(context: Context): String = context.getString(R.string.paymentdetails_desc_closing_channel) -fun InboundLiquidityOutgoingPayment.smartDescription(context: Context): String = when (purchase.paymentDetails) { + +@Composable +fun SpliceOutgoingPayment.smartDescription(): String = stringResource(id = R.string.paymentdetails_desc_splice_out) + +@Composable +fun SpliceCpfpOutgoingPayment.smartDescription(): String = stringResource(id = R.string.paymentdetails_desc_cpfp) + +@Composable +fun ChannelCloseOutgoingPayment.smartDescription(): String = stringResource(id = R.string.paymentdetails_desc_closing_channel) + +@Composable +fun InboundLiquidityOutgoingPayment.smartDescription(): String = when (purchase.paymentDetails) { // manual inbound liquidity LiquidityAds.PaymentDetails.FromChannelBalance -> "Manual liquidity" // context.getString(R.string.paymentdetails_desc_inbound_liquidity, purchase.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)) // pay-to-open/pay-to-splice else -> "Automated liquidity" } -fun IncomingPayment.smartDescription(context: Context) : String? = when (val origin = this.origin) { +@Composable +fun IncomingPayment.smartDescription() : String? = when (val origin = this.origin) { is IncomingPayment.Origin.Invoice -> origin.paymentRequest.description - is IncomingPayment.Origin.SwapIn, is IncomingPayment.Origin.OnChain -> context.getString(R.string.paymentdetails_desc_swapin) + is IncomingPayment.Origin.SwapIn, is IncomingPayment.Origin.OnChain -> stringResource(id = R.string.paymentdetails_desc_swapin) is IncomingPayment.Origin.Offer -> null }?.takeIf { it.isNotBlank() } @@ -149,11 +159,29 @@ fun IncomingPayment.smartDescription(context: Context) : String? = when (val ori * For example, a payment closing a channel has no description, and it's up to us to create one. Others like a LN * payment with an invoice do have a description baked in, and that's what is returned. */ -fun WalletPayment.smartDescription(context: Context): String? = when (this) { - is LightningOutgoingPayment -> smartDescription(context) - is IncomingPayment -> smartDescription(context) - is SpliceOutgoingPayment -> smartDescription(context) - is ChannelCloseOutgoingPayment -> smartDescription(context) - is SpliceCpfpOutgoingPayment -> smartDescription(context) - is InboundLiquidityOutgoingPayment -> smartDescription(context) -} \ No newline at end of file +@Composable +fun WalletPayment.smartDescription(): String? = when (this) { + is LightningOutgoingPayment -> smartDescription() + is IncomingPayment -> smartDescription() + is ChannelCloseOutgoingPayment -> smartDescription() + is SpliceOutgoingPayment -> smartDescription() + is SpliceCpfpOutgoingPayment -> smartDescription() + is InboundLiquidityOutgoingPayment -> smartDescription() +} + +fun WalletPayment.basicDescription(): String? = when (this) { + is LightningOutgoingPayment -> when (val details = this.details) { + is LightningOutgoingPayment.Details.Normal -> details.paymentRequest.desc + is LightningOutgoingPayment.Details.SwapOut -> null + is LightningOutgoingPayment.Details.Blinded -> details.paymentRequest.description + } + is IncomingPayment -> when (val origin = this.origin) { + is IncomingPayment.Origin.Invoice -> origin.paymentRequest.description + is IncomingPayment.Origin.SwapIn, is IncomingPayment.Origin.OnChain -> null + is IncomingPayment.Origin.Offer -> null + }?.takeIf { it.isNotBlank() } + is ChannelCloseOutgoingPayment -> null + is SpliceOutgoingPayment -> null + is SpliceCpfpOutgoingPayment -> null + is InboundLiquidityOutgoingPayment -> null +}?.takeIf { it.isNotBlank() } \ No newline at end of file From b517e681015d1c9e200b4ca8eb9b1040d7a7b51f Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:12:30 +0200 Subject: [PATCH 06/25] Fix testnet3 chain in explorer links --- .../payments/details/PaymentDetailsTechnicalView.kt | 3 +++ .../kotlin/fr/acinq/phoenix/android/utils/extensions.kt | 3 ++- .../kotlin/fr.acinq.phoenix/utils/BlockchainExplorer.kt | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) 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 99914036b..1d40497b4 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 @@ -472,6 +472,9 @@ private fun ReceivedWithLightning( if (receivedWith.channelId != ByteVector32.Zeroes) { ChannelIdRow(receivedWith.channelId) } + receivedWith.fundingFee?.let { + TransactionRow(it.fundingTxId) + } TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_amount_received_label), amount = receivedWith.amountReceived, rateThen = rateThen) } 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 eb07ea33f..0f30a3ff4 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 @@ -26,6 +26,7 @@ import fr.acinq.lightning.utils.Connection import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.android.* import fr.acinq.phoenix.android.R +import fr.acinq.phoenix.android.utils.Converter.toPrettyString import fr.acinq.phoenix.data.BitcoinUnit import fr.acinq.phoenix.data.FiatCurrency import fr.acinq.phoenix.utils.extensions.desc @@ -141,7 +142,7 @@ fun ChannelCloseOutgoingPayment.smartDescription(): String = stringResource(id = @Composable fun InboundLiquidityOutgoingPayment.smartDescription(): String = when (purchase.paymentDetails) { // manual inbound liquidity - LiquidityAds.PaymentDetails.FromChannelBalance -> "Manual liquidity" // context.getString(R.string.paymentdetails_desc_inbound_liquidity, purchase.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)) + LiquidityAds.PaymentDetails.FromChannelBalance -> "Manual liquidity +${purchase.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)}" // context.getString(R.string.paymentdetails_desc_inbound_liquidity, purchase.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)) // pay-to-open/pay-to-splice else -> "Automated liquidity" } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/BlockchainExplorer.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/BlockchainExplorer.kt index dde985b3d..78dcbf06f 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/BlockchainExplorer.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/BlockchainExplorer.kt @@ -16,7 +16,7 @@ class BlockchainExplorer(private val chain: Chain) { Website.MempoolSpace -> { when (chain) { Chain.Mainnet -> "${website.base}/tx/$txId" - Chain.Testnet3 -> "${website.base}/testnet3/tx/$txId" + Chain.Testnet3 -> "${website.base}/testnet/tx/$txId" Chain.Testnet4 -> "${website.base}/testnet4/tx/$txId" Chain.Signet -> "${website.base}/signet/tx/$txId" Chain.Regtest -> "${website.base}/_REGTEST_/tx/$txId" @@ -25,7 +25,7 @@ class BlockchainExplorer(private val chain: Chain) { Website.BlockstreamInfo -> { when (chain) { Chain.Mainnet -> "${website.base}/tx/$txId" - Chain.Testnet3 -> "${website.base}/testnet3/tx/$txId" + Chain.Testnet3 -> "${website.base}/testnet/tx/$txId" Chain.Testnet4 -> "${website.base}/testnet4/tx/$txId" Chain.Signet -> "${website.base}/signet/tx/$txId" Chain.Regtest -> "${website.base}/_REGTEST_/tx/$txId" @@ -39,7 +39,7 @@ class BlockchainExplorer(private val chain: Chain) { Website.MempoolSpace -> { when (chain) { Chain.Mainnet -> "${website.base}/address/$addr" - Chain.Testnet3 -> "${website.base}/testnet3/address/$addr" + Chain.Testnet3 -> "${website.base}/testnet/address/$addr" Chain.Testnet4 -> "${website.base}/testnet4/address/$addr" Chain.Signet -> "${website.base}/signet/address/$addr" Chain.Regtest -> "${website.base}/_REGTEST_/address/$addr" @@ -48,7 +48,7 @@ class BlockchainExplorer(private val chain: Chain) { Website.BlockstreamInfo -> { when (chain) { Chain.Mainnet -> "${website.base}/address/$addr" - Chain.Testnet3 -> "${website.base}/testnet3/address/$addr" + Chain.Testnet3 -> "${website.base}/testnet/address/$addr" Chain.Testnet4 -> "${website.base}/testnet4/address/$addr" Chain.Signet -> "${website.base}/signet/address/$addr" Chain.Regtest -> "${website.base}/_REGTEST_/address/$addr" From f616a9516a9830c4f09fd1eccb40feb6fd2d5936 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:40:06 +0200 Subject: [PATCH 07/25] (android) Simplify liquidity screen Also fixed several UI issues. --- buildSrc/src/main/kotlin/Versions.kt | 2 +- .../android/components/SplashLayout.kt | 4 +- .../details/PaymentDetailsTechnicalView.kt | 45 ++++++++----------- .../payments/details/splash/SplashIncoming.kt | 1 + .../details/splash/SplashLightningOut.kt | 1 + .../details/splash/SplashLiquidityPurchase.kt | 44 +++++++++++++++--- .../payments/history/PaymentsHistoryView.kt | 4 +- .../android/payments/offer/SendOfferView.kt | 4 +- .../android/settings/channels/ChannelsView.kt | 2 +- .../acinq/phoenix/android/utils/extensions.kt | 2 +- .../src/main/res/values-fr/strings.xml | 2 +- 11 files changed, 71 insertions(+), 40 deletions(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 8987bbd52..8fbdee2dc 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -18,7 +18,7 @@ object Versions { const val lifecycle = "2.6.0" const val prefs = "1.2.0" const val datastore = "1.0.0" - const val compose = "1.6.2" + const val compose = "1.6.8" const val composeCompiler = "1.5.8" const val navCompose = "2.6.0" const val accompanist = "0.30.1" diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt index 7a47b94b0..4f60c5e41 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt @@ -103,8 +103,8 @@ fun SplashLayout( } Column( modifier = Modifier - .widthIn(max = 500.dp) - .padding(horizontal = 24.dp), + .widthIn(max = 700.dp) + .padding(horizontal = 6.dp), horizontalAlignment = Alignment.CenterHorizontally ) { bottomContent() 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 1d40497b4..ec6b279a0 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 @@ -71,6 +71,7 @@ import fr.acinq.phoenix.data.ExchangeRate import fr.acinq.phoenix.data.WalletPaymentId import fr.acinq.phoenix.data.WalletPaymentInfo import fr.acinq.phoenix.utils.extensions.amountFeeCredit +import fr.acinq.phoenix.utils.extensions.relatedPaymentIds @Composable @@ -385,34 +386,26 @@ private fun DetailsForInboundLiquidity( TransactionRow(payment.txId) ChannelIdRow(channelId = payment.channelId) TechnicalRow(label = "Purchase Type") { - Text(text = when (payment.purchase) { - is LiquidityAds.Purchase.Standard -> "Standard" - is LiquidityAds.Purchase.WithFeeCredit -> "Fee credit" - }) - } - val details = payment.purchase.paymentDetails - TechnicalRow(label = "Purchase details") { - Text(text = details.paymentType.toString()) - } - when (details) { - is LiquidityAds.PaymentDetails.FromFutureHtlc -> ListLinksOfPaymentHashes(details.paymentHashes) - is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> ListLinksOfPaymentHashes(details.paymentHashes) - else -> Unit + Text(text = "${ + when (payment.purchase) { + is LiquidityAds.Purchase.Standard -> "Standard" + is LiquidityAds.Purchase.WithFeeCredit -> "Fee credit" + } + } [${payment.purchase.paymentDetails.paymentType}]") } -} - -@Composable -private fun ListLinksOfPaymentHashes(paymentHashes: List) { + val paymentIds = payment.relatedPaymentIds() val navController = navController - TechnicalRow(label = "Triggered by payments") { - Column { - paymentHashes.forEach { - InlineButton( - text = "- ${it.toHex()}", - onClick = { navigateToPaymentDetails(navController, WalletPaymentId.IncomingPaymentId(it), isFromEvent = false) }, - maxLines = 1, - ) - } + paymentIds.forEach { + TechnicalRowClickable( + label = "Triggered by", + onClick = { navigateToPaymentDetails(navController, it, isFromEvent = false) }, + ) { + TextWithIcon( + text = "(incoming) ${it.dbId}", + icon = R.drawable.ic_arrow_down_circle, + maxLines = 1, textOverflow = TextOverflow.Ellipsis, + space = 4.dp + ) } } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt index 4a7a926ce..d75f49fa6 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt @@ -61,6 +61,7 @@ fun SplashIncoming( Spacer(modifier = Modifier.height(8.dp)) } OfferSentBy(payerPubkey = meta.payerKey, !meta.payerNote.isNullOrBlank()) + Spacer(modifier = Modifier.height(4.dp)) } SplashDescription( diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt index 30811e798..651a1a79a 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt @@ -67,6 +67,7 @@ fun SplashLightningOutgoing( payment.outgoingInvoiceRequest()?.payerNote?.takeIf { it.isNotBlank() }?.let { OfferPayerNote(payerNote = it) + Spacer(modifier = Modifier.height(4.dp)) } SplashDescription( diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt index 2dde1e294..667c51c7c 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -41,6 +42,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment import fr.acinq.lightning.utils.sat @@ -51,6 +56,7 @@ import fr.acinq.phoenix.android.Screen import fr.acinq.phoenix.android.business import fr.acinq.phoenix.android.components.BorderButton import fr.acinq.phoenix.android.components.BottomSheetDialog +import fr.acinq.phoenix.android.components.Button import fr.acinq.phoenix.android.components.Clickable import fr.acinq.phoenix.android.components.SplashClickableContent import fr.acinq.phoenix.android.components.SplashLabelRow @@ -78,7 +84,7 @@ fun SplashLiquidityPurchase( metadata: WalletPaymentMetadata, onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, ) { - SplashPurchase(purchase = payment.purchase) + SplashPurchase(payment = payment) Spacer(modifier = Modifier.height(12.dp)) SplashFee(payment = payment) @@ -87,7 +93,7 @@ fun SplashLiquidityPurchase( // However, swap-ins do not **yet** request additional liquidity, so **for now** we can make a safe approximation. // Eventually, once swap-ins are upgraded to request liquidity, this will have to be fixed, . if (payment.purchase.paymentDetails !is LiquidityAds.PaymentDetails.FromChannelBalance) { - AutoLiquidityDetails(payment) + // AutoLiquidityDetails(payment) } } @@ -162,12 +168,39 @@ private fun SplashFee( @Composable private fun SplashPurchase( - purchase: LiquidityAds.Purchase + payment: InboundLiquidityOutgoingPayment ) { val btcUnit = LocalBitcoinUnit.current Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = "Liquidity") { - Text(text = purchase.amount.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + SplashLabelRow(label = "Liquidity added") { + Text(text = payment.purchase.amount.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + if (payment.purchase.paymentDetails !is LiquidityAds.PaymentDetails.FromChannelBalance) { + Spacer(modifier = Modifier.height(4.dp)) + val relatedPaymentId = payment.relatedPaymentIds().firstOrNull() + if (payment.feePaidFromChannelBalance.total == 0.sat || relatedPaymentId == null) { + Text(text = "This liquidity was needed to receive new payments", style = MaterialTheme.typography.subtitle2) + } else { + // this is an automated liquidity paid from balance => show a clickable link for nice UX + val navController = navController + val text = buildAnnotatedString { + append("This liquidity was needed to receive ") + pushStringAnnotation("payments", annotation = "click") + withStyle(SpanStyle(textDecoration = TextDecoration.Underline, color = MaterialTheme.colors.primary)) { + append("new payments.") + } + pop() + } + ClickableText( + text = text, + onClick = { offset -> + text.getStringAnnotations(tag = "payments", start = offset, end = offset).firstOrNull()?.let { + navigateToPaymentDetails(navController, relatedPaymentId, isFromEvent = false) + } + }, + style = MaterialTheme.typography.subtitle2 + ) + } + } } } @@ -223,6 +256,7 @@ private fun AutoLiquidityDetails( ) } } + // Text( // text = "Swipe right to see these payments.", // style = MaterialTheme.typography.caption.copy(fontSize = 14.sp), diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/PaymentsHistoryView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/PaymentsHistoryView.kt index 66770c983..74d08839c 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/PaymentsHistoryView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/PaymentsHistoryView.kt @@ -147,7 +147,7 @@ fun PaymentsHistoryView( .distinctUntilChanged() .filter { index -> val entriesInListCount = groupedPayments.entries.size + payments.size - val isLastElementFetched = index == entriesInListCount - 1 + val isLastElementFetched = index >= entriesInListCount - 1 isLastElementFetched } .distinctUntilChanged() @@ -155,7 +155,7 @@ fun PaymentsHistoryView( val hasMorePaymentsToFetch = payments.size < allPaymentsCount if (hasMorePaymentsToFetch) { // Subscribe to a bit more payments. Ideally would be the screen height / height of each payment. - paymentsViewModel.subscribeToPayments(offset = 0, count = index + 10) + paymentsViewModel.subscribeToPayments(offset = 0, count = index + 16) } } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/offer/SendOfferView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/offer/SendOfferView.kt index e722dafdc..6e4e583cc 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/offer/SendOfferView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/offer/SendOfferView.kt @@ -42,6 +42,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -78,6 +79,7 @@ fun SendOfferView( val context = LocalContext.current val balance = business.balanceManager.balance.collectAsState(null).value val prefBitcoinUnit = LocalBitcoinUnit.current + val keyboardManager = LocalSoftwareKeyboardController.current val vm = viewModel(factory = SendOfferViewModel.Factory(offer, business.peerManager, business.nodeParamsManager, business.contactsManager)) val requestedAmount = offer.amount @@ -171,7 +173,7 @@ fun SendOfferView( } if (showMessageDialog) { - PayerNoteInput(initialMessage = message, onMessageChange = { message = it }, onDismiss = { showMessageDialog = false }) + PayerNoteInput(initialMessage = message, onMessageChange = { message = it }, onDismiss = { showMessageDialog = false ; keyboardManager?.hide() }) } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ChannelsView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ChannelsView.kt index fc9207aea..bcdc02da2 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ChannelsView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ChannelsView.kt @@ -111,7 +111,7 @@ private fun LightningBalanceView( if (balance != null && inboundLiquidity != null) { val balanceVsInbound = remember(balance, inboundLiquidity) { (balance.msat.toFloat() / (balance.msat + inboundLiquidity.msat)) - .coerceIn(0.1f, 0.9f)// unreadable otherwise + .coerceIn(0.1f, if (inboundLiquidity.msat > 0) 0.9f else 1f) // unreadable otherwise .takeUnless { it.isNaN() } } Row(verticalAlignment = Alignment.CenterVertically) { 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 0f30a3ff4..d8a5c01f7 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 @@ -142,7 +142,7 @@ fun ChannelCloseOutgoingPayment.smartDescription(): String = stringResource(id = @Composable fun InboundLiquidityOutgoingPayment.smartDescription(): String = when (purchase.paymentDetails) { // manual inbound liquidity - LiquidityAds.PaymentDetails.FromChannelBalance -> "Manual liquidity +${purchase.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)}" // context.getString(R.string.paymentdetails_desc_inbound_liquidity, purchase.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)) + LiquidityAds.PaymentDetails.FromChannelBalance -> "Manual liquidity +${purchase.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)}" // pay-to-open/pay-to-splice else -> "Automated liquidity" } diff --git a/phoenix-android/src/main/res/values-fr/strings.xml b/phoenix-android/src/main/res/values-fr/strings.xml index 6e9f52a50..f88196b24 100644 --- a/phoenix-android/src/main/res/values-fr/strings.xml +++ b/phoenix-android/src/main/res/values-fr/strings.xml @@ -327,7 +327,7 @@ Message Déchiffrement du message… - Desc. + Description Envoyé à Mineurs Bitcoin Frais From 42eba9e4c54a760caeb9b6afe8c1299fa676f763 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:09:10 +0200 Subject: [PATCH 08/25] (android) Add links to related payments in liquidity splash Also added 2 helper extensions methods in shared module. --- .../details/PaymentDetailsTechnicalView.kt | 4 +- .../android/payments/details/PaymentLine.kt | 7 +- .../details/splash/PaymentSplashStatus.kt | 29 ++++ .../details/splash/PaymentSplashView.kt | 2 +- .../details/splash/SplashLiquidityPurchase.kt | 137 ++++++------------ .../utils/extensions/PaymentExtensions.kt | 18 +++ 6 files changed, 93 insertions(+), 104 deletions(-) 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 ec6b279a0..e2852432d 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 @@ -53,7 +53,6 @@ import fr.acinq.phoenix.android.components.AmountView import fr.acinq.phoenix.android.components.Card import fr.acinq.phoenix.android.components.CardHeader import fr.acinq.phoenix.android.components.Clickable -import fr.acinq.phoenix.android.components.InlineButton import fr.acinq.phoenix.android.components.TextWithIcon import fr.acinq.phoenix.android.components.InlineTransactionLink import fr.acinq.phoenix.android.components.openLink @@ -68,7 +67,6 @@ import fr.acinq.phoenix.android.utils.MSatDisplayPolicy import fr.acinq.phoenix.android.utils.copyToClipboard import fr.acinq.phoenix.android.utils.mutedBgColor import fr.acinq.phoenix.data.ExchangeRate -import fr.acinq.phoenix.data.WalletPaymentId import fr.acinq.phoenix.data.WalletPaymentInfo import fr.acinq.phoenix.utils.extensions.amountFeeCredit import fr.acinq.phoenix.utils.extensions.relatedPaymentIds @@ -397,7 +395,7 @@ private fun DetailsForInboundLiquidity( val navController = navController paymentIds.forEach { TechnicalRowClickable( - label = "Triggered by", + label = "Caused by", onClick = { navigateToPaymentDetails(navController, it, isFromEvent = false) }, ) { TextWithIcon( diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt index 3013d6bbd..6c46c89f3 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentLine.kt @@ -51,7 +51,6 @@ import fr.acinq.lightning.db.OutgoingPayment import fr.acinq.lightning.db.SpliceCpfpOutgoingPayment import fr.acinq.lightning.db.SpliceOutgoingPayment import fr.acinq.lightning.db.WalletPayment -import fr.acinq.lightning.utils.sat import fr.acinq.phoenix.android.R import fr.acinq.phoenix.android.business import fr.acinq.phoenix.android.components.AmountView @@ -68,6 +67,7 @@ import fr.acinq.phoenix.data.WalletPaymentInfo import fr.acinq.phoenix.data.walletPaymentId import fr.acinq.phoenix.utils.extensions.WalletPaymentState import fr.acinq.phoenix.utils.extensions.incomingOfferMetadata +import fr.acinq.phoenix.utils.extensions.isPaidInTheFuture import fr.acinq.phoenix.utils.extensions.outgoingInvoiceRequest import fr.acinq.phoenix.utils.extensions.state @@ -142,9 +142,8 @@ fun PaymentLine( Row { PaymentDescription(paymentInfo = paymentInfo, contactInfo = contactInfo, modifier = Modifier.weight(1.0f)) Spacer(modifier = Modifier.width(16.dp)) - val showPayment = payment.state() != WalletPaymentState.Failure - && !(payment is InboundLiquidityOutgoingPayment && payment.feePaidFromChannelBalance.total == 0.sat) - if (showPayment) { + val hideAmount = payment.state() == WalletPaymentState.Failure || (payment is InboundLiquidityOutgoingPayment && payment.isPaidInTheFuture()) + if (!hideAmount) { val isOutgoing = payment is OutgoingPayment if (isAmountRedacted) { Text(text = "****") diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashStatus.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashStatus.kt index 0eb146a6c..6646a9d2d 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashStatus.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashStatus.kt @@ -58,6 +58,7 @@ import fr.acinq.lightning.db.LightningOutgoingPayment import fr.acinq.lightning.db.SpliceCpfpOutgoingPayment import fr.acinq.lightning.db.SpliceOutgoingPayment import fr.acinq.lightning.db.WalletPayment +import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.android.R import fr.acinq.phoenix.android.business import fr.acinq.phoenix.android.components.Card @@ -385,4 +386,32 @@ private fun BumpTransactionDialog( ) { CpfpView(channelId = channelId, onSuccess = onSuccess) } +} + +@Composable +fun SplashLiquidityStatus(payment: InboundLiquidityOutgoingPayment, fromEvent: Boolean) { + when (val lockedAt = payment.lockedAt) { + null -> { + PaymentStatusIcon( + message = null, + imageResId = R.drawable.ic_payment_details_pending_onchain_static, + isAnimated = false, + color = mutedTextColor, + ) + } + else -> { + PaymentStatusIcon( + message = { + if (payment.purchase.paymentDetails is LiquidityAds.PaymentDetails.FromChannelBalance) { + Text(text = annotatedStringResource(id = R.string.paymentdetails_status_inbound_liquidity_success, lockedAt.toRelativeDateString())) + } else { + Text(text = annotatedStringResource(id = R.string.paymentdetails_status_inbound_liquidity_auto_success, lockedAt.toRelativeDateString())) + } + }, + imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, + isAnimated = fromEvent, + color = positiveColor, + ) + } + } } \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt index 34571fae0..148a3db45 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt @@ -114,7 +114,7 @@ fun PaymentDetailsSplashView( is ChannelCloseOutgoingPayment -> SplashChannelClose(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) is SpliceCpfpOutgoingPayment -> SplashSpliceOutCpfp(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) is SpliceOutgoingPayment -> SplashSpliceOut(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) - is InboundLiquidityOutgoingPayment -> SplashLiquidityPurchase(payment = payment, metadata = data.metadata, onMetadataDescriptionUpdate = onMetadataDescriptionUpdate) + is InboundLiquidityOutgoingPayment -> SplashLiquidityPurchase(payment = payment) } Spacer(modifier = Modifier.height(48.dp)) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt index 667c51c7c..da408d182 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt @@ -25,11 +25,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.ClickableText import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -42,13 +42,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withStyle +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment -import fr.acinq.lightning.utils.sat import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.android.LocalBitcoinUnit import fr.acinq.phoenix.android.R @@ -66,85 +62,50 @@ import fr.acinq.phoenix.android.navigateToPaymentDetails import fr.acinq.phoenix.android.payments.details.PaymentLine import fr.acinq.phoenix.android.payments.details.PaymentLineLoading import fr.acinq.phoenix.android.utils.Converter.toPrettyString -import fr.acinq.phoenix.android.utils.Converter.toRelativeDateString import fr.acinq.phoenix.android.utils.MSatDisplayPolicy -import fr.acinq.phoenix.android.utils.annotatedStringResource -import fr.acinq.phoenix.android.utils.mutedTextColor -import fr.acinq.phoenix.android.utils.positiveColor +import fr.acinq.phoenix.android.utils.mutedBgColor import fr.acinq.phoenix.data.WalletPaymentFetchOptions import fr.acinq.phoenix.data.WalletPaymentId import fr.acinq.phoenix.data.WalletPaymentInfo -import fr.acinq.phoenix.data.WalletPaymentMetadata +import fr.acinq.phoenix.utils.extensions.isManualPurchase +import fr.acinq.phoenix.utils.extensions.isPaidInTheFuture import fr.acinq.phoenix.utils.extensions.relatedPaymentIds import kotlinx.coroutines.launch @Composable fun SplashLiquidityPurchase( payment: InboundLiquidityOutgoingPayment, - metadata: WalletPaymentMetadata, - onMetadataDescriptionUpdate: (WalletPaymentId, String?) -> Unit, ) { SplashPurchase(payment = payment) - Spacer(modifier = Modifier.height(12.dp)) SplashFee(payment = payment) + SplashRelatedPayments(payment) - // FIXME: dangerous!! - // In general, FromChannelBalance only happens for manual purchases OR automated swap-ins with additional liquidity. - // However, swap-ins do not **yet** request additional liquidity, so **for now** we can make a safe approximation. - // Eventually, once swap-ins are upgraded to request liquidity, this will have to be fixed, . - if (payment.purchase.paymentDetails !is LiquidityAds.PaymentDetails.FromChannelBalance) { - // AutoLiquidityDetails(payment) - } +// if (payment.purchase.paymentDetails !is LiquidityAds.PaymentDetails.FromChannelBalance) { +// AutoLiquidityDetails(payment) +// } } @Composable -fun SplashLiquidityStatus(payment: InboundLiquidityOutgoingPayment, fromEvent: Boolean) { - when (val lockedAt = payment.lockedAt) { - null -> { - PaymentStatusIcon( - message = null, - imageResId = R.drawable.ic_payment_details_pending_onchain_static, - isAnimated = false, - color = mutedTextColor, - ) - } - else -> { - PaymentStatusIcon( - message = { - if (payment.purchase.paymentDetails is LiquidityAds.PaymentDetails.FromChannelBalance) { - Text(text = annotatedStringResource(id = R.string.paymentdetails_status_inbound_liquidity_success, lockedAt.toRelativeDateString())) - } else { - Text(text = annotatedStringResource(id = R.string.paymentdetails_status_inbound_liquidity_auto_success, lockedAt.toRelativeDateString())) - } - }, - imageResId = if (fromEvent) R.drawable.ic_payment_details_success_animated else R.drawable.ic_payment_details_success_static, - isAnimated = fromEvent, - color = positiveColor, - ) - } +private fun SplashPurchase( + payment: InboundLiquidityOutgoingPayment, +) { + val btcUnit = LocalBitcoinUnit.current + SplashLabelRow( + label = "Liquidity", + helpMessage = if (payment.isManualPurchase()) null else "This liquidity was required to receive a payment.", + helpLink = "See how to optimise" to "https://acinq.co/faq" + ) { + Text(text = payment.purchase.amount.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) } } - @Composable private fun SplashFee( payment: InboundLiquidityOutgoingPayment ) { val btcUnit = LocalBitcoinUnit.current - - // if the fee paid from channel balance is 0, it means this is a liquidity purchase for a new channel whose fee are paid - // by a future htlc. In this case, for UX reasons, we don't show the fees here but instead link to the payment whose htlcs - // paid the fees. - if (payment.feePaidFromChannelBalance.total == 0.sat) { - SplashLabelRow(label = "Fees") { - val navController = navController - payment.relatedPaymentIds().forEach { - SplashClickableContent(onClick = { navigateToPaymentDetails(navController, it, isFromEvent = false) }) { - TextWithIcon(text = "See related payment", icon = R.drawable.ic_arrow_next) - } - } - } - } else { + if (!payment.isPaidInTheFuture()) { + Spacer(modifier = Modifier.height(8.dp)) val miningFee = payment.feePaidFromChannelBalance.miningFee val serviceFee = payment.feePaidFromChannelBalance.serviceFee SplashLabelRow( @@ -159,47 +120,31 @@ private fun SplashFee( helpMessage = stringResource(id = R.string.paymentdetails_liquidity_service_fee_help) ) { Text(text = serviceFee.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) - if (payment.purchase is LiquidityAds.Purchase.WithFeeCredit) { - Text(text = "Paid with fee credit") - } } } } @Composable -private fun SplashPurchase( - payment: InboundLiquidityOutgoingPayment -) { - val btcUnit = LocalBitcoinUnit.current - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = "Liquidity added") { - Text(text = payment.purchase.amount.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) - if (payment.purchase.paymentDetails !is LiquidityAds.PaymentDetails.FromChannelBalance) { - Spacer(modifier = Modifier.height(4.dp)) - val relatedPaymentId = payment.relatedPaymentIds().firstOrNull() - if (payment.feePaidFromChannelBalance.total == 0.sat || relatedPaymentId == null) { - Text(text = "This liquidity was needed to receive new payments", style = MaterialTheme.typography.subtitle2) - } else { - // this is an automated liquidity paid from balance => show a clickable link for nice UX - val navController = navController - val text = buildAnnotatedString { - append("This liquidity was needed to receive ") - pushStringAnnotation("payments", annotation = "click") - withStyle(SpanStyle(textDecoration = TextDecoration.Underline, color = MaterialTheme.colors.primary)) { - append("new payments.") - } - pop() - } - ClickableText( - text = text, - onClick = { offset -> - text.getStringAnnotations(tag = "payments", start = offset, end = offset).firstOrNull()?.let { - navigateToPaymentDetails(navController, relatedPaymentId, isFromEvent = false) - } - }, - style = MaterialTheme.typography.subtitle2 - ) - } +private fun SplashRelatedPayments(payment: InboundLiquidityOutgoingPayment) { + val relatedPaymentIds = payment.relatedPaymentIds() + if (relatedPaymentIds.isNotEmpty()) { + val navController = navController + val paymentId = relatedPaymentIds.first() + Spacer(modifier = Modifier.height(4.dp)) + SplashLabelRow( + label = "Caused by", + ) { + Button( + text = paymentId.dbId, + icon = R.drawable.ic_zap, + onClick = { navigateToPaymentDetails(navController, paymentId, isFromEvent = false) }, + maxLines = 1, + padding = PaddingValues(horizontal = 7.dp, vertical = 5.dp), + space = 4.dp, + shape = RoundedCornerShape(12.dp), + backgroundColor = mutedBgColor, + modifier = Modifier.widthIn(max = 170.dp) + ) } } } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt index ad74f425e..c4a49f7a1 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/PaymentExtensions.kt @@ -28,6 +28,7 @@ import fr.acinq.lightning.payment.Bolt12Invoice import fr.acinq.lightning.payment.OfferPaymentMetadata import fr.acinq.lightning.utils.getValue import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.sum import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.lightning.wire.OfferTypes @@ -108,3 +109,20 @@ fun InboundLiquidityOutgoingPayment.relatedPaymentIds() : List is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> details.paymentHashes else -> emptyList() }.map { WalletPaymentId.IncomingPaymentId(it) } + +/** + * Returns true if this liquidity was initiated manually by the user, false otherwise. + * + * FIXME: Dangerous!! + * In general, FromChannelBalance only happens for manual purchases OR automated swap-ins with additional liquidity. + * However, swap-ins do not **yet** request additional liquidity, so **for now** we can make a safe approximation. + * Eventually, once swap-ins are upgraded to request liquidity, this will have to be fixed. + */ +fun InboundLiquidityOutgoingPayment.isManualPurchase(): Boolean = purchase.paymentDetails is LiquidityAds.PaymentDetails.FromChannelBalance + +/** + * Returns true if the liquidity fee was paid by an htlc in a future incoming payment. When that's the case, we should + * not display the fees in the liquidity details screen to avoid confusion. Instead we should link to the payment whose + * HTLCs paid the fees. + */ +fun InboundLiquidityOutgoingPayment.isPaidInTheFuture(): Boolean = feePaidFromChannelBalance.total == 0.sat \ No newline at end of file From c0eaa5f03b487f827f540e436d3d95603ac5b963 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:31:28 +0200 Subject: [PATCH 09/25] (android) Switched help button and clean code --- .../details/splash/SplashLiquidityPurchase.kt | 128 +----------------- 1 file changed, 3 insertions(+), 125 deletions(-) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt index da408d182..3333cfd97 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt @@ -90,11 +90,7 @@ private fun SplashPurchase( payment: InboundLiquidityOutgoingPayment, ) { val btcUnit = LocalBitcoinUnit.current - SplashLabelRow( - label = "Liquidity", - helpMessage = if (payment.isManualPurchase()) null else "This liquidity was required to receive a payment.", - helpLink = "See how to optimise" to "https://acinq.co/faq" - ) { + SplashLabelRow(label = "Liquidity") { Text(text = payment.purchase.amount.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) } } @@ -133,6 +129,8 @@ private fun SplashRelatedPayments(payment: InboundLiquidityOutgoingPayment) { Spacer(modifier = Modifier.height(4.dp)) SplashLabelRow( label = "Caused by", + helpMessage = if (payment.isManualPurchase()) null else "This liquidity was required to receive a payment.", + helpLink = "See how to optimise" to "https://acinq.co/faq", ) { Button( text = paymentId.dbId, @@ -148,123 +146,3 @@ private fun SplashRelatedPayments(payment: InboundLiquidityOutgoingPayment) { } } } - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun AutoLiquidityDetails( - payment: InboundLiquidityOutgoingPayment -) { - val navController = navController - var showPaymentsDialog by remember { mutableStateOf(false) } - - Spacer(modifier = Modifier.height(32.dp)) - BorderButton( - text = "What is this?", - icon = R.drawable.ic_help_circle, - onClick = { showPaymentsDialog = true }, - maxLines = 1, - ) - - if (showPaymentsDialog) { - BottomSheetDialog(onDismiss = { showPaymentsDialog = false }, modifier = Modifier.fillMaxHeight(.6f), internalPadding = PaddingValues(bottom = 32.dp)) { - val pagerState = rememberPagerState(pageCount = { 2 }) - HorizontalPager( - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth(), - state = pagerState, - verticalAlignment = Alignment.Top, - beyondBoundsPageCount = 1 - ) { index -> - when (index) { - 0 -> { - Column { - Text( - text = "Why did this payment happen?", - style = MaterialTheme.typography.h4, - modifier = Modifier.padding(horizontal = 24.dp) - ) - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Your Lightning channel had to be resized, which is an on-chain operation incurring fees.", modifier = Modifier.padding(horizontal = 24.dp)) - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "This operation was necessary to accommodate new incoming payments.", modifier = Modifier.padding(horizontal = 24.dp)) - Spacer(modifier = Modifier.height(12.dp)) - Spacer(modifier = Modifier.height(4.dp)) - val scope = rememberCoroutineScope() - Clickable(onClick = { scope.launch { pagerState.animateScrollToPage(1) } }, modifier = Modifier - .padding(horizontal = 12.dp) - .align(Alignment.CenterHorizontally), shape = RoundedCornerShape(10.dp)) { - Column(modifier = Modifier.padding(12.dp)) { - TextWithIcon( - text = "See related payments", - icon = R.drawable.ic_arrow_next, - ) - } - } - -// Text( -// text = "Swipe right to see these payments.", -// style = MaterialTheme.typography.caption.copy(fontSize = 14.sp), -// modifier = Modifier.padding(horizontal = 24.dp) -// ) - - Spacer(modifier = Modifier.height(32.dp)) - Text( - text = "How to optimise channels resizing?", - style = MaterialTheme.typography.h4, - modifier = Modifier.padding(horizontal = 24.dp), - ) - Spacer(modifier = Modifier.height(4.dp)) - Clickable(onClick = { navController.navigate(Screen.LiquidityPolicy.route) }, modifier = Modifier.padding(horizontal = 12.dp), shape = RoundedCornerShape(10.dp)) { - Column(modifier = Modifier - .fillMaxWidth() - .padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally) { - TextWithIcon( - text = "Configure automated management", - icon = R.drawable.ic_settings, - ) - Spacer(modifier = Modifier.height(2.dp)) - Text(text = "Cap fees, or disable them altogether", style = MaterialTheme.typography.subtitle2) - } - } - Clickable(onClick = { navController.navigate(Screen.LiquidityRequest.route) }, modifier = Modifier.padding(horizontal = 12.dp), shape = RoundedCornerShape(10.dp)) { - Column(modifier = Modifier - .fillMaxWidth() - .padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally) { - TextWithIcon( - text = "Purchase liquidity in advance", - icon = R.drawable.ic_idea, - ) - Spacer(modifier = Modifier.height(2.dp)) - Text(text = "Requires some planning, but is most optimal", style = MaterialTheme.typography.subtitle2) - } - } - } - } - 1 -> { - Column(modifier = Modifier.fillMaxSize()) { - Text(text = "Operation triggered by...", style = MaterialTheme.typography.h4, modifier = Modifier.padding(horizontal = 16.dp)) - Spacer(modifier = Modifier.height(8.dp)) - TriggeredBy(ids = payment.relatedPaymentIds()) - } - } - } - } - } - } -} - -@Composable -private fun TriggeredBy(ids: List) { - val navController = navController - val paymentsManager = business.paymentsManager - ids.forEach { id -> - val paymentInfo by produceState(initialValue = null) { - value = paymentsManager.getPayment(id = id, options = WalletPaymentFetchOptions.None) - } - - paymentInfo?.let { - PaymentLine(paymentInfo = it, contactInfo = null, onPaymentClick = { navigateToPaymentDetails(navController, id, isFromEvent = false) }) - } ?: PaymentLineLoading(paymentId = id, onPaymentClick = { navigateToPaymentDetails(navController, id, isFromEvent = false) }) - } -} From 9cbe20cc143ac81f9e436ae7d14c5b550d839ba5 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:59:30 +0200 Subject: [PATCH 10/25] (android) Normalise splash row height --- .../fr/acinq/phoenix/android/components/SplashLayout.kt | 1 + .../payments/details/PaymentDetailsTechnicalView.kt | 4 ++-- .../android/payments/details/splash/PaymentSplashView.kt | 1 - .../payments/details/splash/SplashChannelClose.kt | 9 ++++++--- .../android/payments/details/splash/SplashIncoming.kt | 2 -- .../payments/details/splash/SplashLightningOut.kt | 1 - .../payments/details/splash/SplashLiquidityPurchase.kt | 4 ---- 7 files changed, 9 insertions(+), 13 deletions(-) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt index 4f60c5e41..d08ab6c1b 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/SplashLayout.kt @@ -127,6 +127,7 @@ fun SplashLabelRow( Row( modifier = Modifier .weight(1f) + .heightIn(min = 22.dp) .alignByBaseline(), horizontalArrangement = Arrangement.End ) { 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 e2852432d..3289c61dd 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 @@ -381,8 +381,6 @@ private fun DetailsForCpfp( private fun DetailsForInboundLiquidity( payment: InboundLiquidityOutgoingPayment ) { - TransactionRow(payment.txId) - ChannelIdRow(channelId = payment.channelId) TechnicalRow(label = "Purchase Type") { Text(text = "${ when (payment.purchase) { @@ -391,6 +389,8 @@ private fun DetailsForInboundLiquidity( } } [${payment.purchase.paymentDetails.paymentType}]") } + TransactionRow(payment.txId) + ChannelIdRow(channelId = payment.channelId) val paymentIds = payment.relatedPaymentIds() val navController = navController paymentIds.forEach { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt index 148a3db45..d9c0b31be 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt @@ -151,7 +151,6 @@ fun SplashDescription( } } } - Spacer(modifier = Modifier.height(5.dp)) SplashLabelRow(label = if (userDescription.isNullOrBlank()) "" else "Note") { SplashClickableContent(onClick = { showEditDescriptionDialog = true }) { if (!userDescription.isNullOrBlank()) { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt index a9fa90d9e..ceac944ea 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashChannelClose.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import fr.acinq.lightning.db.ChannelCloseOutgoingPayment +import fr.acinq.lightning.utils.msat import fr.acinq.phoenix.android.LocalBitcoinUnit import fr.acinq.phoenix.android.R import fr.acinq.phoenix.android.business @@ -77,8 +78,10 @@ private fun SplashDestination(payment: ChannelCloseOutgoingPayment, metadata: Wa @Composable private fun SplashFee(payment: ChannelCloseOutgoingPayment) { val btcUnit = LocalBitcoinUnit.current - Spacer(modifier = Modifier.height(8.dp)) - SplashLabelRow(label = stringResource(id = R.string.paymentdetails_fees_label)) { - Text(text = payment.fees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + if (payment.fees > 0.msat) { + Spacer(modifier = Modifier.height(8.dp)) + SplashLabelRow(label = stringResource(id = R.string.paymentdetails_fees_label)) { + Text(text = payment.fees.toPrettyString(btcUnit, withUnit = true, mSatDisplayPolicy = MSatDisplayPolicy.SHOW_IF_ZERO_SATS)) + } } } \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt index d75f49fa6..4d43a62ca 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncoming.kt @@ -58,10 +58,8 @@ fun SplashIncoming( payment.incomingOfferMetadata()?.let { meta -> meta.payerNote?.takeIf { it.isNotBlank() }?.let { OfferPayerNote(payerNote = it) - Spacer(modifier = Modifier.height(8.dp)) } OfferSentBy(payerPubkey = meta.payerKey, !meta.payerNote.isNullOrBlank()) - Spacer(modifier = Modifier.height(4.dp)) } SplashDescription( diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt index 651a1a79a..30811e798 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLightningOut.kt @@ -67,7 +67,6 @@ fun SplashLightningOutgoing( payment.outgoingInvoiceRequest()?.payerNote?.takeIf { it.isNotBlank() }?.let { OfferPayerNote(payerNote = it) - Spacer(modifier = Modifier.height(4.dp)) } SplashDescription( diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt index 3333cfd97..ec7cdae4b 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt @@ -79,10 +79,6 @@ fun SplashLiquidityPurchase( SplashPurchase(payment = payment) SplashFee(payment = payment) SplashRelatedPayments(payment) - -// if (payment.purchase.paymentDetails !is LiquidityAds.PaymentDetails.FromChannelBalance) { -// AutoLiquidityDetails(payment) -// } } @Composable From 699af1166526ebdf501b985c6547de7617cc3c19 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:56:54 +0200 Subject: [PATCH 11/25] (android) Update wording and add translations Also removed channel-funding notification which is replaced by a generic error. --- .../details/PaymentDetailsTechnicalView.kt | 8 +++-- .../details/splash/SplashLiquidityPurchase.kt | 6 ++-- .../phoenix/android/services/NodeService.kt | 6 ++-- .../android/settings/NotificationsView.kt | 5 ++-- .../settings/walletinfo/SwapInWalletInfo.kt | 5 ++-- .../android/utils/SystemNotificationHelper.kt | 30 ++++++++++++------- .../acinq/phoenix/android/utils/extensions.kt | 10 +++---- .../res/values-b+es+419/important_strings.xml | 9 +++--- .../src/main/res/values-b+es+419/strings.xml | 2 ++ .../main/res/values-cs/important_strings.xml | 9 +++--- .../src/main/res/values-cs/strings.xml | 2 ++ .../main/res/values-de/important_strings.xml | 9 +++--- .../src/main/res/values-de/strings.xml | 3 +- .../main/res/values-es/important_strings.xml | 9 +++--- .../main/res/values-fr/important_strings.xml | 9 +++--- .../src/main/res/values-fr/strings.xml | 3 +- .../res/values-pt-rBR/important_strings.xml | 9 +++--- .../main/res/values-sk/important_strings.xml | 9 +++--- .../src/main/res/values-sk/strings.xml | 3 +- .../main/res/values-sw/important_strings.xml | 9 +++--- .../src/main/res/values-sw/strings.xml | 3 +- .../main/res/values-vi/important_strings.xml | 9 +++--- .../src/main/res/values-vi/strings.xml | 3 +- .../src/main/res/values/important_strings.xml | 13 ++++---- .../src/main/res/values/strings.xml | 4 ++- .../fr.acinq.phoenix/data/Notification.kt | 8 ----- .../db/notifications/NotificationDataType.kt | 6 ---- .../db/notifications/NotificationsQueries.kt | 8 ----- .../managers/NotificationsManager.kt | 8 +---- .../fr.acinq.phoenix/utils/CsvWriter.kt | 5 +++- 30 files changed, 113 insertions(+), 109 deletions(-) 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 3289c61dd..67c27f0f1 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 @@ -381,7 +381,7 @@ private fun DetailsForCpfp( private fun DetailsForInboundLiquidity( payment: InboundLiquidityOutgoingPayment ) { - TechnicalRow(label = "Purchase Type") { + TechnicalRow(label = stringResource(id = R.string.paymentdetails_liquidity_purchase_type)) { Text(text = "${ when (payment.purchase) { is LiquidityAds.Purchase.Standard -> "Standard" @@ -395,7 +395,7 @@ private fun DetailsForInboundLiquidity( val navController = navController paymentIds.forEach { TechnicalRowClickable( - label = "Caused by", + label = stringResource(id = R.string.paymentdetails_liquidity_caused_by_label), onClick = { navigateToPaymentDetails(navController, it, isFromEvent = false) }, ) { TextWithIcon( @@ -714,7 +714,9 @@ private fun TechnicalRowClickable( Clickable( onClick = onClick, onLongClick = onLongClick, - modifier = Modifier.fillMaxWidth().offset(x = (-8).dp), + modifier = Modifier + .fillMaxWidth() + .offset(x = (-8).dp), shape = RoundedCornerShape(12.dp), backgroundColor = mutedBgColor, ) { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt index ec7cdae4b..50d88e73b 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt @@ -124,9 +124,9 @@ private fun SplashRelatedPayments(payment: InboundLiquidityOutgoingPayment) { val paymentId = relatedPaymentIds.first() Spacer(modifier = Modifier.height(4.dp)) SplashLabelRow( - label = "Caused by", - helpMessage = if (payment.isManualPurchase()) null else "This liquidity was required to receive a payment.", - helpLink = "See how to optimise" to "https://acinq.co/faq", + label = stringResource(id = R.string.paymentdetails_liquidity_caused_by_label), + helpMessage = if (payment.isManualPurchase()) null else stringResource(id = R.string.paymentdetails_liquidity_caused_by_help), + helpLink = stringResource(id = R.string.paymentdetails_liquidity_caused_by_help_link) to "https://acinq.co/faq", ) { Button( text = paymentId.dbId, diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt index e75bb913b..91bf81f44 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/NodeService.kt @@ -300,7 +300,7 @@ class NodeService : Service() { // TODO: click on notif must deeplink to the notification screen when (event) { is LiquidityEvents.Rejected -> { - log.debug("processing liquidity_event=$event") + log.debug("processing liquidity_event={}", event) if (event.source == LiquidityEvents.Source.OnChainWallet) { // Check the last time a rejected on-chain swap notification has been shown. If recent, we do not want to trigger a notification every time. val lastRejectedSwap = internalData.getLastRejectedOnchainSwap.first().takeIf { @@ -327,9 +327,11 @@ class NodeService : Service() { is LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee -> { SystemNotificationHelper.notifyPaymentRejectedOverRelative(applicationContext, event.source, event.amount, event.fee, reason.maxRelativeFeeBasisPoints, nextTimeout?.second) } + is LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow -> { + SystemNotificationHelper.notifyPaymentRejectedAmountTooLow(applicationContext, event.source, event.amount) + } // Temporary errors is LiquidityEvents.Rejected.Reason.ChannelFundingInProgress, - is LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow, is LiquidityEvents.Rejected.Reason.NoMatchingFundingRate, is LiquidityEvents.Rejected.Reason.TooManyParts -> { SystemNotificationHelper.notifyPaymentRejectedFundingError(applicationContext, event.source, event.amount) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/NotificationsView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/NotificationsView.kt index 25004ed6b..86f7130ab 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/NotificationsView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/NotificationsView.kt @@ -273,9 +273,8 @@ private fun PaymentNotification( notification.fee.toPrettyString(btcUnit, withUnit = true), DecimalFormat("0.##").format(notification.maxRelativeFeeBasisPoints.toDouble() / 100), ) - is Notification.GenericError -> "An error has occurred. Please try again." - is Notification.ChannelFundingInProgress -> "A funding is in progress. Try again later." - is Notification.MissingOffChainAmountTooLow -> "The amount is too low." + is Notification.MissingOffChainAmountTooLow -> stringResource(id = R.string.notif_rejected_amount_too_low) + is Notification.GenericError -> stringResource(id = R.string.notif_rejected_generic_error) }, bottomText = when (notification) { is Notification.OverAbsoluteFee, is Notification.OverRelativeFee, is Notification.FeePolicyDisabled -> { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInWalletInfo.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInWalletInfo.kt index f910b2188..c96e9efd5 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInWalletInfo.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/walletinfo/SwapInWalletInfo.kt @@ -237,9 +237,8 @@ private fun ReadyForSwapView( DecimalFormat("0.##").format(lastSwapFailedNotification.maxRelativeFeeBasisPoints.toDouble() / 100), ) is Notification.FeePolicyDisabled -> stringResource(id = R.string.walletinfo_onchain_swapin_last_attempt_disabled) - is Notification.ChannelFundingInProgress -> stringResource(id = R.string.walletinfo_onchain_swapin_last_attempt_funding_in_progress) - is Notification.MissingOffChainAmountTooLow -> stringResource(id = R.string.walletinfo_onchain_swapin_last_attempt_amount_too_low) - is Notification.GenericError -> stringResource(id = R.string.walletinfo_onchain_swapin_last_attempt_generic) + is Notification.MissingOffChainAmountTooLow -> stringResource(id = R.string.notif_rejected_amount_too_low) + is Notification.GenericError -> stringResource(id = R.string.notif_rejected_generic_error) }, ) } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt index e59388a0e..9326ba525 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/SystemNotificationHelper.kt @@ -151,16 +151,6 @@ object SystemNotificationHelper { ) } - fun notifyPaymentRejectedFundingError(context: Context, source: LiquidityEvents.Source, amountIncoming: MilliSatoshi): Notification { - return notifyPaymentFailed( - context = context, - title = context.getString(if (source == LiquidityEvents.Source.OnChainWallet) R.string.notif_rejected_deposit_title else R.string.notif_rejected_payment_title, - amountIncoming.toPrettyString(BitcoinUnit.Sat, withUnit = true)), - message = context.getString(R.string.notif_rejected_generic_error), - deepLink = if (source == LiquidityEvents.Source.OnChainWallet) "phoenix:swapinwallet" else "phoenix:liquiditypolicy", - ) - } - fun notifyPaymentRejectedOverAbsolute(context: Context, source: LiquidityEvents.Source, amountIncoming: MilliSatoshi, fee: MilliSatoshi, absoluteMax: Satoshi, nextTimeoutRemainingBlocks: Int?): Notification { return notifyPaymentFailed( context = context, @@ -203,6 +193,26 @@ object SystemNotificationHelper { ) } + fun notifyPaymentRejectedAmountTooLow(context: Context, source: LiquidityEvents.Source, amountIncoming: MilliSatoshi): Notification { + return notifyPaymentFailed( + context = context, + title = context.getString(if (source == LiquidityEvents.Source.OnChainWallet) R.string.notif_rejected_deposit_title else R.string.notif_rejected_payment_title, + amountIncoming.toPrettyString(BitcoinUnit.Sat, withUnit = true)), + message = context.getString(R.string.notif_rejected_amount_too_low), + deepLink = if (source == LiquidityEvents.Source.OnChainWallet) "phoenix:swapinwallet" else "phoenix:liquiditypolicy", + ) + } + + fun notifyPaymentRejectedFundingError(context: Context, source: LiquidityEvents.Source, amountIncoming: MilliSatoshi): Notification { + return notifyPaymentFailed( + context = context, + title = context.getString(if (source == LiquidityEvents.Source.OnChainWallet) R.string.notif_rejected_deposit_title else R.string.notif_rejected_payment_title, + amountIncoming.toPrettyString(BitcoinUnit.Sat, withUnit = true)), + message = context.getString(R.string.notif_rejected_generic_error), + deepLink = if (source == LiquidityEvents.Source.OnChainWallet) "phoenix:swapinwallet" else "phoenix:liquiditypolicy", + ) + } + fun notifyPaymentMissedAppUnavailable(context: Context): Notification { return notifyPaymentFailed( context = context, 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 d8a5c01f7..7dbb18673 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 @@ -23,13 +23,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import fr.acinq.lightning.db.* import fr.acinq.lightning.utils.Connection -import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.android.* import fr.acinq.phoenix.android.R import fr.acinq.phoenix.android.utils.Converter.toPrettyString import fr.acinq.phoenix.data.BitcoinUnit import fr.acinq.phoenix.data.FiatCurrency import fr.acinq.phoenix.utils.extensions.desc +import fr.acinq.phoenix.utils.extensions.isManualPurchase import java.security.cert.CertificateException import java.util.* import kotlin.contracts.ExperimentalContracts @@ -140,11 +140,9 @@ fun SpliceCpfpOutgoingPayment.smartDescription(): String = stringResource(id = R fun ChannelCloseOutgoingPayment.smartDescription(): String = stringResource(id = R.string.paymentdetails_desc_closing_channel) @Composable -fun InboundLiquidityOutgoingPayment.smartDescription(): String = when (purchase.paymentDetails) { - // manual inbound liquidity - LiquidityAds.PaymentDetails.FromChannelBalance -> "Manual liquidity +${purchase.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)}" - // pay-to-open/pay-to-splice - else -> "Automated liquidity" +fun InboundLiquidityOutgoingPayment.smartDescription(): String = when { + isManualPurchase() -> stringResource(id = R.string.paymentdetails_desc_liquidity_manual, purchase.amount.toPrettyString(BitcoinUnit.Sat, withUnit = true)) + else -> stringResource(id = R.string.paymentdetails_desc_liquidity_automated) } @Composable diff --git a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml index 1e58b9b5c..74b22891f 100644 --- a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml @@ -41,6 +41,8 @@ La comisión fue %1$s, pero el límite máximo se fijó en %2$s. Este depósito vencerá el %3$s. La comisión fue %1$s, que es más del %2$s%% del importe recibido. Toca para obtener más información. La comisión fue %1$s, que es más del %2$s%% del importe recibido. Este depósito vencerá el %3$s. + El importe del pago es demasiado bajo. + Se ha producido un error durante la financiación. Vuelva a intentarlo más tarde. Inicia Phoenix Es posible que algunos de los canales se hayan cerrado. @@ -223,7 +225,6 @@ La gestión automatizada de canales está desactivada. La comisión fue %1$s, pero el límite máximo se fijó en %2$s. La comisión fue %1$s, que es más del %2$s%% del importe. - Los canales todavía se están inicializando, por lo que no se pudo recibir ese pago. Toca para configurar. Ver detalles @@ -253,8 +254,9 @@ Comisiones pagadas por el servicio de liquidez. Comisiones de minería Comisiones pagadas a los mineros de la red Bitcoin para procesar la transacción en la cadena. - Duración - 1 año + Causado por + Esta liquidez era necesaria para recibir un pago. + Vea cómo optimizar @@ -307,7 +309,6 @@ Error al intentar realizar el intercambio %1$s La gestión de canales estaba desactivada. - Los canales aún se estaban inicializando. Este intercambio vencerá en un día. Este intercambio vencerá en %1$s días. diff --git a/phoenix-android/src/main/res/values-b+es+419/strings.xml b/phoenix-android/src/main/res/values-b+es+419/strings.xml index cfcaeb56b..c293e7976 100644 --- a/phoenix-android/src/main/res/values-b+es+419/strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/strings.xml @@ -342,6 +342,8 @@ Canal de cierre Migración desde aplicación heredada Impulsar transacciones + Liquidez manual (+%1$s) + Liquidez automatizada Intercambiar a %1$s Depósito en la cadena diff --git a/phoenix-android/src/main/res/values-cs/important_strings.xml b/phoenix-android/src/main/res/values-cs/important_strings.xml index 11856103e..e6d577e41 100644 --- a/phoenix-android/src/main/res/values-cs/important_strings.xml +++ b/phoenix-android/src/main/res/values-cs/important_strings.xml @@ -44,6 +44,8 @@ Poplatek činil %1$s, ale váš maximální poplatek byl nastaven na %2$s. Platnost této zálohy vyprší %3$s. Poplatek činil %1$s, což je více než %2$s%% z přijaté částky. Můžete to upravit v nastavení. Poplatek činil %1$s, což je více než %2$s%% z přijaté částky. Platnost této zálohy vyprší %3$s. + Výše platby je příliš nízká. + Při financování došlo k chybě. Zkuste to prosím později. Spusťte prosím Phoenix Některý z vašich kanálů mohl být uzavřen. @@ -225,7 +227,6 @@ Automatická správa kanálů je zakázána. Poplatek byl %1$s, ale váš maximální poplatek byl nastaven na %2$s. Poplatek byl %1$s, což je více než %2$s%% částky. - Vaše kanály se stále inicializují a nemohly tuto platbu přijmout. Klepnutím na položku provedete konfiguraci. Zobrazit podrobnosti @@ -255,8 +256,9 @@ Poplatky za službu likvidity. Poplatky těžařům Poplatky placené těžařům Bitcoinové sítě za zpracování on-chain transakce. - Doba trvání - 1 rok + Způsobeno + Tato likvidita byla vyžadována pro získání platby. + Podívejte se, jak optimalizovat @@ -306,7 +308,6 @@ Automatizovaná poplatková politika je vypnutá. Prostředky nebudou vyměňovány. Zůstanou na této peněžence. Poslední pokus (%1$s). Automatizovaná poplatková politika je vypnutá. - Vaše kanály byly inicializovány. Neprobíhají žádné výměny. Platnost této výměny vyprší za den! diff --git a/phoenix-android/src/main/res/values-cs/strings.xml b/phoenix-android/src/main/res/values-cs/strings.xml index d847e2baa..b7042e1c2 100644 --- a/phoenix-android/src/main/res/values-cs/strings.xml +++ b/phoenix-android/src/main/res/values-cs/strings.xml @@ -326,6 +326,8 @@ Zavírání kanálu Migrace ze staré aplikace Postrčit transakci + Manuální likvidita (+%1$s) + Automatizovaná likvidita Swap-out do %1$s On-Chain vklad diff --git a/phoenix-android/src/main/res/values-de/important_strings.xml b/phoenix-android/src/main/res/values-de/important_strings.xml index 40c28e957..af1f54b64 100644 --- a/phoenix-android/src/main/res/values-de/important_strings.xml +++ b/phoenix-android/src/main/res/values-de/important_strings.xml @@ -41,6 +41,8 @@ Die Gebühr wäre %1$s, aber Ihr Gebührenlimit beträgt %2$s. Diese Einzahlung verfällt am %3$s. Die Gebühr wäre %1$s, was mehr als %2$s%% des zu empfangenden Betrags ist. Sie können dies in den Einstellungen anpassen. Die Gebühr wäre %1$s, was mehr als %2$s%% des zu empfangenden Betrags ist. Diese Einzahlung verfällt am %3$s. + Der Zahlungsbetrag ist zu niedrig. + Bei der Überweisung ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal. Bitte öffnen Sie Phoenix Einige Ihrer Kanäle wurden möglicherweise geschlossen. @@ -222,7 +224,6 @@ Automatisches Kanal-Management ist deaktiviert. Die Gebühr wäre %1$s, aber Ihr Gebührenlimit beträgt %2$s. Die Gebühr wäre %1$s, was mehr als %2$s%% des zu empfangenden Betrags ist. - Ihre Kanäle werden gerade initialisiert und konnten die Zahlung nicht empfangen. Tippen, um zu bestätigen. Details ansehen @@ -252,8 +253,9 @@ Gebühren für die Liquiditätsdienstleistung. Miner-Gebühren Gebühren, die an die Miner des Bitcoin-Netzwerks für die Verarbeitung der On-Chain-Transaktion gezahlt werden. - Dauer - 1 Jahr + Verursacht durch + Diese Liquidität war erforderlich, um eine Zahlung zu erhalten. + Sehen Sie, wie Sie optimieren können @@ -306,7 +308,6 @@ Letzter Versuch (%1$s). Die automatische Gebührenregelung ist deaktiviert. - Channels were still initializing. Dieser Swap wird in einem Tag auslaufen! Dieser Swap wird in %1$s Tagen auslaufen. diff --git a/phoenix-android/src/main/res/values-de/strings.xml b/phoenix-android/src/main/res/values-de/strings.xml index ee56cb041..9b9fb89a8 100644 --- a/phoenix-android/src/main/res/values-de/strings.xml +++ b/phoenix-android/src/main/res/values-de/strings.xml @@ -349,7 +349,8 @@ Kanal-Schließung Migration von Legacy-App Beschleunigte Transaktion - +%1$s eingehende Liquidität + Manuelle Liquidität (+%1$s) + Automatisierte Liquidität Swap-out an %1$s On-Chain Einzahlung diff --git a/phoenix-android/src/main/res/values-es/important_strings.xml b/phoenix-android/src/main/res/values-es/important_strings.xml index 4f9c70115..fc8f138d9 100644 --- a/phoenix-android/src/main/res/values-es/important_strings.xml +++ b/phoenix-android/src/main/res/values-es/important_strings.xml @@ -44,6 +44,8 @@ La tasa era %1$s, pero tu tarifa máxima estaba fijada en %2$s. Este depósito expirará el %3$s. La tasa fue de %1$s, que es más del %2$s%% del importe recibido. Puedes modificarlo en la configuración. La tasa fue de %1$s, que es más del %2$s%% del importe recibido. Este depósito expirará el %3$s. + El importe del pago es demasiado bajo. + Se ha producido un error durante la financiación. Vuelva a intentarlo más tarde. Por favor, inicie Phoenix Es posible que algunos de sus canales hayan cerrado. @@ -226,7 +228,6 @@ La gestión automática de canales está desactivada. La tasa era de %1$s, pero su tasa máxima estaba fijada en %2$s. La tasa ascendía a %1$s, lo que supone más del %2$s%% del importe. - Sus canales aún se están inicializando y no han podido recibir ese pago. Puntee para configurar. Ver detalles @@ -256,8 +257,9 @@ Tasas pagadas por el servicio de liquidez. Tasas mineras Tasa pagada a los mineros de la red Bitcoin para procesar la transacción en cadena. - Duración - 1 año + Causado por + Esta liquidez era necesaria para recibir un pago. + Vea cómo optimizar @@ -307,7 +309,6 @@ La política de tasas automatizadas está desactivada. Los fondos no serán intercambiados. Permanecerán en esta cartera. Último intento (%1$s). La política de tasas automatizadas está desactivada. - Sus canales aún se estaban inicializando. No hay intercambios en curso. Este swap caducará en un día. diff --git a/phoenix-android/src/main/res/values-fr/important_strings.xml b/phoenix-android/src/main/res/values-fr/important_strings.xml index 3ee5a3cac..2750fc855 100644 --- a/phoenix-android/src/main/res/values-fr/important_strings.xml +++ b/phoenix-android/src/main/res/values-fr/important_strings.xml @@ -44,6 +44,8 @@ Les frais étaient de %1$s, mais votre max est de %2$s. Le dépôt expirera le %3$s. Les frais étaient de %1$s et dépassent %2$s%% du montant. Cette configuration peut être changée. Les frais étaient de %1$s et dépassent %2$s%% du montant. Le dépôt expirera le %3$s. + Le montant du paiement est trop faible. + Une erreur s\'est produite lors du financement. Veuillez réessayer plus tard. Veuillez démarrer Phoenix Certains de vos canaux pourraient avoir fermé. @@ -226,7 +228,6 @@ La gestion des canaux automatisée est désactivée. Les frais étaient de %1$s, mais votre max est de %2$s. Les frais de %1$s représentaient plus de %2$s%% du montant. - Vous canaux étaient en initialisation et n\'ont pu recevoir ce paiement. Cliquez pour configurer. Voir les détails @@ -256,8 +257,9 @@ Frais payés pour le service de liquidité. Frais de\nminage Frais payés aux mineurs du réseau Bitcoin pour traiter la transaction sur la chaîne. - Durée - 1 an + Causé par + Cette liquidité était nécessaire pour recevoir un paiement. + Voir comment optimiser @@ -307,7 +309,6 @@ La gestion des canaux automatisée est désactivée. Les fonds ne seront pas basculés sur Lightning et vont rester sur ce wallet. Dernière tentative (%1$s). La gestion des canaux automatisée était désactivée. - Les canaux étaient encore en cours d\'initialisation. Il n\'y a pas de fonds en attente de swap. Ce swap va expirer dans moins d\'une journée! diff --git a/phoenix-android/src/main/res/values-fr/strings.xml b/phoenix-android/src/main/res/values-fr/strings.xml index f88196b24..9fae5c7b3 100644 --- a/phoenix-android/src/main/res/values-fr/strings.xml +++ b/phoenix-android/src/main/res/values-fr/strings.xml @@ -347,7 +347,8 @@ Fermeture de canal Migration depuis l\'ancienne appli Accélération de transactions - +%1$s de liquidité entrante + Liquidité manuelle (+%1$s) + Liquidité automatique Swap-out vers %1$s Dépôt on-chain diff --git a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml index 379065e28..42e914c7c 100644 --- a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml +++ b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml @@ -44,6 +44,8 @@ A taxa foi de %1$s, mas sua taxa máxima foi definida como %2$s. Esse depósito expirará em %3$s. A taxa foi de %1$s, que é mais do que %2$s%% do valor recebido. Você pode ajustar isso nas configurações. A taxa foi de %1$s, que é mais do que %2$s%% do valor recebido. Esse depósito expirará em %3$s. + O valor do pagamento é muito baixo. + Ocorreu um erro durante o financiamento. Tente novamente mais tarde. Por favor, inicie o Phoenix Alguns de seus canais podem ter sido fechados. @@ -225,7 +227,6 @@ O gerenciamento automatizado de canais está desativado. A taxa foi de %1$s, mas sua taxa máxima foi definida como %2$s. A taxa foi de %1$s, que é mais do que %2$s%% do valor. - Seus canais ainda estão sendo inicializados e não puderam receber esse pagamento. Toque para configurar. Ver detalhes @@ -255,8 +256,9 @@ Taxas pagas pelo serviço de liquidez. Taxas do minerador Taxas pagas aos mineradores da rede Bitcoin para processar a transação na cadeia. - Duração - 1 ano + Causado por + Essa liquidez era necessária para receber um pagamento. + Veja como otimizar @@ -302,7 +304,6 @@ A política de taxas automatizadas está desativada. Os fundos não serão trocados. Eles permanecerão nessa carteira. Última tentativa (%1$s). A política de taxas automatizadas está desativada. - Seus canais ainda estavam sendo inicializados. Não há swaps em andamento. Essa troca expirará em um dia! diff --git a/phoenix-android/src/main/res/values-sk/important_strings.xml b/phoenix-android/src/main/res/values-sk/important_strings.xml index 53497fc86..bceba38c0 100644 --- a/phoenix-android/src/main/res/values-sk/important_strings.xml +++ b/phoenix-android/src/main/res/values-sk/important_strings.xml @@ -44,6 +44,8 @@ Poplatok bol %1$s, ale váš maximálny poplatok bol nastavený na %2$s. Platnosť tohto vkladu vyprší %3$s. Poplatok bol %1$s, čo je viac než %2$s%% z prijatej sumy. Kliknite pre podrobnosti. Poplatok bol %1$s, čo je viac než %2$s%% z prijatej sumy. Platnosť tohto vkladu vyprší %3$s. + Výška platby je príliš nízka. + Počas financovania došlo k chybe. Skúste to prosím neskôr. Spustite prosím Phoenix Niektorý z vašich kanálov mohol byť uzavretý. @@ -225,7 +227,6 @@ Automatická správa kanálov je zakázaná. Poplatok bol %1$s, ale váš maximálny poplatok bol nastavený na %2$s. Poplatok bol %1$s, čo je viac než %2$s%% sumy. - Vaše kanály sa stále inicializujú a nemohli prijať túto platbu. Kliknutím na položku vykonajte konfiguráciu. Zobraziť podrobnosti @@ -255,8 +256,9 @@ Poplatky za službu likvidity. Poplatky ťažiarom Poplatky platené ťažiarom Bitcoinovej siete za spracovanie on-chain transakcie. - Trvanie - 1 rok + Spôsobené + Táto likvidita bola potrebná na získanie platby. + Pozrite sa, ako optimalizovať @@ -306,7 +308,6 @@ Automatizovaná poplatková politika je vypnutá. Prostriedky nebudú vymieňané. Zostanú na tejto peňaženke. Posledný pokus (%1$s). Automatizovaná poplatková politika je vypnutá. - Vaše kanály boli inicializované. Neprebiehajú žiadne výmeny. Platnosť tejto výmeny vyprší za deň! diff --git a/phoenix-android/src/main/res/values-sk/strings.xml b/phoenix-android/src/main/res/values-sk/strings.xml index 98502402c..3e0b3ce76 100644 --- a/phoenix-android/src/main/res/values-sk/strings.xml +++ b/phoenix-android/src/main/res/values-sk/strings.xml @@ -369,7 +369,8 @@ Zatváranie kanála Migrácia zo staršej aplikácie Urýchliť transakciu - +%1$s prichádzajúca likvidita + Manuálna likvidita (+%1$s) + Automatizovaná likvidita Swap-out na %1$s On-chain vklad diff --git a/phoenix-android/src/main/res/values-sw/important_strings.xml b/phoenix-android/src/main/res/values-sw/important_strings.xml index 72d18613a..341984669 100644 --- a/phoenix-android/src/main/res/values-sw/important_strings.xml +++ b/phoenix-android/src/main/res/values-sw/important_strings.xml @@ -48,6 +48,8 @@ Ada ilikuwa %1$s, lakini ada yako ya juu ilikuwa imewekwa kwa %2$s. Amana hii itaisha muda wake ifikapo %3$s. Ada ilikuwa %1$s ambayo ni zaidi ya %2$s%% ya kiasi kilichopokelewa. Bonyeza kwa maelezo zaidi. Ada ilikuwa %1$s ambayo ni zaidi ya %2$s%% ya kiasi kilichopokelewa. Amana hii itaisha muda wake ifikapo %3$s. + Kiasi cha malipo ni kidogo sana. + Hitilafu ilitokea wakati wa ufadhili. Tafadhali jaribu tena baadaye. Tafadhali anzisha Phoenix Baadhi ya chaneli zako zinaweza kuwa zimefungwa. @@ -228,7 +230,6 @@ Usimamizi wa njia otomatiki umelemazwa. Ada ilikuwa %1$s, lakini ada yako ya juu ilikuwa imewekwa kwa %2$s. Ada ilikuwa %1$s ambayo ni zaidi ya %2$s%% ya kiasi hicho. - Njia zako bado zinaanzishwa na haziwezi kupokea malipo hayo. Gusa kusanidi. Angalia maelezo @@ -258,8 +259,9 @@ Ada zilizolipwa kwa huduma ya ukwasi. Ada za wachimba madini Ada zilizolipwa kwa wachimba madini wa mtandao wa Bitcoin kushughulikia muamala wa mtandaoni. - Muda - 1 mwaka + Imesababishwa na + Ukwasi huu ulihitajika ili kupokea malipo. + Tazama jinsi ya kuboresha @@ -312,7 +314,6 @@ Jaribio la kubadilisha lilishindikana %1$s Usimamizi wa njia ulikuwa umelemazwa. - Njia zilikuwa bado zinaanzishwa. Swap hii itaisha muda wake ndani ya siku moja! Swap hii itaisha muda wake ndani ya siku %1$s. diff --git a/phoenix-android/src/main/res/values-sw/strings.xml b/phoenix-android/src/main/res/values-sw/strings.xml index 70b4890c2..7aa25759e 100644 --- a/phoenix-android/src/main/res/values-sw/strings.xml +++ b/phoenix-android/src/main/res/values-sw/strings.xml @@ -388,7 +388,8 @@ Kufunga channel Uhamaji kutoka programu ya zamani Kuweka vipaumbele vya muamala - +%1$s fedha za ndani + Ukwasi wa mwongozo (+%1$s) + Ukwasi wa kiotomatiki Kubadilisha kwa %1$s Depo ya on-chain diff --git a/phoenix-android/src/main/res/values-vi/important_strings.xml b/phoenix-android/src/main/res/values-vi/important_strings.xml index b5a0c5784..78daeb64c 100644 --- a/phoenix-android/src/main/res/values-vi/important_strings.xml +++ b/phoenix-android/src/main/res/values-vi/important_strings.xml @@ -51,6 +51,8 @@ Khoản phí là %1$s, nhưng phí tối đa của bạn được đặt là %2$s. Khoản tiền cọc này sẽ hết hạn vào %3$s. Khoản phí là %1$s và cao hơn %2$s%% so với khoản nhận được. Nhấn để biết thêm chi tiết. Khoản phí là %1$s và cao hơn %2$s%% so với khoản tiền sẽ nhận được. Khoản tiền này sẽ hết hạn vào %3$s. + Số tiền thanh toán quá thấp. + Đã xảy ra lỗi trong quá trình cấp vốn. Vui lòng thử lại sau. Xin hãy khởi động Phoenix. Một vài kênh của bạn có thể đã đóng. @@ -232,7 +234,6 @@ Chức năng quản lý kênh tự động bị tắt. Phí là %1$s, tuy nhiên mức phí tối đa của bạn được đặt là %2$s. Phí là %1$s và cao hơn %2$s%% so với số tiền. - Các kênh của bạn vẫn đang được khởi tạo và không thể nhận khoản thanh toán này. Nhấn để định cấu hình. Xem chi tiết @@ -262,8 +263,9 @@ Phí dịch vụ thanh khoản. Phí đào Phí thanh toán cho mạng lưới thợ đào Bitcoin để xử lý giao dịch on-chain. - Thời hạn - 1 năm + Do + Cần có thanh khoản này để nhận được khoản thanh toán. + Xem cách tối ưu hóa @@ -316,7 +318,6 @@ Một giao dịch swap không thành công %1$s Chức năng quản lý kênh tự động đã bị tắt. - Các kênh vẫn đang được khởi tạo. Giao dịch swap này sẽ hết hạn sau 1 ngày nữa! Giao dịch swap này sẽ hết hạn sau %1$s ngày nữa. diff --git a/phoenix-android/src/main/res/values-vi/strings.xml b/phoenix-android/src/main/res/values-vi/strings.xml index 36a841419..66f2eb9b8 100644 --- a/phoenix-android/src/main/res/values-vi/strings.xml +++ b/phoenix-android/src/main/res/values-vi/strings.xml @@ -350,7 +350,8 @@ Đang đóng kênh Di chuyển từ ứng dụng cũ Các giao dịch tăng đột biến - +%1$s thanh khoản đầu vào + Thanh khoản thủ công (+%1$s) + Thanh khoản tự động Swap-out thành %1$s Tiền cọc on-chain diff --git a/phoenix-android/src/main/res/values/important_strings.xml b/phoenix-android/src/main/res/values/important_strings.xml index 253a0e171..f311449a6 100644 --- a/phoenix-android/src/main/res/values/important_strings.xml +++ b/phoenix-android/src/main/res/values/important_strings.xml @@ -48,7 +48,8 @@ The fee was %1$s, but your max fee was set to %2$s. This deposit will expire by %3$s. The fee was %1$s which is more than %2$s%% of the amount received. Tap for details. The fee was %1$s which is more than %2$s%% of the amount received. This deposit will expire by %3$s. - An error occurred during the funding. Please try again later. + Payment amount is too low. + An error occurred during funding. Please try again later. Please start Phoenix Some of your channels may have closed. @@ -229,7 +230,6 @@ Automated channel management is disabled. The fee was %1$s, but your max fee was set to %2$s. The fee was %1$s which is more than %2$s%% of the amount. - Your channels are still initializing and could not receive that payment. Tap to configure. View details @@ -255,12 +255,14 @@ Fees paid to the Bitcoin network miners to process the on-chain transaction. Service fees Fees paid for the creation of a new payment channel. This is not always required. + Service fees Fees paid for the liquidity service. Miner fees Fees paid to the Bitcoin network miners to process the on-chain transaction. - Duration - 1 year + Caused by + This liquidity was required to receive a payment. + See how to optimise @@ -313,9 +315,6 @@ A swap attempt failed %1$s Channels management was disabled. - A funding is in progress. - The amount was too low. - An error has occurred. Try again later. This swap will expire in a day! This swap will expire in %1$s days. diff --git a/phoenix-android/src/main/res/values/strings.xml b/phoenix-android/src/main/res/values/strings.xml index 697f827c6..f9a3e800d 100644 --- a/phoenix-android/src/main/res/values/strings.xml +++ b/phoenix-android/src/main/res/values/strings.xml @@ -391,7 +391,8 @@ Closing channel Migration from legacy app Bump transactions - +%1$s inbound liquidity + Manual liquidity (+%1$s) + Automated liquidity Swap-out to %1$s On-chain deposit @@ -444,6 +445,7 @@ Offer Bolt12 invoice Metadata + Purchase type Payment status Successful diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/Notification.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/Notification.kt index bd675a454..53b9ce9ec 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/Notification.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/Notification.kt @@ -71,14 +71,6 @@ sealed class Notification { override val source: LiquidityEvents.Source, ) : PaymentRejected() - data class ChannelFundingInProgress( - override val id: UUID, - override val createdAt: Long, - override val readAt: Long?, - override val amount: MilliSatoshi, - override val source: LiquidityEvents.Source, - ) : PaymentRejected() - data class GenericError( override val id: UUID, override val createdAt: Long, diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationDataType.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationDataType.kt index b91cff6f5..0d12f85bd 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationDataType.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationDataType.kt @@ -67,11 +67,6 @@ internal sealed class NotificationData { data class V0(@Serializable val amount: MilliSatoshi, val source: LiquidityEvents.Source) : Disabled() } - sealed class ChannelFundingInProgress : PaymentRejected() { - @Serializable - data class V0(@Serializable val amount: MilliSatoshi, val source: LiquidityEvents.Source) : ChannelFundingInProgress() - } - sealed class MissingOffchainAmountTooLow : PaymentRejected() { @Serializable data class V0(@Serializable val amount: MilliSatoshi, val source: LiquidityEvents.Source) : MissingOffchainAmountTooLow() @@ -113,7 +108,6 @@ internal sealed class NotificationData { is Notification.OverAbsoluteFee -> PaymentRejected.OverAbsoluteFee.V0(amount, source, fee, maxAbsoluteFee) is Notification.OverRelativeFee -> PaymentRejected.OverRelativeFee.V0(amount, source, fee, maxRelativeFeeBasisPoints) is Notification.FeePolicyDisabled -> PaymentRejected.Disabled.V0(amount, source) - is Notification.ChannelFundingInProgress -> PaymentRejected.ChannelFundingInProgress.V0(amount, source) is Notification.MissingOffChainAmountTooLow -> PaymentRejected.MissingOffchainAmountTooLow.V0(amount, source) is Notification.GenericError -> PaymentRejected.GenericError.V0(amount, source) is fr.acinq.phoenix.data.WatchTowerOutcome.Nominal -> WatchTowerOutcome.Nominal.V0(channelsWatchedCount) diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationsQueries.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationsQueries.kt index 260c55e9b..6100e2cb3 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationsQueries.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/notifications/NotificationsQueries.kt @@ -46,7 +46,6 @@ internal class NotificationsQueries(val database: AppDatabase) { is Notification.OverAbsoluteFee -> "PAYMENT_REJECTED_OVER_ABSOLUTE_FEE" is Notification.OverRelativeFee -> "PAYMENT_REJECTED_OVER_RELATIVE_FEE" is Notification.FeePolicyDisabled -> "PAYMENT_REJECTED_POLICY_DISABLED" - is Notification.ChannelFundingInProgress -> "PAYMENT_REJECTED_CHANNEL_FUNDING_IN_PROGRESS" is Notification.MissingOffChainAmountTooLow -> "PAYMENT_REJECTED_OFFCHAIN_AMOUNT_TOO_LOW" is Notification.GenericError -> "PAYMENT_REJECTED_GENERIC_ERROR" is WatchTowerOutcome.Nominal -> "WATCH_TOWER_NOMINAL" @@ -142,13 +141,6 @@ internal class NotificationsQueries(val database: AppDatabase) { amount = data.amount, source = data.source, ) - is NotificationData.PaymentRejected.ChannelFundingInProgress.V0 -> Notification.ChannelFundingInProgress( - id = UUID.fromString(id), - createdAt = created_at, - readAt = read_at, - amount = data.amount, - source = data.source, - ) is NotificationData.PaymentRejected.MissingOffchainAmountTooLow.V0 -> Notification.MissingOffChainAmountTooLow( id = UUID.fromString(id), createdAt = created_at, diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NotificationsManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NotificationsManager.kt index 57bab0343..1072adb84 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NotificationsManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NotificationsManager.kt @@ -99,13 +99,6 @@ class NotificationsManager( amount = event.amount, source = event.source, ) - is LiquidityEvents.Rejected.Reason.ChannelFundingInProgress -> Notification.ChannelFundingInProgress( - id = UUID.randomUUID(), - createdAt = currentTimestampMillis(), - readAt = null, - amount = event.amount, - source = event.source, - ) is LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow -> Notification.MissingOffChainAmountTooLow( id = UUID.randomUUID(), createdAt = currentTimestampMillis(), @@ -113,6 +106,7 @@ class NotificationsManager( amount = event.amount, source = event.source, ) + is LiquidityEvents.Rejected.Reason.ChannelFundingInProgress, is LiquidityEvents.Rejected.Reason.NoMatchingFundingRate, is LiquidityEvents.Rejected.Reason.TooManyParts -> Notification.GenericError( id = UUID.randomUUID(), 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 f770a7be3..2f0c5a48b 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 @@ -3,6 +3,7 @@ package fr.acinq.phoenix.utils import fr.acinq.lightning.db.* import fr.acinq.lightning.payment.OfferPaymentMetadata import fr.acinq.phoenix.data.WalletPaymentInfo +import fr.acinq.phoenix.utils.extensions.isManualPurchase import kotlinx.datetime.Instant class CsvWriter { @@ -135,7 +136,9 @@ class CsvWriter { is SpliceOutgoingPayment -> "Outgoing splice to ${payment.address}" is ChannelCloseOutgoingPayment -> "Channel closing to ${payment.address}" is SpliceCpfpOutgoingPayment -> "Accelerate transactions with CPFP" - is InboundLiquidityOutgoingPayment -> "Inbound liquidity purchase (+${payment.purchase.amount.sat} sat)" + is InboundLiquidityOutgoingPayment -> + if (payment.isManualPurchase()) "Manual liquidity (+${payment.purchase.amount.sat} sat)" + else "Automated liquidity (+${payment.purchase.amount.sat} sat)" } row += ",${processField(details)}" } From 3ddc32354170429f014f4858d3c3321dd8cfdbed Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:04:47 +0200 Subject: [PATCH 12/25] (android) Complete Portuguese (BR) translation --- .../src/main/res/values-b+es+419/strings.xml | 2 +- .../src/main/res/values-pt-rBR/strings.xml | 724 ++++++++++++++++++ 2 files changed, 725 insertions(+), 1 deletion(-) create mode 100644 phoenix-android/src/main/res/values-pt-rBR/strings.xml diff --git a/phoenix-android/src/main/res/values-b+es+419/strings.xml b/phoenix-android/src/main/res/values-b+es+419/strings.xml index c293e7976..a04d13546 100644 --- a/phoenix-android/src/main/res/values-b+es+419/strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/strings.xml @@ -448,7 +448,7 @@ Cambiar esquema Iniciar sesión Intentar de nuevo - Iniciando sesión en%1$s. + Iniciando sesión en\n%1$s. Autenticación correcta. Error de autenticación: Error de red. Comprueba tu conexión a Internet y vuelve a intentarlo. diff --git a/phoenix-android/src/main/res/values-pt-rBR/strings.xml b/phoenix-android/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000..510d5fdf1 --- /dev/null +++ b/phoenix-android/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,724 @@ + + + + + + Observador de canais + Exibido quando você precisa iniciar o Phoenix. + + Pagamento pendente + Informa quando o Phoenix deve ser iniciado para efetuar um pagamento. + + Pagamento rejeitado + Exibido quando o Phoenix não consegue receber um pagamento devido a um problema de liquidez. + + Pagamento recebido + Exibido quando você recebe um pagamento enquanto o aplicativo está em segundo plano. + + Execução em segundo plano + Informa quando o Phoenix está sendo executado em segundo plano. + + Tempo limite de troca + Informa quando uma troca expirará. + + + + Criando sua carteira… + Erro ao criar carteira + + Restaurar minha carteira + Próximo + Restaurando sua carteira… + + + + Desbloquear para continuar + + + + Vinculando o serviço… + Preparando a carteira… + Verificando carteira herdada… + Desbloqueando… + Acesso concedido… + Falha no desbloqueio + Erro do Android Keystore + Desbloquear com seed + + Digite suas 12 palavras de recuperação para desbloquear a carteira.\n\nAs palavras devem ser digitadas na ordem correta e separadas por um único espaço. + Digite a palavra #%1$d + Não insira mais de 12 palavras! + Insira a semente completa para continuar. + Esta semente é inválida. Verifique se as palavras estão corretas e em ordem. + Desbloquear carteira + Verificando a semente… + Ocorreu um erro. Tente novamente. + Esta semente não corresponde à carteira. + Correspondências iniciais.\nGravando no disco… + Iniciando o Phoenix… + + Iniciando… + Não foi possível iniciar o aplicativo. + + + + Aplicativo legado + Reinicie o aplicativo legado + + + + - + + + Aguardando confirmação + Pagamento pendente + Pagamento confirmado + Pagamento concluído + Erro ao processar o pagamento + + + + Fatura ou código QR do endereço + + Bitcoin + Este código QR é um endereço Bitcoin clássico.\n\nQuase qualquer serviço ou carteira Bitcoin pode lê-lo, mas os pagamentos levarão mais tempo para chegar. + Endereço + Não foi possível gerar um endereço Bitcoin + Endereço Bitcoin + Compartilhe este endereço Bitcoin com… + + Lightning + Este código QR é uma fatura Lightning.\n\nOs pagamentos Lightning são muito rápidos e geralmente mais baratos, mas podem ainda não ser suportados por algumas carteiras e serviços.\n\nNesse caso, deslize para a esquerda para obter um endereço Bitcoin normal. + Gerando… + Valor + Desc + Não foi possível gerar a fatura + fatura relâmpago + Compartilhe este endereço do Lightning com… + Verificar + + Editar + Editar fatura + Valor (opcional) + Valor a ser recebido + Descrição (opcional) + Insira uma descrição para esta fatura + Criar fatura + + + + Saldo: + O valor é muito alto. + O valor não pode ser negativo. + Este valor é inválido. + Este valor excede o saldo. + Não é possível pagar mais de %1$s. + Este valor é menor que o valor solicitado de %1$s. + Enviar para + Descrição + Comissão + N/A + Carregando comissão… + Fatura sem valor + A fatura deste pagamento não solicita um valor específico. Nós maliciosos podem tirar vantagem disso durante a finalização da compra.\n\nPara maior segurança, peça ao destinatário para especificar um valor ao gerar a fatura. + Aguardando canais… + Pagar + + Pagar em cadeia + As transações on-chain são normalmente mais lentas e mais adequadas para fazer grandes pagamentos. + Pague com Lightning + Os pagamentos relâmpago são rápidos e mais adequados para valores baixos. + + Endereço + Taxa de comissão + Obtendo a taxa de comissão atual… + + Insira uma taxa de comissão válida. + Total excede o saldo + Erro ao processar o pagamento + + Preparar transação + Preparando transação… + Executando emenda… + Taxa de mineração + Usa uma taxa de comissão efetiva de %1$s sat/vbyte. + Total + + + + Entrada manual + Colar dados da área de transferência + Toque para conceder permissão de câmera + Permissão de câmera negada + + Obtendo dados do serviço… + + Entrada manual + Insira uma fatura Lightning, LNURL ou endereço Lightning para o qual deseja enviar dinheiro. + Fatura ou endereço Lightning + + + + + ≈ %1$s + Taxa de câmbio BTC não disponível + + agora + N/A + Carregando dados… + Carregando preferências… + Copiado para a área de transferência + + Abrir link em um navegador + Abrir transação em um explorador + Este campo não pode ficar vazio + Insira um valor válido + Insira um número válido + Este campo deve conter um número inteiro + + Retornar + Próximo + Copiar + OK + Salvar + Confirmar + Cancelar + Fechar + + + + Esvaziar minha carteira + Carregando… + Verificando saldo… + Saldo: %1$s (≈ %2$s) + A carteira não possui canais que possam ser fechados. + Esvaziar minha carteira + Este endereço usa um blockchain diferente + Este endereço usa funções não suportadas + Este endereço não é suportado + %1$d canal(is) fechado(s). Você pode encontrar os detalhes na tela inicial. + + Não há canais que possam ser fechados. + + + + Canais de pagamento + Importar canais + Saldo + O saldo é a quantidade total de canais ativos. Isso é o que você pode gastar no Lightning. + Liquidez de entrada + Liquidez de entrada é o que os canais podem receber via Lightning sem precisar entrar na rede e pagar taxas. + Carregando dados do canal… + Você ainda não tem canais.\n\nUm novo canal pago será criado automaticamente quando necessário. + + + + Detalhes do canal + Não há canal ativo para esse identificador. + Identificador do canal + Estado + Saldo + + Compromissos ativos + Compromissos inativos + Transação de financiamento: + Saldo: + Capacidade: + Iniciado por: + + Exibir dados brutos + Compartilhar + Dados do canal + Compartilhar dados do canal + + + + Importar dados brutos do canal + Esta tela é uma ferramenta de depuração que pode ser usada para importar manualmente dados criptografados do canal.\n\nUse com cuidado. + Blob de dados + Importar + Importando dados… + Importação bem-sucedida + Você deve reiniciar o Phoenix agora. + Erro de importação + O formato dos dados está incorreto. É esperado um blob hexadecimal criptografado. + Não foi possível descriptografar os dados com esta carteira. + A versão %1$d não é suportada + + + + Carregando… + +%1$s + + Perguntas frequentes + Use os botões Receber e Enviar na parte inferior desta tela para começar. + Mostrar todos os pagamentos… + Atraso de Eletro + Certificado Electro + Conectando… + Tor + + + + Notificações + Mensagens importantes + Atividade recente + Nenhuma notificação ainda + + + + Frase de recuperação + Desbloqueando semente… + Não foi possível desbloquear a semente + Semente BIP39 com caminho de derivação BIP84 padrão + Carregando preferências… + Confirmação de backup + + + + Logs de aplicativos + Exportando registros… + Erro: não foi possível exportar logs + Ver registros… + Ver registros com… + Compartilhar registros… + Logs do aplicativo Phoenix + Compartilhe registros do Phoenix… + + + + Carregando detalhes de pagamento… + Detalhes do pagamento não encontrados + + COMPLETO %1$s + ENVIADO %1$s + Pendente… + FALHA\nNenhum dinheiro foi enviado. + + Ainda não recebido. + Aguardando a abertura do canal + RECEBIDO %1$s + + Aguardando confirmações + Obtendo status… + 0 confirmações + Toque para acelerar + %1$d confirmações + %1$d/%2$d confirmação(ões) + Confirmado em string + + Serviço por + Mensagem + Link + Abrir link + Mensagem + Descriptografando mensagem… + + Descrição + Enviado para + Mineradores de Bitcoin + Taxas + Erro + Sem descrição + Este pagamento foi feito devido a um conflito em um canal. + + Pagamento em cadeia + Encerrando canal + Migração de aplicativo legado + Impulsionar transações + Liquidez manual (+%1$s) + Liquidez automatizada + Trocar para %1$s + Depósito na rede + + %1$s + de %1$s + + Adicione uma descrição personalizada para este pagamento + Descrição + Anexar nota + Editar nota + Detalhes técnicos + + + + Detalhes técnicos + Tipo de pagamento + Pagamento pelo encerramento do canal + Pagamento pela emenda de saída + Acelere as transações na rede + Pagamento relâmpago (padrão) + Recebimento de pagamento relâmpago (padrão) + Depósito de Bitcoin para troca + Trocar para endereço Bitcoin + Endereço de depósito + Endereço Bitcoin + + Canal emendado + Transações + - #%1$s: + + Tipo de fechamento + Muto + Local + Remoto + Revogado + Outro + + Chave pública de destino + Descrição da fatura + Hash de pagamento + Fatura + Pré-imagem + + Status do pagamento + Sucesso + Confirmando + Pendente + Erro + + Partes do pagamento (%1$d) + Parte + Lúpulos + + Recebido via + Splice (adicionando ao canal existente) + Novo canal (criado automaticamente) + Pagamento relâmpago + Identificador do canal + Transação + + Criado em + Concluído em + Decorrido + %1$s ms + + Valor solicitado + Valor enviado (comissões incluídas) + Valor recebido + ≈ %1$s (agora) + ≈ %1$s (então) + + + + Acesso ao aplicativo + + Não é possível ativar o bloqueio de tela + Não há hardware de autenticação adequado neste dispositivo. + O hardware biométrico não está disponível. Tente novamente mais tarde. + Primeiro registre um PIN, esquema ou impressão digital no Android. + O hardware não é seguro. É necessária uma atualização de segurança do Android. + Não compatível com esta versão do Android. + Muitas tentativas, tente novamente mais tarde. + Erro não tratado do fornecedor de hardware + Tempo limite da tentativa de autenticação expirou + A autenticação foi cancelada + Esta versão do Android não é compatível. + Código de erro não tratado: %1$d + + Bloqueio de tela do sistema + Bloqueie o acesso ao aplicativo com bloqueio de tela do Android ou impressão digital. + + + + Modo legado + Toque aqui para obter mais informações + Como funciona? + Phoenix autêntica com uma chave exclusiva para %1$s. Essa chave exclusiva se torna a senha da sua conta. + Privacidade + O serviço não terá acesso à sua carteira. Eles não poderão ver seu saldo, seus pagamentos ou suas senhas. + Modo legado + Phoenix usa um esquema não padrão neste serviço para ser compatível com versões mais antigas do aplicativo. A conta associada não pode ser transferida para outras carteiras. + Alterar esquema + Login + Tente novamente + Efetuando login em\n%1$s. + Autenticação bem-sucedida. + Erro de autenticação: + Erro de rede. Verifique sua conexão com a Internet e tente novamente. + Ocorreu um erro desconhecido. Tente novamente + + + + Padrão + Usa um esquema padrão que está em conformidade com as especificações LNURL. Esta é a opção recomendada para novas carteiras e usada pelo aplicativo Phoenix iOS. + Android Legacy + Use um esquema legado para se conectar a contas criadas com o antigo aplicativo Phoenix para Android. + + + + Resgatar + Solicitando fundos… + O valor deve ser de pelo menos %1$s. + O valor não pode exceder %1$s. + Erro de retirada: + + + + O serviço %1$s respondeu com um erro. Entre em contato com o suporte técnico, se necessário.\n\nDetalhes da mensagem de serviço: \"%2$s\" + O serviço %1$s retornou um erro HTTP (%2$s).\n\nEntre em contato com o suporte técnico, se necessário. + O serviço %1$s retornou uma mensagem malformada. + Não foi possível conectar ao serviço %1$s. + + + + Atendido por + Descrição + Anexar uma mensagem + Minha mensagem + Você pode anexar uma mensagem ao pagamento. Esta mensagem será enviada ao destinatário. + Pagar + Solicitando fatura… + + O valor deve ser de pelo menos %1$s. + O valor deve ser no máximo %1$s. + + Erro ao processar o pagamento. + A fatura retornada por %1$s não usa a mesma cadeia de carteira. + A fatura devolvida por %1$s já foi paga. + A fatura retornada por %1$s tem um valor incorreto. + A fatura retornada por %1$s está malformada + + + + Opções de exibição + Unidade Bitcoin + Satoshi (sat) + 1 sat equivale a 0,00000001 BTC + Bit (bit) + 1 bit equivale a 0,000001 BTC + Milibitcoin (mBTC) + 1 mBTC equivale a 0,001 BTC + Bitcoin (BTC) + Moeda fiduciária + Tema do aplicativo + Tema escuro + Tema claro + Igual ao sistema + Idioma do aplicativo + + + + Servidor Electrum + Para proteger seus canais de pagamento, a Phoenix monitora o blockchain do Bitcoin por meio de servidores Electrum.\n\nPor padrão, são usados servidores aleatórios. Você também pode configurar o Phoenix para se conectar apenas ao seu próprio servidor. + Altura do bloco + + Desconectado da Electrum + Desconectado de %1$s + Conectando-se a %1$s (aleatório) + Conectando-se a %1$s + Conectando-se a %1$s + + Você está usando um servidor personalizado + Este servidor forneceu um certificado desconhecido. A conexão foi recusada. + + Use um servidor personalizado + Endereço do servidor (host:porta) + Este endereço é inválido. + Conectar + Verificando certificado… + Erro de conexão:\n%1$s + Este endereço não pode ser resolvido. + Certificado não confiável + Impressão digital SHA1 + Impressão digital SHA256 + Emissor + Assunto + Válido até + Copiar certificado + Certificado de confiança + + + + Tor + Verificando preferências… + Tor está ativado + Tor está desabilitado + O proxy Tor ainda não foi iniciado. + O proxy Tor está desativado.\nAguarde e verifique sua conexão com a Internet. + Iniciando o proxy Tor… + Tor está online + + + + Status da conexão + O aplicativo não funcionará corretamente até que todas as conexões tenham sido estabelecidas. + Seu dispositivo não possui conexão com a Internet. O app não funcionará corretamente.\n\nVerifique as configurações do seu dispositivo. + Internet + Electrum + Tor + Par + Gerenciar conexão para %1$s + Conectando… + Conectado + O servidor Electrum registra um atraso (bloco $1$d) + Desconectado + Certificado com defeito + + + + Informações sobre Phoenix + Versão Phoenix: %1$s + Você tem dúvidas? Verifique as perguntas frequentes + Suporte + Privacidade + Condições + + + + Opções de pagamento + + Pagamentos recebidos + LNURL + + Descrição da fatura + Nenhuma descrição definida… + Descrição padrão + As faturas usarão esta descrição por padrão. Você pode cancelar caso a caso. + Descrição da fatura + + Expiração da fatura + Expiração da fatura + As faturas criadas expiram após esse período. O valor padrão é 1 semana. + 1 hora + 1 dia + 1 semana (padrão) + 2 semanas + 3 semanas + %1$s segundos + + Esquema de autenticação LNURL + + + + Peso argentino (taxa oficial) + Peso argentino + Peso cubano (taxa oficial) + Peso cubano + Libra libanesa (taxa oficial) + Libra libanesa + + + + Pagamentos locais (%1$d) + Exportar + Hoje + Ontem + Esta semana + Semana passada + Exportar pagamentos + Exporte pagamentos locais bem-sucedidos em formato CSV (valores separados por vírgula). + Data de início + Data de término + Incluir origem/destino + Incluir descrição + Exportar + Pagamentos ainda não efetuados + Escolha uma data de início/término válida + Exportando pagamento #%1$d + %1$d pagamentos exportados com sucesso + Phoenix - pagamentos de %1$s a %2$s + Compartilhe pagamentos Phoenix… + Compartilhar arquivo + Erro de exportação. + Nenhum pagamento encontrado. + + + + (par) + + + + Informações da carteira + Descritor + Chave pública mestra + (Caminho: %1$s) + Pronto para trocar + Aguardando %1$d confirmações + +%1$d mais… + Saldo confirmado + Saldo não confirmado + +%1$s recebidos + Carregando dados da carteira… + + Lightning + Identificador do nó + Identificador de nó legado + + Carteira final + + + + Gerenciamento de canais + Obtendo taxa de comissão… + Minhas configurações de taxas + Gerenciamento avançado de canais + Obtendo política… + + + + Taxa de comissão + %1$s sat/vbyte + + Preparar pagamento + Calculando comissões… + Você pagará %1$s aos mineradores de Bitcoin + Executar pagamento + Executando pagamento… + Pagamento concluído + Erro ao executar o pagamento + Não é possível continuar + + + + Status do pool de memória desconhecido + O Phoenix não conseguiu obter o estado atual do pool de memória, portanto não pode calcular a velocidade da transação.\n\nVerifique o pool de memória manualmente em um navegador e use um valor apropriado . + ≈ Próximo bloco + ≈ 30 minutos + ≈ 1 hora + Comissão baixa + + + + Você não tem canais + Abortado pelo par [%1$s] + Não é possível criar um novo commit + O canal está desconectado + Erro de financiamento [%1$s] + Fundos insuficientes + Não é possível iniciar a sessão de transação com peer + Erro de sessão de transação interativa [%1$s] + O script de chave pública de emenda é inválido + Um pagamento de emenda já está em andamento + + + + Redefinir carteira + Excluir todos os dados da carteira neste dispositivo + Isso redefinirá o aplicativo, como se você tivesse acabado de instalá-lo. + Analisar + + Confirmar redefinição + A carteira será completamente excluída neste dispositivo. + Isso redefinirá o aplicativo. Iremos direcioná-lo para a tela de introdução, onde solicitaremos que você crie ou restaure uma carteira. + Não perca seus fundos:\n%1$s (≈ %2$s). + Você é responsável por salvar sua frase de recuperação. + Excluir carteira + + A carteira foi redefinida com sucesso + Redefinir erro + + From bc48b8b0b344323219d59a72b53ddf46f4b283ae Mon Sep 17 00:00:00 2001 From: Robbie Hanson <304604+robbiehanson@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:26:37 -0500 Subject: [PATCH 13/25] (ios) Fix compilation on the command line --- .../acinq/phoenix/utils/LightningExposure.kt | 358 +++++++++--------- .../fr/acinq/phoenix/utils/PhoenixExposure.kt | 8 +- 2 files changed, 183 insertions(+), 183 deletions(-) diff --git a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/LightningExposure.kt b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/LightningExposure.kt index eb8553520..25b3adac1 100644 --- a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/LightningExposure.kt +++ b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/LightningExposure.kt @@ -273,75 +273,75 @@ fun LiquidityPolicy.asAuto(): LiquidityPolicy.Auto? = when (this) { else -> null } -fun ChannelCommand.Commitment.Splice.Response.asCreated(): ChannelCommand.Commitment.Splice.Response.Created? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Created -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.asFailure(): ChannelCommand.Commitment.Splice.Response.Failure? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asInsufficientFunds(): ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asInvalidSpliceOutPubKeyScript(): ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asSpliceAlreadyInProgress(): ChannelCommand.Commitment.Splice.Response.Failure.SpliceAlreadyInProgress? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.SpliceAlreadyInProgress -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asChannelNotQuiescent(): ChannelCommand.Commitment.Splice.Response.Failure.ChannelNotQuiescent? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.ChannelNotQuiescent -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asConcurrentRemoteSplice(): ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asInvalidLiquidityAds(): ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asFundingFailure(): ChannelCommand.Commitment.Splice.Response.Failure.FundingFailure? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.FundingFailure -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asCannotStartSession(): ChannelCommand.Commitment.Splice.Response.Failure.CannotStartSession? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.CannotStartSession -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asInteractiveTxSessionFailed(): ChannelCommand.Commitment.Splice.Response.Failure.InteractiveTxSessionFailed? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.InteractiveTxSessionFailed -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asCannotCreateCommitTx(): ChannelCommand.Commitment.Splice.Response.Failure.CannotCreateCommitTx? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.CannotCreateCommitTx -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asAbortedByPeer(): ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer -> this - else -> null -} - -fun ChannelCommand.Commitment.Splice.Response.Failure.asDisconnected(): ChannelCommand.Commitment.Splice.Response.Failure.Disconnected? = when (this) { - is ChannelCommand.Commitment.Splice.Response.Failure.Disconnected -> this - else -> null -} +//fun ChannelCommand.Commitment.Splice.Response.asCreated(): ChannelCommand.Commitment.Splice.Response.Created? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Created -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.asFailure(): ChannelCommand.Commitment.Splice.Response.Failure? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asInsufficientFunds(): ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asInvalidSpliceOutPubKeyScript(): ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asSpliceAlreadyInProgress(): ChannelCommand.Commitment.Splice.Response.Failure.SpliceAlreadyInProgress? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.SpliceAlreadyInProgress -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asChannelNotQuiescent(): ChannelCommand.Commitment.Splice.Response.Failure.ChannelNotQuiescent? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.ChannelNotQuiescent -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asConcurrentRemoteSplice(): ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asInvalidLiquidityAds(): ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asFundingFailure(): ChannelCommand.Commitment.Splice.Response.Failure.FundingFailure? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.FundingFailure -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asCannotStartSession(): ChannelCommand.Commitment.Splice.Response.Failure.CannotStartSession? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.CannotStartSession -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asInteractiveTxSessionFailed(): ChannelCommand.Commitment.Splice.Response.Failure.InteractiveTxSessionFailed? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.InteractiveTxSessionFailed -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asCannotCreateCommitTx(): ChannelCommand.Commitment.Splice.Response.Failure.CannotCreateCommitTx? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.CannotCreateCommitTx -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asAbortedByPeer(): ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer -> this +// else -> null +//} +// +//fun ChannelCommand.Commitment.Splice.Response.Failure.asDisconnected(): ChannelCommand.Commitment.Splice.Response.Failure.Disconnected? = when (this) { +// is ChannelCommand.Commitment.Splice.Response.Failure.Disconnected -> this +// else -> null +//} suspend fun ElectrumClient.kotlin_getConfirmations(txid: TxId): Int? { return this.getConfirmations(txid) @@ -500,116 +500,116 @@ fun ByteArray_toNSData(buffer: ByteArray): NSData = buffer.toNSData() * which (in my experience) fixes all these problems. * But in the meantime, we're working around it by exposing our own class & wrapper functions. */ -data class LiquidityAds_LeaseRate( - val leaseDuration: Int, - val fundingWeight: Int, - val leaseFeeProportional: Int, - val leaseFeeBase: Satoshi, - val maxRelayFeeProportional: Int, - val maxRelayFeeBase: MilliSatoshi -) { - constructor(src: LiquidityAds.LeaseRate) : this( - leaseDuration = src.leaseDuration, - fundingWeight = src.fundingWeight, - leaseFeeProportional = src.leaseFeeProportional, - leaseFeeBase = src.leaseFeeBase, - maxRelayFeeProportional = src.maxRelayFeeProportional, - maxRelayFeeBase = src.maxRelayFeeBase - ) - fun unwrap() = LiquidityAds.LeaseRate( - leaseDuration = this.leaseDuration, - fundingWeight = this.fundingWeight, - leaseFeeProportional = this.leaseFeeProportional, - leaseFeeBase = this.leaseFeeBase, - maxRelayFeeProportional = this.maxRelayFeeProportional, - maxRelayFeeBase = this.maxRelayFeeBase - ) -} - -data class LiquidityAds_LeaseFees( - val miningFee: Satoshi, - val serviceFee: Satoshi -) { - constructor(src: LiquidityAds.LeaseFees) : this( - miningFee = src.miningFee, - serviceFee = src.serviceFee - ) - fun unwrap() = LiquidityAds.LeaseFees( - miningFee = this.miningFee, - serviceFee = this.serviceFee - ) - - val total: Satoshi = unwrap().total -} - -data class LiquidityAds_LeaseWitness( - val fundingScript: ByteVector, - val leaseDuration: Int, - val leaseEnd: Int, - val maxRelayFeeProportional: Int, - val maxRelayFeeBase: MilliSatoshi -) { - constructor(src: LiquidityAds.LeaseWitness) : this( - fundingScript = src.fundingScript, - leaseDuration = src.leaseDuration, - leaseEnd = src.leaseEnd, - maxRelayFeeProportional = src.maxRelayFeeProportional, - maxRelayFeeBase = src.maxRelayFeeBase - ) - fun unwrap() = LiquidityAds.LeaseWitness( - fundingScript = this.fundingScript, - leaseDuration = this.leaseDuration, - leaseEnd = this.leaseEnd, - maxRelayFeeProportional = this.maxRelayFeeProportional, - maxRelayFeeBase = this.maxRelayFeeBase - ) - - fun sign(nodeKey: PrivateKey): ByteVector64 = unwrap().sign(nodeKey) - fun verify(nodeId: PublicKey, sig: ByteVector64): Boolean = unwrap().verify(nodeId, sig) - fun encode(): ByteArray = unwrap().encode() -} - -data class LiquidityAds_Lease( - val amount: Satoshi, - val fees: LiquidityAds_LeaseFees, - val sellerSig: ByteVector64, - val witness: LiquidityAds_LeaseWitness -) { - constructor(src: LiquidityAds.Lease) : this( - amount = src.amount, - fees = LiquidityAds_LeaseFees(src.fees), - sellerSig = src.sellerSig, - witness = LiquidityAds_LeaseWitness(src.witness) - ) - fun unwrap() = LiquidityAds.Lease( - amount = this.amount, - fees = this.fees.unwrap(), - sellerSig = this.sellerSig, - witness = this.witness.unwrap() - ) - - val start: Int = unwrap().start - val expiry: Int = unwrap().expiry -} - -suspend fun Peer._estimateFeeForInboundLiquidity( - amount: Satoshi, - targetFeerate: FeeratePerKw, - leaseRate: LiquidityAds_LeaseRate -): Pair? { - return this.estimateFeeForInboundLiquidity(amount, targetFeerate, leaseRate.unwrap()) -} - -suspend fun Peer._requestInboundLiquidity( - amount: Satoshi, - feerate: FeeratePerKw, - leaseRate: LiquidityAds_LeaseRate -): ChannelCommand.Commitment.Splice.Response? { - return this.requestInboundLiquidity(amount, feerate, leaseRate.unwrap()) -} - -val InboundLiquidityOutgoingPayment._lease: LiquidityAds_Lease - get() = LiquidityAds_Lease(this.lease) +//data class LiquidityAds_LeaseRate( +// val leaseDuration: Int, +// val fundingWeight: Int, +// val leaseFeeProportional: Int, +// val leaseFeeBase: Satoshi, +// val maxRelayFeeProportional: Int, +// val maxRelayFeeBase: MilliSatoshi +//) { +// constructor(src: LiquidityAds.LeaseRate) : this( +// leaseDuration = src.leaseDuration, +// fundingWeight = src.fundingWeight, +// leaseFeeProportional = src.leaseFeeProportional, +// leaseFeeBase = src.leaseFeeBase, +// maxRelayFeeProportional = src.maxRelayFeeProportional, +// maxRelayFeeBase = src.maxRelayFeeBase +// ) +// fun unwrap() = LiquidityAds.LeaseRate( +// leaseDuration = this.leaseDuration, +// fundingWeight = this.fundingWeight, +// leaseFeeProportional = this.leaseFeeProportional, +// leaseFeeBase = this.leaseFeeBase, +// maxRelayFeeProportional = this.maxRelayFeeProportional, +// maxRelayFeeBase = this.maxRelayFeeBase +// ) +//} + +//data class LiquidityAds_LeaseFees( +// val miningFee: Satoshi, +// val serviceFee: Satoshi +//) { +// constructor(src: LiquidityAds.LeaseFees) : this( +// miningFee = src.miningFee, +// serviceFee = src.serviceFee +// ) +// fun unwrap() = LiquidityAds.LeaseFees( +// miningFee = this.miningFee, +// serviceFee = this.serviceFee +// ) +// +// val total: Satoshi = unwrap().total +//} + +//data class LiquidityAds_LeaseWitness( +// val fundingScript: ByteVector, +// val leaseDuration: Int, +// val leaseEnd: Int, +// val maxRelayFeeProportional: Int, +// val maxRelayFeeBase: MilliSatoshi +//) { +// constructor(src: LiquidityAds.LeaseWitness) : this( +// fundingScript = src.fundingScript, +// leaseDuration = src.leaseDuration, +// leaseEnd = src.leaseEnd, +// maxRelayFeeProportional = src.maxRelayFeeProportional, +// maxRelayFeeBase = src.maxRelayFeeBase +// ) +// fun unwrap() = LiquidityAds.LeaseWitness( +// fundingScript = this.fundingScript, +// leaseDuration = this.leaseDuration, +// leaseEnd = this.leaseEnd, +// maxRelayFeeProportional = this.maxRelayFeeProportional, +// maxRelayFeeBase = this.maxRelayFeeBase +// ) +// +// fun sign(nodeKey: PrivateKey): ByteVector64 = unwrap().sign(nodeKey) +// fun verify(nodeId: PublicKey, sig: ByteVector64): Boolean = unwrap().verify(nodeId, sig) +// fun encode(): ByteArray = unwrap().encode() +//} + +//data class LiquidityAds_Lease( +// val amount: Satoshi, +// val fees: LiquidityAds_LeaseFees, +// val sellerSig: ByteVector64, +// val witness: LiquidityAds_LeaseWitness +//) { +// constructor(src: LiquidityAds.Lease) : this( +// amount = src.amount, +// fees = LiquidityAds_LeaseFees(src.fees), +// sellerSig = src.sellerSig, +// witness = LiquidityAds_LeaseWitness(src.witness) +// ) +// fun unwrap() = LiquidityAds.Lease( +// amount = this.amount, +// fees = this.fees.unwrap(), +// sellerSig = this.sellerSig, +// witness = this.witness.unwrap() +// ) +// +// val start: Int = unwrap().start +// val expiry: Int = unwrap().expiry +//} + +//suspend fun Peer._estimateFeeForInboundLiquidity( +// amount: Satoshi, +// targetFeerate: FeeratePerKw, +// leaseRate: LiquidityAds_LeaseRate +//): Pair? { +// return this.estimateFeeForInboundLiquidity(amount, targetFeerate, leaseRate.unwrap()) +//} +// +//suspend fun Peer._requestInboundLiquidity( +// amount: Satoshi, +// feerate: FeeratePerKw, +// leaseRate: LiquidityAds_LeaseRate +//): ChannelCommand.Commitment.Splice.Response? { +// return this.requestInboundLiquidity(amount, feerate, leaseRate.unwrap()) +//} + +//val InboundLiquidityOutgoingPayment._lease: LiquidityAds_Lease +// get() = LiquidityAds_Lease(this.lease) fun WalletState.WalletWithConfirmations._spendExpiredSwapIn( swapInKeys: KeyManager.SwapInOnChainKeys, diff --git a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/PhoenixExposure.kt b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/PhoenixExposure.kt index 54e5d721a..a64b72bb9 100644 --- a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/PhoenixExposure.kt +++ b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/PhoenixExposure.kt @@ -27,10 +27,10 @@ fun WalletPaymentOrderRow.kotlinId(): WalletPaymentId { return this.id } -fun NodeParamsManager.Companion._liquidityLeaseRate(amount: Satoshi): LiquidityAds_LeaseRate { - val result = this.liquidityLeaseRate(amount) - return LiquidityAds_LeaseRate(result) -} +//fun NodeParamsManager.Companion._liquidityLeaseRate(amount: Satoshi): LiquidityAds_LeaseRate { +// val result = this.liquidityLeaseRate(amount) +// return LiquidityAds_LeaseRate(result) +//} fun LocalChannelInfo.Companion.availableForReceive( channels: List From ef07ee940c67e7aeea4ffc9909679c5550347468 Mon Sep 17 00:00:00 2001 From: Robbie Hanson <304604+robbiehanson@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:27:28 -0500 Subject: [PATCH 14/25] (ios) Project now compiles --- .../phoenix-ios.xcodeproj/project.pbxproj | 8 +- phoenix-ios/phoenix-ios/Localizable.xcstrings | 13 + .../kotlin/KotlinExtensions+Payments.swift | 2 +- .../officers/BusinessManager.swift | 2 +- .../prefs/UserDefaults+Codable.swift | 26 +- .../LiquidityAdsView.swift | 75 +-- .../LiquidityFeeInfo.swift | 2 +- .../phoenix-ios/views/inspect/CpfpView.swift | 4 +- .../views/inspect/DetailsView.swift | 2 +- .../inspect/WalletPaymentExtensions.swift | 2 +- .../notifications/BizNotificationCell.swift | 13 +- ...blem.swift => ChannelFundingProblem.swift} | 27 +- .../phoenix-ios/views/send/ValidateView.swift | 4 +- .../acinq/phoenix/utils/LightningExposure.kt | 498 +++++------------- 14 files changed, 244 insertions(+), 434 deletions(-) rename phoenix-ios/phoenix-ios/views/send/{SpliceOutProblem.swift => ChannelFundingProblem.swift} (72%) diff --git a/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj b/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj index 58c56c6b2..a12ac8023 100644 --- a/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj +++ b/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj @@ -212,6 +212,7 @@ DC81B79F25BF2AA200F5A52C /* MVI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC81B79E25BF2AA200F5A52C /* MVI.swift */; }; DC82EED629789853007A5853 /* TxHistoryExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC82EED529789853007A5853 /* TxHistoryExporter.swift */; }; DC89857F25914747007B253F /* UIApplicationState+Phoenix.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89857E25914747007B253F /* UIApplicationState+Phoenix.swift */; }; + DC8D94142CA7015F00EE844E /* ChannelFundingProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8D94132CA7015F00EE844E /* ChannelFundingProblem.swift */; }; DC9130A02AE045FA00F9B8C6 /* Sequence+Sum.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59377027516296003B4B53 /* Sequence+Sum.swift */; }; DC9473FA261270B4008D7242 /* MVI+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9473F9261270B4008D7242 /* MVI+Mock.swift */; }; DC949E6A2B45B1EC00E80BB5 /* LiquidityAdsHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC949E692B45B1EC00E80BB5 /* LiquidityAdsHelp.swift */; }; @@ -277,7 +278,6 @@ DCB511CE281AED58001BC525 /* phoenix-notifySrvExt.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DCB511C7281AED58001BC525 /* phoenix-notifySrvExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DCB5D2DF280879460020B8F5 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB5D2DE280879460020B8F5 /* DeviceInfo.swift */; }; DCB62F472A5DF19D00912A71 /* KotlinPublishers+Lightning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB62F462A5DF19D00912A71 /* KotlinPublishers+Lightning.swift */; }; - DCB62F492A5E09F900912A71 /* SpliceOutProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB62F482A5E09F900912A71 /* SpliceOutProblem.swift */; }; DCB876302735AA7300657570 /* UserDefaults+Serialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB8762F2735AA7300657570 /* UserDefaults+Serialization.swift */; }; DCB876322735AAB500657570 /* UserDefaults+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB876312735AAB500657570 /* UserDefaults+Codable.swift */; }; DCBA371B2758076F00610EC8 /* SyncSeedManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBA371A2758076F00610EC8 /* SyncSeedManager.swift */; }; @@ -617,6 +617,7 @@ DC81B79E25BF2AA200F5A52C /* MVI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MVI.swift; sourceTree = ""; }; DC82EED529789853007A5853 /* TxHistoryExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxHistoryExporter.swift; sourceTree = ""; }; DC89857E25914747007B253F /* UIApplicationState+Phoenix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplicationState+Phoenix.swift"; sourceTree = ""; }; + DC8D94132CA7015F00EE844E /* ChannelFundingProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelFundingProblem.swift; sourceTree = ""; }; DC9473F9261270B4008D7242 /* MVI+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVI+Mock.swift"; sourceTree = ""; }; DC949E692B45B1EC00E80BB5 /* LiquidityAdsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidityAdsHelp.swift; sourceTree = ""; }; DC98D3952AF170AC005BD177 /* PaymentWarningPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentWarningPopover.swift; sourceTree = ""; }; @@ -672,7 +673,6 @@ DCB511CB281AED58001BC525 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DCB5D2DE280879460020B8F5 /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = ""; }; DCB62F462A5DF19D00912A71 /* KotlinPublishers+Lightning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KotlinPublishers+Lightning.swift"; sourceTree = ""; }; - DCB62F482A5E09F900912A71 /* SpliceOutProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpliceOutProblem.swift; sourceTree = ""; }; DCB8762F2735AA7300657570 /* UserDefaults+Serialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Serialization.swift"; sourceTree = ""; }; DCB876312735AAB500657570 /* UserDefaults+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Codable.swift"; sourceTree = ""; }; DCBA371A2758076F00610EC8 /* SyncSeedManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSeedManager.swift; sourceTree = ""; }; @@ -995,7 +995,7 @@ DC1771B32ABC99CE00B286C7 /* WebsiteLinkPopover.swift */, DC98D3952AF170AC005BD177 /* PaymentWarningPopover.swift */, DCA02B9F2BD1A5FC0080520F /* ChannelSizeImpactWarning.swift */, - DCB62F482A5E09F900912A71 /* SpliceOutProblem.swift */, + DC8D94132CA7015F00EE844E /* ChannelFundingProblem.swift */, DC1718A62C20BF8A000CCAF5 /* PayOfferProblem.swift */, DC6F042C2C3DC4CD00627B4F /* ManualInput.swift */, ); @@ -1886,13 +1886,13 @@ DCE7233027B167240017CF56 /* SyncSeedManager_Actor.swift in Sources */, DCBA371B2758076F00610EC8 /* SyncSeedManager.swift in Sources */, DC39D4F12874DDF40030F18D /* View+If.swift in Sources */, - DCB62F492A5E09F900912A71 /* SpliceOutProblem.swift in Sources */, DC370A8B2B7FFFC70093C56F /* SwapInAddresses.swift in Sources */, 53BEFD54160278C5E393E319 /* HomeView.swift in Sources */, DC0C52662BF3C31700143831 /* WhichPinSheet.swift in Sources */, DCD1208728663F4A00EB39C5 /* TransactionsView.swift in Sources */, DC70A99C2BBB6093002DBFF8 /* InboundFeeWarning.swift in Sources */, DC118BFC27B4504B0080BBAC /* ScanView.swift in Sources */, + DC8D94142CA7015F00EE844E /* ChannelFundingProblem.swift in Sources */, DC46BAF326CACCF700E760A6 /* KotlinExtensions+Other.swift in Sources */, DC118C0427B454720080BBAC /* PaymentInFlightView.swift in Sources */, DCB5D2DF280879460020B8F5 /* DeviceInfo.swift in Sources */, diff --git a/phoenix-ios/phoenix-ios/Localizable.xcstrings b/phoenix-ios/phoenix-ios/Localizable.xcstrings index 999a5f166..ecad7b971 100644 --- a/phoenix-ios/phoenix-ios/Localizable.xcstrings +++ b/phoenix-ios/phoenix-ios/Localizable.xcstrings @@ -9926,6 +9926,9 @@ } } } + }, + "Channel funding in progress." : { + }, "channel id" : { "comment" : "Label in DetailsView_IncomingPayment", @@ -10210,6 +10213,7 @@ } }, "Channels initializing..." : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -23214,6 +23218,9 @@ } } } + }, + "Invalid channel parameters" : { + }, "Invalid date range" : { "localizations" : { @@ -27104,6 +27111,9 @@ } } } + }, + "Missing off-chain amount too low." : { + }, "Modify" : { "localizations" : { @@ -43968,6 +43978,9 @@ } } } + }, + "Unexpected message" : { + }, "Unknown" : { "comment" : "Connection state", diff --git a/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift b/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift index 625d98e06..12546a5c0 100644 --- a/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift +++ b/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift @@ -163,7 +163,7 @@ extension WalletPaymentInfo { return String(localized: "Bump fees", comment: "Payment description for splice CPFP") } else if let il = outgoingPayment as? Lightning_kmpInboundLiquidityOutgoingPayment { - let amount = Utils.formatBitcoin(sat: il._lease.amount, bitcoinUnit: .sat) + let amount = Utils.formatBitcoin(sat: il.purchase.amount, bitcoinUnit: .sat) return String( localized: "+\(amount.string) inbound liquidity", comment: "Payment description for inbound liquidity" diff --git a/phoenix-ios/phoenix-ios/officers/BusinessManager.swift b/phoenix-ios/phoenix-ios/officers/BusinessManager.swift index 7d9d04767..ac5e5412b 100644 --- a/phoenix-ios/phoenix-ios/officers/BusinessManager.swift +++ b/phoenix-ios/phoenix-ios/officers/BusinessManager.swift @@ -93,7 +93,7 @@ class BusinessManager { private init() { // must use shared instance business = PhoenixBusiness(ctx: PlatformContext.default) - BusinessManager._isTestnet = business.chain.isTestnet() + BusinessManager._isTestnet = !business.chain.isMainnet() let nc = NotificationCenter.default diff --git a/phoenix-ios/phoenix-ios/prefs/UserDefaults+Codable.swift b/phoenix-ios/phoenix-ios/prefs/UserDefaults+Codable.swift index a6c0b74de..90f3b2dd8 100644 --- a/phoenix-ios/phoenix-ios/prefs/UserDefaults+Codable.swift +++ b/phoenix-ios/phoenix-ios/prefs/UserDefaults+Codable.swift @@ -106,10 +106,26 @@ struct LiquidityPolicy: Equatable, Codable { ) } + var effectiveInboundLiquidityTargetSats: Int64? { + return NodeParamsManager.companion.defaultLiquidityPolicy.inboundLiquidityTarget?.sat + } + + var effectiveInboundLiquidityTarget: Bitcoin_kmpSatoshi? { + if let sats = effectiveInboundLiquidityTargetSats { + return Bitcoin_kmpSatoshi(sat: sats) + } else { + return nil + } + } + var effectiveMaxFeeSats: Int64 { return maxFeeSats ?? NodeParamsManager.companion.defaultLiquidityPolicy.maxAbsoluteFee.sat } + var effectiveMaxFee: Bitcoin_kmpSatoshi { + return Bitcoin_kmpSatoshi(sat: effectiveMaxFeeSats) + } + var effectiveMaxFeeBasisPoints: Int32 { return maxFeeBasisPoints ?? NodeParamsManager.companion.defaultLiquidityPolicy.maxRelativeFeeBasisPoints } @@ -118,14 +134,20 @@ struct LiquidityPolicy: Equatable, Codable { return skipAbsoluteFeeCheck ?? NodeParamsManager.companion.defaultLiquidityPolicy.skipAbsoluteFeeCheck } + var effectiveMaxAllowedFeeCredit: Lightning_kmpMilliSatoshi { + return NodeParamsManager.companion.defaultLiquidityPolicy.maxAllowedFeeCredit + } + func toKotlin() -> Lightning_kmpLiquidityPolicy { if enabled { return Lightning_kmpLiquidityPolicy.Auto( - maxAbsoluteFee: Bitcoin_kmpSatoshi(sat: effectiveMaxFeeSats), + inboundLiquidityTarget: effectiveInboundLiquidityTarget, + maxAbsoluteFee: effectiveMaxFee, maxRelativeFeeBasisPoints: effectiveMaxFeeBasisPoints, - skipAbsoluteFeeCheck: effectiveSkipAbsoluteFeeCheck + skipAbsoluteFeeCheck: effectiveSkipAbsoluteFeeCheck, + maxAllowedFeeCredit: effectiveMaxAllowedFeeCredit ) } else { diff --git a/phoenix-ios/phoenix-ios/views/configuration/fees/liquidity management/LiquidityAdsView.swift b/phoenix-ios/phoenix-ios/views/configuration/fees/liquidity management/LiquidityAdsView.swift index 10be646bc..f06ec01f8 100644 --- a/phoenix-ios/phoenix-ios/views/configuration/fees/liquidity management/LiquidityAdsView.swift +++ b/phoenix-ios/phoenix-ios/views/configuration/fees/liquidity management/LiquidityAdsView.swift @@ -24,7 +24,7 @@ struct LiquidityAdsView: View { @State var sliderValue: Double = 0 @State var feeInfo: LiquidityFeeInfo? = nil - @State var finalResult: Lightning_kmpChannelCommand.CommitmentSpliceResponse? = nil + @State var finalResult: Lightning_kmpChannelFundingResponse? = nil @State var isEstimating: Bool = false @State var isPurchasing: Bool = false @@ -474,12 +474,12 @@ struct LiquidityAdsView: View { @ViewBuilder func section_result( - _ finalResult: Lightning_kmpChannelCommand.CommitmentSpliceResponse + _ finalResult: Lightning_kmpChannelFundingResponse ) -> some View { Section { - if let _ = finalResult.asCreated() { - section_result_created() + if let _ = finalResult.asSuccess() { + section_result_success() } else if let failure = finalResult.asFailure() { section_result_failure(failure) } @@ -487,7 +487,7 @@ struct LiquidityAdsView: View { } @ViewBuilder - func section_result_created() -> some View { + func section_result_success() -> some View { VStack(alignment: HorizontalAlignment.center, spacing: 0) { @@ -535,7 +535,7 @@ struct LiquidityAdsView: View { @ViewBuilder func section_result_failure( - _ failure: Lightning_kmpChannelCommand.CommitmentSpliceResponseFailure + _ failure: Lightning_kmpChannelFundingResponse.Failure ) -> some View { VStack(alignment: HorizontalAlignment.center, spacing: 0) { @@ -585,7 +585,7 @@ struct LiquidityAdsView: View { @ViewBuilder func section_result_failure_details( - _ failure: Lightning_kmpChannelCommand.CommitmentSpliceResponseFailure + _ failure: Lightning_kmpChannelFundingResponse.Failure ) -> some View { Group { @@ -595,10 +595,12 @@ struct LiquidityAdsView: View { Text("Invalid splice-out pubKeyScript") } else if let _ = failure.asSpliceAlreadyInProgress() { Text("Splice already in progress") - } else if let _ = failure.asChannelNotQuiescent() { - Text("Splice has been aborted") } else if let _ = failure.asConcurrentRemoteSplice() { Text("Concurrent splice in progress") + } else if let _ = failure.asChannelNotQuiescent() { + Text("Splice has been aborted") + } else if let _ = failure.asInvalidChannelParameters() { + Text("Invalid channel parameters") } else if let _ = failure.asInvalidLiquidityAds() { Text("Invalid liquidity ads") } else if let _ = failure.asFundingFailure() { @@ -611,6 +613,8 @@ struct LiquidityAdsView: View { Text("Cannot create commit tx") } else if let _ = failure.asAbortedByPeer() { Text("Aborted by peer") + } else if let _ = failure.asUnexpectedMessage() { + Text("Unexpected message") } else if let _ = failure.asDisconnected() { Text("Disconnected") } else { @@ -791,30 +795,38 @@ struct LiquidityAdsView: View { let feePerByte = Lightning_kmpFeeratePerByte(feerate: satsPerByte) let feePerKw = Lightning_kmpFeeratePerKw(feeratePerByte: feePerByte) - let leaseRate = NodeParamsManager.companion._liquidityLeaseRate(amount: amount) - isEstimating = true Task { @MainActor in + var fundingRate: Lightning_kmpLiquidityAdsFundingRate? = nil + do { + fundingRate = try await peer.fundingRate(amount: amount) + } catch { + log.error("peer.fundingRate(amount): error: \(error)") + } + var pair: KotlinPair< Lightning_kmpFeeratePerKw, Lightning_kmpChannelManagementFees>? = nil var _channelsNotAvailable = false - do { - pair = try await peer._estimateFeeForInboundLiquidity( - amount: amount, - targetFeerate: feePerKw, - leaseRate: leaseRate - ) - - if pair == nil { - log.error("peer.estimateFeeForInboundLiquidity() == nil") - _channelsNotAvailable = true + + if let fundingRate { + do { + pair = try await peer.estimateFeeForInboundLiquidity( + amount: amount, + targetFeerate: feePerKw, + fundingRate: fundingRate + ) + + if pair == nil { + log.error("peer.estimateFeeForInboundLiquidity() == nil") + _channelsNotAvailable = true + } + + } catch { + log.error("peer.estimateFeeForInboundLiquidity(): error: \(error)") } - - } catch { - log.error("peer.estimateFeeForInboundLiquidity(): error: \(error)") } let currentAmount = self.selectedLiquidityAmount() @@ -826,10 +838,11 @@ struct LiquidityAdsView: View { if let pair = pair, let feerate: Lightning_kmpFeeratePerKw = pair.first, - let fees: Lightning_kmpChannelManagementFees = pair.second + let fees: Lightning_kmpChannelManagementFees = pair.second, + let fundingRate = fundingRate { feeInfo = LiquidityFeeInfo( - params: LiquidityFeeParams(amount: amount, feerate: feerate, leaseRate: leaseRate), + params: LiquidityFeeParams(amount: amount, feerate: feerate, fundingRate: fundingRate), estimate: LiquidityFeeEstimate(minerFee: fees.miningFee, serviceFee: fees.serviceFee) ) } @@ -853,13 +866,13 @@ struct LiquidityAdsView: View { isPurchasing = true Task { @MainActor in - var result: Lightning_kmpChannelCommand.CommitmentSpliceResponse? = nil + var result: Lightning_kmpChannelFundingResponse? = nil var _channelsNotAvailable = false do { - result = try await peer._requestInboundLiquidity( + result = try await peer.requestInboundLiquidity( amount: feeInfo.params.amount, feerate: feeInfo.params.feerate, - leaseRate: feeInfo.params.leaseRate + fundingRate: feeInfo.params.fundingRate ) if result == nil { @@ -874,12 +887,12 @@ struct LiquidityAdsView: View { if let result { finalResult = result } else if !_channelsNotAvailable { - finalResult = Lightning_kmpChannelCommand.CommitmentSpliceResponseFailureDisconnected() + finalResult = Lightning_kmpChannelFundingResponse.FailureDisconnected() } channelsNotAvailable = _channelsNotAvailable isPurchasing = false - isPurchased = (result?.asCreated() != nil) + isPurchased = (result?.asSuccess() != nil) } // } diff --git a/phoenix-ios/phoenix-ios/views/configuration/fees/liquidity management/LiquidityFeeInfo.swift b/phoenix-ios/phoenix-ios/views/configuration/fees/liquidity management/LiquidityFeeInfo.swift index 741809b8a..9c5875d04 100644 --- a/phoenix-ios/phoenix-ios/views/configuration/fees/liquidity management/LiquidityFeeInfo.swift +++ b/phoenix-ios/phoenix-ios/views/configuration/fees/liquidity management/LiquidityFeeInfo.swift @@ -4,7 +4,7 @@ import PhoenixShared struct LiquidityFeeParams { let amount: Bitcoin_kmpSatoshi let feerate: Lightning_kmpFeeratePerKw - let leaseRate: LiquidityAds_LeaseRate + let fundingRate: Lightning_kmpLiquidityAdsFundingRate } struct LiquidityFeeEstimate { diff --git a/phoenix-ios/phoenix-ios/views/inspect/CpfpView.swift b/phoenix-ios/phoenix-ios/views/inspect/CpfpView.swift index d04883a2a..cc404ac4e 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/CpfpView.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/CpfpView.swift @@ -27,7 +27,7 @@ enum CpfpError: Error { case feeNotIncreased case noChannels case errorThrown(message: String) - case executeError(problem: SpliceOutProblem) + case executeError(problem: ChannelFundingProblem) } @@ -715,7 +715,7 @@ struct CpfpView: View { feerate: feeratePerKw ) - if let problem = SpliceOutProblem.fromResponse(response) { + if let problem = ChannelFundingProblem.fromResponse(response) { self.cpfpError = .executeError(problem: problem) } else { switch location { diff --git a/phoenix-ios/phoenix-ios/views/inspect/DetailsView.swift b/phoenix-ios/phoenix-ios/views/inspect/DetailsView.swift index fb7e9186f..ead1948b5 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/DetailsView.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/DetailsView.swift @@ -1090,7 +1090,7 @@ fileprivate struct DetailsInfoGrid: InfoGridView { commonValue_amounts( identifier: identifier, displayAmounts: displayAmounts( - sat: payment._lease.amount, + sat: payment.purchase.amount, originalFiat: paymentInfo.metadata.originalFiat ) ) diff --git a/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift b/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift index ed5837965..1974bc38f 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift @@ -228,7 +228,7 @@ extension Lightning_kmpWalletPayment { if let il = self as? Lightning_kmpInboundLiquidityOutgoingPayment { - let sat = il._lease.fees.serviceFee + let sat = il.purchase.fees.serviceFee let msat = Utils.toMsat(sat: sat) let title = NSLocalizedString("Service Fees", comment: "Label in SummaryInfoGrid") diff --git a/phoenix-ios/phoenix-ios/views/notifications/BizNotificationCell.swift b/phoenix-ios/phoenix-ios/views/notifications/BizNotificationCell.swift index 302cea37d..4dc3a04c6 100644 --- a/phoenix-ios/phoenix-ios/views/notifications/BizNotificationCell.swift +++ b/phoenix-ios/phoenix-ios/views/notifications/BizNotificationCell.swift @@ -140,13 +140,14 @@ struct BizNotificationCell: View { } // Group { - if reason is PhoenixShared.Notification.PaymentRejected.FeePolicyDisabled { + switch onEnum(of: reason) { + case .feePolicyDisabled(_): Text("Automated incoming liquidity is disabled in your incoming fee settings.") - - } else if reason is PhoenixShared.Notification.PaymentRejected.ChannelsInitializing { - Text("Channels initializing...") - - } else { + case .missingOffChainAmountTooLow(_): + Text("Missing off-chain amount too low.") + case .channelFundingInProgress(_): + Text("Channel funding in progress.") + default: Text("Unknown reason.") } } diff --git a/phoenix-ios/phoenix-ios/views/send/SpliceOutProblem.swift b/phoenix-ios/phoenix-ios/views/send/ChannelFundingProblem.swift similarity index 72% rename from phoenix-ios/phoenix-ios/views/send/SpliceOutProblem.swift rename to phoenix-ios/phoenix-ios/views/send/ChannelFundingProblem.swift index bf7a04c13..2e8112cb3 100644 --- a/phoenix-ios/phoenix-ios/views/send/SpliceOutProblem.swift +++ b/phoenix-ios/phoenix-ios/views/send/ChannelFundingProblem.swift @@ -1,10 +1,10 @@ import Foundation import PhoenixShared -enum SpliceOutProblem: Error { +enum ChannelFundingProblem: Error { case insufficientFunds case spliceAlreadyInProgress - case channelNotQuiescent + case spliceAborted case sessionError case disconnected case other @@ -16,7 +16,7 @@ enum SpliceOutProblem: Error { return String(localized: "Insufficient funds") case .spliceAlreadyInProgress: return String(localized: "Splice already in progress") - case .channelNotQuiescent: + case .spliceAborted: return String(localized: "Splice has been aborted") case .sessionError: return String(localized: "Splice-out session error") @@ -28,8 +28,8 @@ enum SpliceOutProblem: Error { } static func fromResponse( - _ response: Lightning_kmpChannelCommand.CommitmentSpliceResponse? - ) -> SpliceOutProblem? { + _ response: Lightning_kmpChannelFundingResponse? + ) -> ChannelFundingProblem? { guard let response else { return .other @@ -42,11 +42,23 @@ enum SpliceOutProblem: Error { if let _ = failure.asInsufficientFunds() { return .insufficientFunds } + if let _ = failure.asInvalidSpliceOutPubKeyScript() { + return .sessionError + } if let _ = failure.asSpliceAlreadyInProgress() { return .spliceAlreadyInProgress } + if let _ = failure.asConcurrentRemoteSplice() { + return .spliceAborted + } if let _ = failure.asChannelNotQuiescent() { - return .channelNotQuiescent + return .spliceAborted + } + if let _ = failure.asInvalidChannelParameters() { + return .sessionError + } + if let _ = failure.asInvalidLiquidityAds() { + return .sessionError } if let _ = failure.asFundingFailure() { return .sessionError @@ -63,6 +75,9 @@ enum SpliceOutProblem: Error { if let _ = failure.asAbortedByPeer() { return .sessionError } + if let _ = failure.asUnexpectedMessage() { + return .sessionError + } if let _ = failure.asDisconnected() { return .disconnected } diff --git a/phoenix-ios/phoenix-ios/views/send/ValidateView.swift b/phoenix-ios/phoenix-ios/views/send/ValidateView.swift index 11baded3d..a728e7ed4 100644 --- a/phoenix-ios/phoenix-ios/views/send/ValidateView.swift +++ b/phoenix-ios/phoenix-ios/views/send/ValidateView.swift @@ -37,7 +37,7 @@ struct ValidateView: View { @State var paymentInProgress: Bool = false @State var payOfferProblem: PayOfferProblem? = nil - @State var spliceOutProblem: SpliceOutProblem? = nil + @State var spliceOutProblem: ChannelFundingProblem? = nil @State var preTipAmountMsat: Int64? = nil @State var postTipAmountMsat: Int64? = nil @@ -1765,7 +1765,7 @@ struct ValidateView: View { self.paymentInProgress = false - if let problem = SpliceOutProblem.fromResponse(response) { + if let problem = ChannelFundingProblem.fromResponse(response) { self.spliceOutProblem = problem } else { diff --git a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/LightningExposure.kt b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/LightningExposure.kt index 25b3adac1..d7cc22cca 100644 --- a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/LightningExposure.kt +++ b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/LightningExposure.kt @@ -3,13 +3,10 @@ package fr.acinq.phoenix.utils import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.ByteVector64 -import fr.acinq.bitcoin.Crypto import fr.acinq.bitcoin.PrivateKey -import fr.acinq.bitcoin.PublicKey import fr.acinq.bitcoin.Satoshi import fr.acinq.bitcoin.Transaction import fr.acinq.bitcoin.TxId -import fr.acinq.bitcoin.byteVector import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.ChannelEvents import fr.acinq.lightning.DefaultSwapInParams @@ -23,16 +20,13 @@ import fr.acinq.lightning.blockchain.electrum.ElectrumClient import fr.acinq.lightning.blockchain.electrum.ElectrumMiniWallet import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.ChannelCommand -import fr.acinq.lightning.channel.ChannelManagementFees +import fr.acinq.lightning.channel.ChannelFundingResponse import fr.acinq.lightning.channel.states.Aborted import fr.acinq.lightning.channel.states.ChannelState import fr.acinq.lightning.channel.states.Closed import fr.acinq.lightning.channel.states.Closing import fr.acinq.lightning.channel.states.Offline import fr.acinq.lightning.crypto.KeyManager -import fr.acinq.lightning.crypto.LocalKeyManager -import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment import fr.acinq.lightning.db.IncomingPayment import fr.acinq.lightning.db.LightningOutgoingPayment import fr.acinq.lightning.io.NativeSocketException @@ -51,22 +45,19 @@ import fr.acinq.lightning.payment.LiquidityPolicy import fr.acinq.lightning.payment.OutgoingPaymentFailure import fr.acinq.lightning.utils.Connection import fr.acinq.lightning.utils.UUID -import fr.acinq.lightning.utils.concat import fr.acinq.lightning.utils.copyTo import fr.acinq.lightning.utils.toByteArray import fr.acinq.lightning.utils.toNSData import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.lightning.wire.OfferTypes -import fr.acinq.phoenix.managers.cloudKey -import io.ktor.utils.io.core.toByteArray import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import platform.Foundation.NSData -import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds /** @@ -76,35 +67,24 @@ import kotlin.time.Duration.Companion.seconds * This problem is restricted to iOS, and does not affect Android. */ -fun IncomingPayment.Origin.asInvoice(): IncomingPayment.Origin.Invoice? = when (this) { - is IncomingPayment.Origin.Invoice -> this - else -> null -} +fun IncomingPayment.Origin.asInvoice(): IncomingPayment.Origin.Invoice? = + (this as? IncomingPayment.Origin.Invoice) -fun IncomingPayment.Origin.asSwapIn(): IncomingPayment.Origin.SwapIn? = when (this) { - is IncomingPayment.Origin.SwapIn -> this - else -> null -} +fun IncomingPayment.Origin.asSwapIn(): IncomingPayment.Origin.SwapIn? = + (this as? IncomingPayment.Origin.SwapIn) -fun IncomingPayment.Origin.asOnChain(): IncomingPayment.Origin.OnChain? = when (this) { - is IncomingPayment.Origin.OnChain -> this - else -> null -} +fun IncomingPayment.Origin.asOnChain(): IncomingPayment.Origin.OnChain? = + (this as? IncomingPayment.Origin.OnChain) -fun IncomingPayment.ReceivedWith.asLightningPayment(): IncomingPayment.ReceivedWith.LightningPayment? = when (this) { - is IncomingPayment.ReceivedWith.LightningPayment -> this - else -> null -} +fun IncomingPayment.ReceivedWith.asLightningPayment(): + IncomingPayment.ReceivedWith.LightningPayment? = + (this as? IncomingPayment.ReceivedWith.LightningPayment) -fun IncomingPayment.ReceivedWith.asNewChannel(): IncomingPayment.ReceivedWith.NewChannel? = when (this) { - is IncomingPayment.ReceivedWith.NewChannel -> this - else -> null -} +fun IncomingPayment.ReceivedWith.asNewChannel(): IncomingPayment.ReceivedWith.NewChannel? = + (this as? IncomingPayment.ReceivedWith.NewChannel) -fun IncomingPayment.ReceivedWith.asSpliceIn(): IncomingPayment.ReceivedWith.SpliceIn? = when (this) { - is IncomingPayment.ReceivedWith.SpliceIn -> this - else -> null -} +fun IncomingPayment.ReceivedWith.asSpliceIn(): IncomingPayment.ReceivedWith.SpliceIn? = + (this as? IncomingPayment.ReceivedWith.SpliceIn) fun LightningOutgoingPayment.outgoingPaymentFailure(): OutgoingPaymentFailure? { return (status as? LightningOutgoingPayment.Status.Completed.Failed)?.let { status -> @@ -134,214 +114,122 @@ fun LightningOutgoingPayment.explainAsFinalFailure(): FinalFailure? { } } -fun LightningOutgoingPayment.Details.asNormal(): LightningOutgoingPayment.Details.Normal? = when (this) { - is LightningOutgoingPayment.Details.Normal -> this - else -> null -} +fun LightningOutgoingPayment.Details.asNormal(): LightningOutgoingPayment.Details.Normal? = + (this as? LightningOutgoingPayment.Details.Normal) -fun LightningOutgoingPayment.Details.asSwapOut(): LightningOutgoingPayment.Details.SwapOut? = when (this) { - is LightningOutgoingPayment.Details.SwapOut -> this - else -> null -} +fun LightningOutgoingPayment.Details.asSwapOut(): LightningOutgoingPayment.Details.SwapOut? = + (this as? LightningOutgoingPayment.Details.SwapOut) -fun LightningOutgoingPayment.Status.asPending(): LightningOutgoingPayment.Status.Pending? = when (this) { - is LightningOutgoingPayment.Status.Pending -> this - else -> null -} +fun LightningOutgoingPayment.Status.asPending(): LightningOutgoingPayment.Status.Pending? = + (this as? LightningOutgoingPayment.Status.Pending) -fun LightningOutgoingPayment.Status.asFailed(): LightningOutgoingPayment.Status.Completed.Failed? = when (this) { - is LightningOutgoingPayment.Status.Completed.Failed -> this - else -> null -} +fun LightningOutgoingPayment.Status.asFailed(): LightningOutgoingPayment.Status.Completed.Failed? = + (this as? LightningOutgoingPayment.Status.Completed.Failed) -fun LightningOutgoingPayment.Status.asSucceeded(): LightningOutgoingPayment.Status.Completed.Succeeded? = when (this) { - is LightningOutgoingPayment.Status.Completed.Succeeded -> this - else -> null -} +fun LightningOutgoingPayment.Status.asSucceeded(): + LightningOutgoingPayment.Status.Completed.Succeeded? = + (this as? LightningOutgoingPayment.Status.Completed.Succeeded) -fun LightningOutgoingPayment.Status.asOffChain(): LightningOutgoingPayment.Status.Completed.Succeeded.OffChain? = when (this) { - is LightningOutgoingPayment.Status.Completed.Succeeded.OffChain -> this - else -> null -} +fun LightningOutgoingPayment.Status.asOffChain(): + LightningOutgoingPayment.Status.Completed.Succeeded.OffChain? = + (this as? LightningOutgoingPayment.Status.Completed.Succeeded.OffChain) -fun ChannelState.asOffline(): Offline? = when (this) { - is Offline -> this - else -> null -} -fun ChannelState.asClosing(): Closing? = when (this) { - is Closing -> this - else -> null -} -fun ChannelState.asClosed(): Closed? = when (this) { - is Closed -> this - else -> null -} -fun ChannelState.asAborted(): Aborted? = when (this) { - is Aborted -> this - else -> null -} +fun ChannelState.asOffline(): Offline? = (this as? Offline) +fun ChannelState.asClosing(): Closing? = (this as? Closing) +fun ChannelState.asClosed(): Closed? = (this as? Closed) +fun ChannelState.asAborted(): Aborted? = (this as? Aborted) -fun TcpSocket.TLS.asDisabled(): TcpSocket.TLS.DISABLED? = when (this) { - is TcpSocket.TLS.DISABLED -> this - else -> null -} +fun TcpSocket.TLS.asDisabled(): TcpSocket.TLS.DISABLED? = + (this as? TcpSocket.TLS.DISABLED) -fun TcpSocket.TLS.asTrustedCertificates(): TcpSocket.TLS.TRUSTED_CERTIFICATES? = when (this) { - is TcpSocket.TLS.TRUSTED_CERTIFICATES -> this - else -> null -} +fun TcpSocket.TLS.asTrustedCertificates(): TcpSocket.TLS.TRUSTED_CERTIFICATES? = + (this as? TcpSocket.TLS.TRUSTED_CERTIFICATES) -fun TcpSocket.TLS.asPinnedPublicKey(): TcpSocket.TLS.PINNED_PUBLIC_KEY? = when (this) { - is TcpSocket.TLS.PINNED_PUBLIC_KEY -> this - else -> null -} +fun TcpSocket.TLS.asPinnedPublicKey(): TcpSocket.TLS.PINNED_PUBLIC_KEY? = + (this as? TcpSocket.TLS.PINNED_PUBLIC_KEY) -fun TcpSocket.TLS.asUnsafeCertificates(): TcpSocket.TLS.UNSAFE_CERTIFICATES? = when (this) { - is TcpSocket.TLS.UNSAFE_CERTIFICATES -> this - else -> null -} +fun TcpSocket.TLS.asUnsafeCertificates(): TcpSocket.TLS.UNSAFE_CERTIFICATES? = + (this as? TcpSocket.TLS.UNSAFE_CERTIFICATES) -fun Connection.asClosed(): Connection.CLOSED? = when (this) { - is Connection.CLOSED -> this - else -> null -} +fun Connection.asClosed(): Connection.CLOSED? = (this as? Connection.CLOSED) +fun Connection.asEstablishing(): Connection.ESTABLISHING? = (this as? Connection.ESTABLISHING) +fun Connection.asEstablished(): Connection.ESTABLISHED? = (this as? Connection.ESTABLISHED) -fun Connection.asEstablishing(): Connection.ESTABLISHING? = when (this) { - is Connection.ESTABLISHING -> this - else -> null -} +fun NativeSocketException.asPOSIX(): NativeSocketException.POSIX? = + (this as? NativeSocketException.POSIX) -fun Connection.asEstablished(): Connection.ESTABLISHED? = when (this) { - is Connection.ESTABLISHED -> this - else -> null -} +fun NativeSocketException.asDNS(): NativeSocketException.DNS? = + (this as? NativeSocketException.DNS) -fun NativeSocketException.asPOSIX(): NativeSocketException.POSIX? = when (this) { - is NativeSocketException.POSIX -> this - else -> null -} +fun NativeSocketException.asTLS(): NativeSocketException.TLS? = + (this as? NativeSocketException.TLS) -fun NativeSocketException.asDNS(): NativeSocketException.DNS? = when (this) { - is NativeSocketException.DNS -> this - else -> null -} +fun ElectrumMiniWallet.currentWalletState(): WalletState = this.walletStateFlow.value -fun NativeSocketException.asTLS(): NativeSocketException.TLS? = when (this) { - is NativeSocketException.TLS -> this - else -> null -} +fun NodeEvents.asChannelEvents(): ChannelEvents? = (this as? ChannelEvents) -fun ElectrumMiniWallet.currentWalletState(): WalletState = this.walletStateFlow.value +fun ChannelEvents.asCreating(): ChannelEvents.Creating? = (this as? ChannelEvents.Creating) +fun ChannelEvents.asCreated(): ChannelEvents.Created? = (this as? ChannelEvents.Created) +fun ChannelEvents.asConfirmed(): ChannelEvents.Confirmed? = (this as? ChannelEvents.Confirmed) -fun NodeEvents.asChannelEvents(): ChannelEvents? = when (this) { - is ChannelEvents -> this - else -> null -} +fun LiquidityEvents.Rejected.Reason.asOverAbsoluteFee(): + LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee? = + (this as? LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee) -fun ChannelEvents.asCreating(): ChannelEvents.Creating? = when (this) { - is ChannelEvents.Creating -> this - else -> null -} +fun LiquidityEvents.Rejected.Reason.asOverRelativeFee(): + LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee? = + (this as? LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee) -fun ChannelEvents.asCreated(): ChannelEvents.Created? = when (this) { - is ChannelEvents.Created -> this - else -> null -} +fun LiquidityPolicy.asDisable(): LiquidityPolicy.Disable? = (this as? LiquidityPolicy.Disable) +fun LiquidityPolicy.asAuto(): LiquidityPolicy.Auto? = (this as? LiquidityPolicy.Auto) -fun ChannelEvents.asConfirmed(): ChannelEvents.Confirmed? = when (this) { - is ChannelEvents.Confirmed -> this - else -> null -} +fun ChannelFundingResponse.asSuccess(): ChannelFundingResponse.Success? = + (this as? ChannelFundingResponse.Success) -fun LiquidityEvents.Rejected.Reason.asOverAbsoluteFee(): LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee? = when (this) { - is LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee -> this - else -> null -} +fun ChannelFundingResponse.asFailure(): ChannelFundingResponse.Failure? = + (this as? ChannelFundingResponse.Failure) -fun LiquidityEvents.Rejected.Reason.asOverRelativeFee(): LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee? = when (this) { - is LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee -> this - else -> null -} +fun ChannelFundingResponse.Failure.asInsufficientFunds(): ChannelFundingResponse.Failure.InsufficientFunds? = + (this as? ChannelFundingResponse.Failure.InsufficientFunds) -fun LiquidityPolicy.asDisable(): LiquidityPolicy.Disable? = when (this) { - is LiquidityPolicy.Disable -> this - else -> null -} +fun ChannelFundingResponse.Failure.asInvalidSpliceOutPubKeyScript(): ChannelFundingResponse.Failure.InvalidSpliceOutPubKeyScript? = + (this as? ChannelFundingResponse.Failure.InvalidSpliceOutPubKeyScript) -fun LiquidityPolicy.asAuto(): LiquidityPolicy.Auto? = when (this) { - is LiquidityPolicy.Auto -> this - else -> null -} +fun ChannelFundingResponse.Failure.asSpliceAlreadyInProgress(): ChannelFundingResponse.Failure.SpliceAlreadyInProgress? = + (this as? ChannelFundingResponse.Failure.SpliceAlreadyInProgress) + +fun ChannelFundingResponse.Failure.asConcurrentRemoteSplice(): ChannelFundingResponse.Failure.ConcurrentRemoteSplice? = + (this as? ChannelFundingResponse.Failure.ConcurrentRemoteSplice) + +fun ChannelFundingResponse.Failure.asChannelNotQuiescent(): ChannelFundingResponse.Failure.ChannelNotQuiescent? = + (this as? ChannelFundingResponse.Failure.ChannelNotQuiescent) + +fun ChannelFundingResponse.Failure.asInvalidChannelParameters(): ChannelFundingResponse.Failure.InvalidChannelParameters? = + (this as? ChannelFundingResponse.Failure.InvalidChannelParameters) + +fun ChannelFundingResponse.Failure.asInvalidLiquidityAds(): ChannelFundingResponse.Failure.InvalidLiquidityAds? = + (this as? ChannelFundingResponse.Failure.InvalidLiquidityAds) -//fun ChannelCommand.Commitment.Splice.Response.asCreated(): ChannelCommand.Commitment.Splice.Response.Created? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Created -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.asFailure(): ChannelCommand.Commitment.Splice.Response.Failure? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asInsufficientFunds(): ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asInvalidSpliceOutPubKeyScript(): ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asSpliceAlreadyInProgress(): ChannelCommand.Commitment.Splice.Response.Failure.SpliceAlreadyInProgress? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.SpliceAlreadyInProgress -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asChannelNotQuiescent(): ChannelCommand.Commitment.Splice.Response.Failure.ChannelNotQuiescent? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.ChannelNotQuiescent -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asConcurrentRemoteSplice(): ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asInvalidLiquidityAds(): ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asFundingFailure(): ChannelCommand.Commitment.Splice.Response.Failure.FundingFailure? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.FundingFailure -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asCannotStartSession(): ChannelCommand.Commitment.Splice.Response.Failure.CannotStartSession? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.CannotStartSession -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asInteractiveTxSessionFailed(): ChannelCommand.Commitment.Splice.Response.Failure.InteractiveTxSessionFailed? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.InteractiveTxSessionFailed -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asCannotCreateCommitTx(): ChannelCommand.Commitment.Splice.Response.Failure.CannotCreateCommitTx? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.CannotCreateCommitTx -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asAbortedByPeer(): ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer -> this -// else -> null -//} -// -//fun ChannelCommand.Commitment.Splice.Response.Failure.asDisconnected(): ChannelCommand.Commitment.Splice.Response.Failure.Disconnected? = when (this) { -// is ChannelCommand.Commitment.Splice.Response.Failure.Disconnected -> this -// else -> null -//} +fun ChannelFundingResponse.Failure.asFundingFailure(): ChannelFundingResponse.Failure.FundingFailure? = + (this as? ChannelFundingResponse.Failure.FundingFailure) + +fun ChannelFundingResponse.Failure.asCannotStartSession(): ChannelFundingResponse.Failure.CannotStartSession? = + (this as? ChannelFundingResponse.Failure.CannotStartSession) + +fun ChannelFundingResponse.Failure.asInteractiveTxSessionFailed(): ChannelFundingResponse.Failure.InteractiveTxSessionFailed? = + (this as? ChannelFundingResponse.Failure.InteractiveTxSessionFailed) + +fun ChannelFundingResponse.Failure.asCannotCreateCommitTx(): ChannelFundingResponse.Failure.CannotCreateCommitTx? = + (this as? ChannelFundingResponse.Failure.CannotCreateCommitTx) + +fun ChannelFundingResponse.Failure.asAbortedByPeer(): ChannelFundingResponse.Failure.AbortedByPeer? = + (this as? ChannelFundingResponse.Failure.AbortedByPeer) + +fun ChannelFundingResponse.Failure.asUnexpectedMessage(): ChannelFundingResponse.Failure.UnexpectedMessage? = + (this as? ChannelFundingResponse.Failure.UnexpectedMessage) + +fun ChannelFundingResponse.Failure.asDisconnected(): ChannelFundingResponse.Failure.Disconnected? = + (this as? ChannelFundingResponse.Failure.Disconnected) suspend fun ElectrumClient.kotlin_getConfirmations(txid: TxId): Int? { return this.getConfirmations(txid) @@ -355,35 +243,24 @@ fun defaultSwapInParams(): SwapInParams { ) } -fun SensitiveTaskEvents.asTaskStarted(): SensitiveTaskEvents.TaskStarted? = when (this) { - is SensitiveTaskEvents.TaskStarted -> this - else -> null -} +fun SensitiveTaskEvents.asTaskStarted(): SensitiveTaskEvents.TaskStarted? = + (this as? SensitiveTaskEvents.TaskStarted) -fun SensitiveTaskEvents.asTaskEnded(): SensitiveTaskEvents.TaskEnded? = when (this) { - is SensitiveTaskEvents.TaskEnded -> this - else -> null -} +fun SensitiveTaskEvents.asTaskEnded(): SensitiveTaskEvents.TaskEnded? = + (this as? SensitiveTaskEvents.TaskEnded) -fun SensitiveTaskEvents.TaskIdentifier.asInteractiveTx(): SensitiveTaskEvents.TaskIdentifier.InteractiveTx? = when (this) { - is SensitiveTaskEvents.TaskIdentifier.InteractiveTx -> this - else -> null -} +fun SensitiveTaskEvents.TaskIdentifier.asInteractiveTx(): + SensitiveTaskEvents.TaskIdentifier.InteractiveTx? = + (this as? SensitiveTaskEvents.TaskIdentifier.InteractiveTx) -fun PeerEvent.asPaymentProgress(): PaymentProgress? = when (this) { - is PaymentProgress -> this - else -> null -} +fun PeerEvent.asPaymentProgress(): PaymentProgress? = + (this as? PaymentProgress) -fun PeerEvent.asPaymentSent(): PaymentSent? = when (this) { - is PaymentSent -> this - else -> null -} +fun PeerEvent.asPaymentSent(): PaymentSent? = + (this as? PaymentSent) -fun PeerEvent.asPaymentNotSent(): PaymentNotSent? = when (this) { - is PaymentNotSent -> this - else -> null -} +fun PeerEvent.asPaymentNotSent(): PaymentNotSent? = + (this as? PaymentNotSent) fun FinalFailure.asAlreadyPaid(): FinalFailure.AlreadyPaid? = (this as? FinalFailure.AlreadyPaid) @@ -485,132 +362,6 @@ fun NSData_copyTo(data: NSData, buffer: ByteArray, offset: Int = 0) = data.copyT fun ByteArray_toNSDataSlice(buffer: ByteArray, offset: Int, length: Int): NSData = buffer.toNSData(offset = offset, length = length) fun ByteArray_toNSData(buffer: ByteArray): NSData = buffer.toNSData() -/** - * The class LiquidityAds.LeaseRate is NOT exposed to iOS. - * That is, it's exposed via Objective-C, but cannot be mapped to Swift. - * The following error message is displayed in Xcode: - * - * > Imported declaration 'PhoenixSharedLightning_kmpLiquidityAdsLeaseRate' could - * > not be mapped to 'Lightning_kmpLiquidityAds.LeaseRate' - * - * The end result is that any function that uses this class as a parameter, - * or as a return value, is NOT available to Swift. - * - * The fix is to start using TouchLab's SKIE library, - * which (in my experience) fixes all these problems. - * But in the meantime, we're working around it by exposing our own class & wrapper functions. - */ -//data class LiquidityAds_LeaseRate( -// val leaseDuration: Int, -// val fundingWeight: Int, -// val leaseFeeProportional: Int, -// val leaseFeeBase: Satoshi, -// val maxRelayFeeProportional: Int, -// val maxRelayFeeBase: MilliSatoshi -//) { -// constructor(src: LiquidityAds.LeaseRate) : this( -// leaseDuration = src.leaseDuration, -// fundingWeight = src.fundingWeight, -// leaseFeeProportional = src.leaseFeeProportional, -// leaseFeeBase = src.leaseFeeBase, -// maxRelayFeeProportional = src.maxRelayFeeProportional, -// maxRelayFeeBase = src.maxRelayFeeBase -// ) -// fun unwrap() = LiquidityAds.LeaseRate( -// leaseDuration = this.leaseDuration, -// fundingWeight = this.fundingWeight, -// leaseFeeProportional = this.leaseFeeProportional, -// leaseFeeBase = this.leaseFeeBase, -// maxRelayFeeProportional = this.maxRelayFeeProportional, -// maxRelayFeeBase = this.maxRelayFeeBase -// ) -//} - -//data class LiquidityAds_LeaseFees( -// val miningFee: Satoshi, -// val serviceFee: Satoshi -//) { -// constructor(src: LiquidityAds.LeaseFees) : this( -// miningFee = src.miningFee, -// serviceFee = src.serviceFee -// ) -// fun unwrap() = LiquidityAds.LeaseFees( -// miningFee = this.miningFee, -// serviceFee = this.serviceFee -// ) -// -// val total: Satoshi = unwrap().total -//} - -//data class LiquidityAds_LeaseWitness( -// val fundingScript: ByteVector, -// val leaseDuration: Int, -// val leaseEnd: Int, -// val maxRelayFeeProportional: Int, -// val maxRelayFeeBase: MilliSatoshi -//) { -// constructor(src: LiquidityAds.LeaseWitness) : this( -// fundingScript = src.fundingScript, -// leaseDuration = src.leaseDuration, -// leaseEnd = src.leaseEnd, -// maxRelayFeeProportional = src.maxRelayFeeProportional, -// maxRelayFeeBase = src.maxRelayFeeBase -// ) -// fun unwrap() = LiquidityAds.LeaseWitness( -// fundingScript = this.fundingScript, -// leaseDuration = this.leaseDuration, -// leaseEnd = this.leaseEnd, -// maxRelayFeeProportional = this.maxRelayFeeProportional, -// maxRelayFeeBase = this.maxRelayFeeBase -// ) -// -// fun sign(nodeKey: PrivateKey): ByteVector64 = unwrap().sign(nodeKey) -// fun verify(nodeId: PublicKey, sig: ByteVector64): Boolean = unwrap().verify(nodeId, sig) -// fun encode(): ByteArray = unwrap().encode() -//} - -//data class LiquidityAds_Lease( -// val amount: Satoshi, -// val fees: LiquidityAds_LeaseFees, -// val sellerSig: ByteVector64, -// val witness: LiquidityAds_LeaseWitness -//) { -// constructor(src: LiquidityAds.Lease) : this( -// amount = src.amount, -// fees = LiquidityAds_LeaseFees(src.fees), -// sellerSig = src.sellerSig, -// witness = LiquidityAds_LeaseWitness(src.witness) -// ) -// fun unwrap() = LiquidityAds.Lease( -// amount = this.amount, -// fees = this.fees.unwrap(), -// sellerSig = this.sellerSig, -// witness = this.witness.unwrap() -// ) -// -// val start: Int = unwrap().start -// val expiry: Int = unwrap().expiry -//} - -//suspend fun Peer._estimateFeeForInboundLiquidity( -// amount: Satoshi, -// targetFeerate: FeeratePerKw, -// leaseRate: LiquidityAds_LeaseRate -//): Pair? { -// return this.estimateFeeForInboundLiquidity(amount, targetFeerate, leaseRate.unwrap()) -//} -// -//suspend fun Peer._requestInboundLiquidity( -// amount: Satoshi, -// feerate: FeeratePerKw, -// leaseRate: LiquidityAds_LeaseRate -//): ChannelCommand.Commitment.Splice.Response? { -// return this.requestInboundLiquidity(amount, feerate, leaseRate.unwrap()) -//} - -//val InboundLiquidityOutgoingPayment._lease: LiquidityAds_Lease -// get() = LiquidityAds_Lease(this.lease) - fun WalletState.WalletWithConfirmations._spendExpiredSwapIn( swapInKeys: KeyManager.SwapInOnChainKeys, scriptPubKey: ByteVector, @@ -619,6 +370,10 @@ fun WalletState.WalletWithConfirmations._spendExpiredSwapIn( return this.spendExpiredSwapIn(swapInKeys, scriptPubKey, feerate) } +suspend fun Peer.fundingRate(amount: Satoshi): LiquidityAds.FundingRate? { + return this.remoteFundingRates.filterNotNull().first().findRate(amount) +} + suspend fun Peer.altPayOffer( paymentId: UUID, amount: MilliSatoshi, @@ -662,12 +417,3 @@ suspend fun Peer.betterPayOffer( send(PayOffer(paymentId, payerKey, payerNote, amount, offer, fetchInvoiceTimeoutInSeconds.seconds)) return res.await() } - -fun LocalKeyManager.cloudHash(name: String): String { - val nid: ByteArray = this.nodeKeys.nodeKey.publicKey.value.toByteArray() - val ck: ByteArray = this.cloudKey().toByteArray() - val nm: ByteArray = name.toByteArray() - - val input = nid.concat(ck).concat(nm) - return Crypto.hash160(input).byteVector().toHex() -} From d7e7832402ce75d787550121d9e79c71fa2ecbec Mon Sep 17 00:00:00 2001 From: Robbie Hanson <304604+robbiehanson@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:27:51 -0500 Subject: [PATCH 15/25] Standardizing chain name across codebase via `chain.phoenixName`. --- .../acinq/phoenix/android/settings/ResetWallet.kt | 5 +++-- .../phoenix/android/startup/StartupViewModel.kt | 3 ++- phoenix-ios/phoenix-ios/officers/WalletReset.swift | 4 ++-- phoenix-ios/phoenix-ios/sync/SyncSeedManager.swift | 12 +----------- .../kotlin/fr/acinq/phoenix/db/androidDbFactory.kt | 13 +++---------- .../managers/AppConfigurationManager.kt | 3 ++- .../utils/extensions/ChainExtensions.kt | 13 +++++++++++++ .../kotlin/fr/acinq/phoenix/db/iosDbFactory.kt | 13 +++---------- 8 files changed, 29 insertions(+), 37 deletions(-) create mode 100644 phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ChainExtensions.kt diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ResetWallet.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ResetWallet.kt index 19574ecd6..26bfea7b1 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ResetWallet.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/ResetWallet.kt @@ -64,6 +64,7 @@ import fr.acinq.phoenix.android.fiatRate import fr.acinq.phoenix.android.security.SeedManager import fr.acinq.phoenix.android.utils.Converter.toPrettyString import fr.acinq.phoenix.android.utils.negativeColor +import fr.acinq.phoenix.utils.extensions.phoenixName import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -115,8 +116,8 @@ class ResetWalletViewModel : ViewModel() { state.value = ResetWalletStep.Deleting.Databases context.deleteDatabase("appdb.sqlite") - context.deleteDatabase("payments-${chain.name.lowercase()}-$nodeIdHash.sqlite") - context.deleteDatabase("channels-${chain.name.lowercase()}-$nodeIdHash.sqlite") + context.deleteDatabase("payments-${chain.phoenixName}-$nodeIdHash.sqlite") + context.deleteDatabase("channels-${chain.phoenixName}-$nodeIdHash.sqlite") delay(500) state.value = ResetWalletStep.Deleting.Prefs diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupViewModel.kt index 02cc30771..58d7b499f 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupViewModel.kt @@ -28,6 +28,7 @@ import fr.acinq.phoenix.android.security.SeedManager import fr.acinq.phoenix.android.services.NodeService import fr.acinq.phoenix.managers.NodeParamsManager import fr.acinq.phoenix.managers.nodeIdHash +import fr.acinq.phoenix.utils.extensions.phoenixName import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -98,7 +99,7 @@ class StartupViewModel : ViewModel() { val seed = MnemonicCode.toSeed(mnemonics = words.joinToString(" "), passphrase = "").byteVector() val localKeyManager = LocalKeyManager(seed = seed, chain = NodeParamsManager.chain, remoteSwapInExtendedPublicKey = NodeParamsManager.remoteSwapInXpub) val nodeIdHash = localKeyManager.nodeIdHash() - val channelsDbFile = context.getDatabasePath("channels-${NodeParamsManager.chain.name.lowercase()}-$nodeIdHash.sqlite") + val channelsDbFile = context.getDatabasePath("channels-${NodeParamsManager.chain.phoenixName}-$nodeIdHash.sqlite") if (channelsDbFile.exists()) { decryptionState.value = StartupDecryptionState.SeedInputFallback.Success.MatchingData val encodedSeed = EncryptedSeed.fromMnemonics(words) diff --git a/phoenix-ios/phoenix-ios/officers/WalletReset.swift b/phoenix-ios/phoenix-ios/officers/WalletReset.swift index d649f08f2..8e870a540 100644 --- a/phoenix-ios/phoenix-ios/officers/WalletReset.swift +++ b/phoenix-ios/phoenix-ios/officers/WalletReset.swift @@ -170,8 +170,8 @@ class WalletReset { let dbDir = groupDir.appendingPathComponent("databases", isDirectory: true) - let chainName = Biz.business.chain.name.lowercased() - let nodeIdHash = Biz.nodeIdHash ?? "nil" + let chainName: String = Biz.business.chain.phoenixName + let nodeIdHash: String = Biz.nodeIdHash ?? "nil" log.debug("dbDir: \(dbDir.path)") log.debug("chainName: \(chainName)") diff --git a/phoenix-ios/phoenix-ios/sync/SyncSeedManager.swift b/phoenix-ios/phoenix-ios/sync/SyncSeedManager.swift index 911449e3d..2552cc776 100644 --- a/phoenix-ios/phoenix-ios/sync/SyncSeedManager.swift +++ b/phoenix-ios/phoenix-ios/sync/SyncSeedManager.swift @@ -756,20 +756,10 @@ class SyncSeedManager: SyncManagerProtcol { private class func record_table_name(chain: Bitcoin_kmpChain) -> String { - // From Apple's docs: - // > A record type must consist of one or more alphanumeric characters - // > and must start with a letter. CloudKit permits the use of underscores, - // > but not spaces. - // - var allowed = CharacterSet.alphanumerics - allowed.insert("_") - - let suffix = chain.name.lowercased().components(separatedBy: allowed.inverted).joined(separator: "") - // E.g.: // - seeds_bitcoin_testnet // - seeds_bitcoin_mainnet - return "seeds_bitcoin_\(suffix)" + return "seeds_bitcoin_\(chain.phoenixName)" } private func recordID() -> CKRecord.ID { diff --git a/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/db/androidDbFactory.kt b/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/db/androidDbFactory.kt index 0167018d7..11f862599 100644 --- a/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/db/androidDbFactory.kt +++ b/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/db/androidDbFactory.kt @@ -20,21 +20,14 @@ import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver import fr.acinq.bitcoin.Chain import fr.acinq.phoenix.utils.PlatformContext +import fr.acinq.phoenix.utils.extensions.phoenixName actual fun createChannelsDbDriver(ctx: PlatformContext, chain: Chain, nodeIdHash: String): SqlDriver { - val chainName = when (chain) { - is Chain.Testnet3 -> "testnet" - else -> chain.name.lowercase() - } - return AndroidSqliteDriver(ChannelsDatabase.Schema, ctx.applicationContext, "channels-$chainName-$nodeIdHash.sqlite") + return AndroidSqliteDriver(ChannelsDatabase.Schema, ctx.applicationContext, "channels-${chain.phoenixName}-$nodeIdHash.sqlite") } actual fun createPaymentsDbDriver(ctx: PlatformContext, chain: Chain, nodeIdHash: String): SqlDriver { - val chainName = when (chain) { - is Chain.Testnet3 -> "testnet" - else -> chain.name.lowercase() - } - return AndroidSqliteDriver(PaymentsDatabase.Schema, ctx.applicationContext, "payments-$chainName-$nodeIdHash.sqlite") + return AndroidSqliteDriver(PaymentsDatabase.Schema, ctx.applicationContext, "payments-${chain.phoenixName}-$nodeIdHash.sqlite") } actual fun createAppDbDriver(ctx: PlatformContext): SqlDriver { diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt index 9db9ad87c..b007f612a 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/AppConfigurationManager.kt @@ -12,6 +12,7 @@ import fr.acinq.lightning.utils.sat import fr.acinq.phoenix.PhoenixBusiness import fr.acinq.phoenix.data.* import fr.acinq.phoenix.db.SqliteAppDb +import fr.acinq.phoenix.utils.extensions.phoenixName import fr.acinq.lightning.logging.debug import fr.acinq.lightning.logging.error import io.ktor.client.* @@ -115,7 +116,7 @@ class AppConfigurationManager( }?.let { json -> logger.debug { "fetched wallet-context=$json" } try { - val base = json[chain.name.lowercase()]!! + val base = json[chain.phoenixName]!! val isMempoolFull = base.jsonObject["mempool"]?.jsonObject?.get("v1")?.jsonObject?.get("high_usage")?.jsonPrimitive?.booleanOrNull val androidLatestVersion = base.jsonObject["version"]?.jsonPrimitive?.intOrNull val androidLatestCriticalVersion = base.jsonObject["latest_critical_version"]?.jsonPrimitive?.intOrNull diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ChainExtensions.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ChainExtensions.kt new file mode 100644 index 000000000..1b9313416 --- /dev/null +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ChainExtensions.kt @@ -0,0 +1,13 @@ +package fr.acinq.phoenix.utils.extensions + +import fr.acinq.bitcoin.Chain + +val Chain.phoenixName: String + get() = when (this) { + Chain.Regtest -> "regtest" + Chain.Signet -> "signet" + // Chain.Testnet -> "testnet" + Chain.Testnet3 -> "testnet" + Chain.Testnet4 -> "testnet4" + Chain.Mainnet -> "mainnet" + } \ No newline at end of file diff --git a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/db/iosDbFactory.kt b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/db/iosDbFactory.kt index 336fae9fd..c7dc1689c 100644 --- a/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/db/iosDbFactory.kt +++ b/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/db/iosDbFactory.kt @@ -23,6 +23,7 @@ import app.cash.sqldelight.driver.native.wrapConnection import fr.acinq.bitcoin.Chain import fr.acinq.phoenix.utils.PlatformContext import fr.acinq.phoenix.utils.getDatabaseFilesDirectoryPath +import fr.acinq.phoenix.utils.extensions.phoenixName actual fun createChannelsDbDriver( ctx: PlatformContext, @@ -30,11 +31,7 @@ actual fun createChannelsDbDriver( nodeIdHash: String ): SqlDriver { val schema = ChannelsDatabase.Schema - val chainName = when (chain) { - is Chain.Testnet3 -> "testnet" - else -> chain.name.lowercase() - } - val name = "channels-$chainName-$nodeIdHash.sqlite" + val name = "channels-${chain.phoenixName}-$nodeIdHash.sqlite" // The foreign_keys constraint needs to be set via the DatabaseConfiguration: // https://github.com/cashapp/sqldelight/issues/1356 @@ -63,11 +60,7 @@ actual fun createPaymentsDbDriver( nodeIdHash: String ): SqlDriver { val schema = PaymentsDatabase.Schema - val chainName = when (chain) { - is Chain.Testnet3 -> "testnet" - else -> chain.name.lowercase() - } - val name = "payments-$chainName-$nodeIdHash.sqlite" + val name = "payments-${chain.phoenixName}-$nodeIdHash.sqlite" val dbDir = getDatabaseFilesDirectoryPath(ctx) val configuration = DatabaseConfiguration( From f9c15c1f8e70cdfe50abea4e8bdec12300016170 Mon Sep 17 00:00:00 2001 From: Robbie Hanson <304604+robbiehanson@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:29:47 -0500 Subject: [PATCH 16/25] (ios) WIP: Updating UI --- phoenix-ios/phoenix-ios/Localizable.xcstrings | 62 +++++ .../extensions/String+Substring.swift | 4 + .../kotlin/KotlinExtensions+Payments.swift | 31 ++- .../phoenix-ios/kotlin/KotlinTypes.swift | 10 + .../phoenix-ios/prefs/Prefs+BackupSeed.swift | 32 ++- .../prefs/Prefs+BackupTransactions.swift | 19 +- .../views/inspect/SummaryInfoGrid.swift | 73 +++++- .../views/inspect/SummaryView.swift | 247 +++++++++++++----- .../inspect/WalletPaymentExtensions.swift | 96 ++++--- .../views/receive/ReceiveView.swift | 1 - .../views/transactions/PaymentCell.swift | 46 +++- 11 files changed, 500 insertions(+), 121 deletions(-) diff --git a/phoenix-ios/phoenix-ios/Localizable.xcstrings b/phoenix-ios/phoenix-ios/Localizable.xcstrings index ecad7b971..ebe4a3c7a 100644 --- a/phoenix-ios/phoenix-ios/Localizable.xcstrings +++ b/phoenix-ios/phoenix-ios/Localizable.xcstrings @@ -1955,6 +1955,7 @@ } }, "%@ - from %@" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -2001,6 +2002,7 @@ } }, "%@ - to %@" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -2132,6 +2134,26 @@ } } }, + "%@ ∙ from %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ ∙ from %2$@" + } + } + } + }, + "%@ ∙ to %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ ∙ to %2$@" + } + } + } + }, "%@ days" : { "extractionState" : "manual", "localizations" : { @@ -3111,6 +3133,7 @@ }, "+%@ inbound liquidity" : { "comment" : "Payment description for inbound liquidity", + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -7626,6 +7649,9 @@ } } }, + "Automated liquidity" : { + "comment" : "Payment description for inbound liquidity" + }, "Automatic Channel Creation" : { "extractionState" : "manual", "localizations" : { @@ -9722,6 +9748,9 @@ } } } + }, + "Caused by" : { + }, "Chain mismatch" : { "comment" : "Error title", @@ -10011,6 +10040,9 @@ } } } + }, + "Channel Resized" : { + }, "Channel size impacted" : { "localizations" : { @@ -24558,6 +24590,7 @@ }, "Lightning fees for routing the payment. Payment required %d hops." : { "comment" : "Fees explanation", + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -24597,6 +24630,9 @@ } } }, + "Lightning fees for routing the payment. Payment required %lld hops." : { + "comment" : "Fees explanation" + }, "Lightning fees for routing the payment. Payment required 1 hop." : { "comment" : "Fees explanation", "localizations" : { @@ -24640,6 +24676,7 @@ }, "Lightning fees for routing the payment. Payment was divided into %d parts, using %d hops." : { "comment" : "Fees explanation", + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -24679,6 +24716,17 @@ } } }, + "Lightning fees for routing the payment. Payment was divided into %lld parts, using %lld hops." : { + "comment" : "Fees explanation", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Lightning fees for routing the payment. Payment was divided into %1$lld parts, using %2$lld hops." + } + } + } + }, "Lightning invoice" : { "comment" : "Type of text being copied", "localizations" : { @@ -25086,6 +25134,7 @@ } }, "Liquidity Added" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -25891,6 +25940,9 @@ } } }, + "Manual liquidity" : { + "comment" : "Payment description for inbound liquidity" + }, "Manual restore" : { "comment" : "Navigation bar title", "localizations" : { @@ -35883,6 +35935,7 @@ }, "Service Fees" : { "comment" : "Label in SummaryInfoGrid", + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -35922,6 +35975,12 @@ } } }, + "Service Fees (1)" : { + "comment" : "Label in SummaryInfoGrid" + }, + "Service Fees (2)" : { + "comment" : "Label in SummaryInfoGrid" + }, "Service returned unreadable response" : { "comment" : "Error message - scanning lightning invoice", "extractionState" : "manual", @@ -41790,6 +41849,9 @@ } } } + }, + "This liquidity was required to receive a payment" : { + }, "This means you will not be able to receive payments when Phoenix is in the background. To receive payments, Phoenix must be open and in the foreground." : { "extractionState" : "manual", diff --git a/phoenix-ios/phoenix-ios/extensions/String+Substring.swift b/phoenix-ios/phoenix-ios/extensions/String+Substring.swift index 08ba0e566..e8865fd91 100644 --- a/phoenix-ios/phoenix-ios/extensions/String+Substring.swift +++ b/phoenix-ios/phoenix-ios/extensions/String+Substring.swift @@ -12,4 +12,8 @@ extension String { let to = index(startIndex, offsetBy: start + limitedLength) return String(self[from.. String { + return substring(location: 0, length: length) + } } diff --git a/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift b/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift index 12546a5c0..c8a0b16b2 100644 --- a/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift +++ b/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift @@ -163,11 +163,17 @@ extension WalletPaymentInfo { return String(localized: "Bump fees", comment: "Payment description for splice CPFP") } else if let il = outgoingPayment as? Lightning_kmpInboundLiquidityOutgoingPayment { - let amount = Utils.formatBitcoin(sat: il.purchase.amount, bitcoinUnit: .sat) - return String( - localized: "+\(amount.string) inbound liquidity", - comment: "Payment description for inbound liquidity" - ) + if il.isManualPurchase() { + return String( + localized: "Manual liquidity", + comment: "Payment description for inbound liquidity" + ) + } else { + return String( + localized: "Automated liquidity", + comment: "Payment description for inbound liquidity" + ) + } } } @@ -276,6 +282,21 @@ extension Lightning_kmpIncomingPayment { } } } + + var isLightningPaymentWithFundingTxId: Bool { + + guard let received else { + return false + } + + return received.receivedWith.contains { rw in + if let lp = rw as? Lightning_kmpIncomingPayment.ReceivedWith_LightningPayment { + return lp.fundingFee?.fundingTxId != nil + } else { + return false + } + } + } } extension Lightning_kmpIncomingPayment.Received { diff --git a/phoenix-ios/phoenix-ios/kotlin/KotlinTypes.swift b/phoenix-ios/phoenix-ios/kotlin/KotlinTypes.swift index d68ed93ea..c5480e561 100644 --- a/phoenix-ios/phoenix-ios/kotlin/KotlinTypes.swift +++ b/phoenix-ios/phoenix-ios/kotlin/KotlinTypes.swift @@ -102,3 +102,13 @@ extension LnurlAuth { typealias Scheme_DEFAULT = LnurlAuth.SchemeDEFAULT_SCHEME typealias Scheme_ANDROID_LEGACY = LnurlAuth.SchemeANDROID_LEGACY_SCHEME } + +extension Lightning_kmpIncomingPayment { + + typealias ReceivedWith_LightningPayment = ReceivedWithLightningPayment + typealias ReceivedWith_AddedToFeeCredit = ReceivedWithAddedToFeeCredit + typealias ReceivedWith_OnChainIncomingPayment = ReceivedWithOnChainIncomingPayment + + typealias ReceivedWith_SpliceIn = ReceivedWithSpliceIn + typealias ReceivedWith_NewChannel = ReceivedWithNewChannel +} diff --git a/phoenix-ios/phoenix-ios/prefs/Prefs+BackupSeed.swift b/phoenix-ios/phoenix-ios/prefs/Prefs+BackupSeed.swift index 3b524a302..cd075208b 100644 --- a/phoenix-ios/phoenix-ios/prefs/Prefs+BackupSeed.swift +++ b/phoenix-ios/phoenix-ios/prefs/Prefs+BackupSeed.swift @@ -16,6 +16,17 @@ class Prefs_BackupSeed { return Prefs.shared.defaults } + /// Updating publishers should always be done on the main thread. + /// Otherwise we risk updating UI components on a background thread, which is dangerous. + /// + private func runOnMainThread(_ block: @escaping () -> Void) { + if Thread.isMainThread { + block() + } else { + DispatchQueue.main.async { block() } + } + } + lazy private(set) var isEnabled_publisher: CurrentValueSubject = { return CurrentValueSubject(self.isEnabled) }() @@ -32,7 +43,9 @@ class Prefs_BackupSeed { set { let key = Key.backupSeed_enabled.rawValue defaults.set(newValue, forKey: key) - isEnabled_publisher.send(newValue) + runOnMainThread { + self.isEnabled_publisher.send(newValue) + } } } @@ -57,7 +70,9 @@ class Prefs_BackupSeed { } else { defaults.removeObject(forKey: key) } - hasUploadedSeed_publisher.send() + runOnMainThread { + self.hasUploadedSeed_publisher.send() + } } lazy private(set) var name_publisher: PassthroughSubject = { @@ -86,7 +101,10 @@ class Prefs_BackupSeed { defaults.setValue(newValue, forKey: key) } setHasUploadedSeed(false, encryptedNodeId: encryptedNodeId) - name_publisher.send() + runOnMainThread { + self.name_publisher.send() + } + } } @@ -111,7 +129,9 @@ class Prefs_BackupSeed { } else { defaults.removeObject(forKey: key) } - manualBackup_taskDone_publisher.send() + runOnMainThread { + self.manualBackup_taskDone_publisher.send() + } } func resetWallet(encryptedNodeId: String) { @@ -122,7 +142,9 @@ class Prefs_BackupSeed { defaults.removeObject(forKey: manualBackup_taskDone_key(encryptedNodeId)) // Reset any publishers with stored state - isEnabled_publisher.send(self.isEnabled) + runOnMainThread { + self.isEnabled_publisher.send(self.isEnabled) + } } } diff --git a/phoenix-ios/phoenix-ios/prefs/Prefs+BackupTransactions.swift b/phoenix-ios/phoenix-ios/prefs/Prefs+BackupTransactions.swift index b1e3a44ed..0b402e179 100644 --- a/phoenix-ios/phoenix-ios/prefs/Prefs+BackupTransactions.swift +++ b/phoenix-ios/phoenix-ios/prefs/Prefs+BackupTransactions.swift @@ -27,6 +27,17 @@ class Prefs_BackupTransactions { return Prefs.shared.defaults } + /// Updating publishers should always be done on the main thread. + /// Otherwise we risk updating UI components on a background thread, which is dangerous. + /// + private func runOnMainThread(_ block: @escaping () -> Void) { + if Thread.isMainThread { + block() + } else { + DispatchQueue.main.async { block() } + } + } + lazy private(set) var isEnabledPublisher: CurrentValueSubject = { return CurrentValueSubject(self.isEnabled) }() @@ -43,7 +54,9 @@ class Prefs_BackupTransactions { set { let key = Key.backupTransactions_enabled.rawValue defaults.set(newValue, forKey: key) - isEnabledPublisher.send(newValue) + runOnMainThread { + self.isEnabledPublisher.send(newValue) + } } } @@ -128,6 +141,8 @@ class Prefs_BackupTransactions { defaults.removeObject(forKey: Key.backupTransactions_useUploadDelay.rawValue) // Reset any publishers with stored state - isEnabledPublisher.send(self.isEnabled) + runOnMainThread { + self.isEnabledPublisher.send(self.isEnabled) + } } } diff --git a/phoenix-ios/phoenix-ios/views/inspect/SummaryInfoGrid.swift b/phoenix-ios/phoenix-ios/views/inspect/SummaryInfoGrid.swift index cc03f380f..0ae924a0e 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/SummaryInfoGrid.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/SummaryInfoGrid.swift @@ -11,9 +11,10 @@ fileprivate var log = LoggerFactory.shared.logger(filename, .warning) struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture discussion @Binding var paymentInfo: WalletPaymentInfo - @Binding var showOriginalFiatValue: Bool + @Binding var showOriginalFiatValue: Bool let showContactView: (_ contact: ContactInfo) -> Void + let switchToPayment: (_ paymentId: WalletPaymentId) -> Void // let minKeyColumnWidth: CGFloat = 50 @@ -42,6 +43,7 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc @State var popoverPresent_standardFees = false @State var popoverPresent_minerFees = false @State var popoverPresent_serviceFees = false + @State var popoverPresent_liquidityCause = false @Environment(\.openURL) var openURL @EnvironmentObject var currencyPrefs: CurrencyPrefs @@ -70,7 +72,13 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc paymentFeesRow_StandardFees() paymentFeesRow_MinerFees() paymentFeesRow_ServiceFees() - paymentDurationRow() + + if isLiquidityPaidInTheFuture() { + causedByRow() + } + + // How do we detect leased liquidity now ? + // paymentDurationRow() paymentErrorRow() } @@ -483,7 +491,7 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc @ViewBuilder func paymentFeesRow_MinerFees() -> some View { - if let minerFees = paymentInfo.payment.minerFees() { + if let minerFees = paymentInfo.payment.minerFees(), !isLiquidityPaidInTheFuture() { paymentFeesRow( msat: minerFees.0, title: minerFees.1, @@ -496,7 +504,7 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc @ViewBuilder func paymentFeesRow_ServiceFees() -> some View { - if let serviceFees = paymentInfo.payment.serviceFees() { + if let serviceFees = paymentInfo.payment.serviceFees(), !isLiquidityPaidInTheFuture() { paymentFeesRow( msat: serviceFees.0, title: serviceFees.1, @@ -606,6 +614,54 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc } } + @ViewBuilder + func causedByRow() -> some View { + let identifier: String = #function + + if let liquidity = paymentInfo.payment as? Lightning_kmpInboundLiquidityOutgoingPayment, + let paymentId = liquidity.relatedPaymentIds().first + { + InfoGridRow( + identifier: identifier, + vAlignment: .firstTextBaseline, + hSpacing: horizontalSpacingBetweenColumns, + keyColumnWidth: keyColumnWidth(identifier: identifier), + keyColumnAlignment: .trailing + ) { + + keyColumn("Caused by") + + } valueColumn: { + + HStack(alignment: VerticalAlignment.center, spacing: 6) { + + Button { + switchToPayment(paymentId) + } label: { + Text(paymentId.dbId) + .lineLimit(1) + .truncationMode(.middle) + } + + Button { + popoverPresent_liquidityCause.toggle() + } label: { + Image(systemName: "questionmark.circle") + .renderingMode(.template) + .foregroundColor(.secondary) + .font(.body) + } + .popover(present: $popoverPresent_liquidityCause) { + InfoPopoverWindow { + Text("This liquidity was required to receive a payment") + } + } + } + + } // + } + } + @ViewBuilder func paymentErrorRow() -> some View { let identifier: String = #function @@ -681,6 +737,15 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc return nil } + + func isLiquidityPaidInTheFuture() -> Bool { + + if let liquidity = paymentInfo.payment as? Lightning_kmpInboundLiquidityOutgoingPayment { + return liquidity.isPaidInTheFuture() + } else { + return false + } + } func toggleCurrencyType() -> Void { currencyPrefs.toggleCurrencyType() diff --git a/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift b/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift index 0512cf283..021bf4d74 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift @@ -1,6 +1,7 @@ import SwiftUI import PhoenixShared import Popovers +import Combine fileprivate let filename = "SummaryView" #if DEBUG && true @@ -44,9 +45,9 @@ struct SummaryView: View { @State var didAppear = false - @State var buttonListTruncationDetected_standard: Bool = false - @State var buttonListTruncationDetected_squeezed: Bool = false - @State var buttonListTruncationDetected_compact: Bool = false + @State var buttonListTruncationDetection_standard: [DynamicTypeSize: Bool] = [:] + @State var buttonListTruncationDetection_squeezed: [DynamicTypeSize: Bool] = [:] + @State var buttonListTruncationDetection_compact: [DynamicTypeSize: Bool] = [:] // @State var navLinkTag: NavLinkTag? = nil @@ -67,6 +68,9 @@ struct SummaryView: View { ) @State var buttonHeight: CGFloat? = nil + @StateObject var blockchainMonitorState = BlockchainMonitorState() + + @Environment(\.dynamicTypeSize) var dynamicTypeSize: DynamicTypeSize @Environment(\.presentationMode) var presentationMode: Binding @EnvironmentObject var navCoordinator: NavigationCoordinator @@ -164,6 +168,9 @@ struct SummaryView: View { .onAppear { onAppear() } + .onChange(of: paymentInfo) { + blockchainMonitorState.paymentInfoPublisher.send($0) + } .task { await monitorBlockchain() } @@ -202,7 +209,7 @@ struct SummaryView: View { VStack { Group { if payment is Lightning_kmpInboundLiquidityOutgoingPayment { - Text("Liquidity Added") + Text("Channel Resized") } else if payment is Lightning_kmpOutgoingPayment { Text("SENT") @@ -505,22 +512,28 @@ struct SummaryView: View { SummaryInfoGrid( paymentInfo: $paymentInfo, showOriginalFiatValue: $showOriginalFiatValue, - showContactView: showContactView + showContactView: showContactView, + switchToPayment: switchToPayment ) } @ViewBuilder func buttonList() -> some View { + let dts = dynamicTypeSize + let buttonListTruncationDetected_compact = buttonListTruncationDetection_compact[dts] ?? false + let buttonListTruncationDetected_squeezed = buttonListTruncationDetection_squeezed[dts] ?? false + let buttonListTruncationDetected_standard = buttonListTruncationDetection_standard[dts] ?? false + Group { if buttonListTruncationDetected_compact { buttonList_accessibility() } else if buttonListTruncationDetected_squeezed { - buttonList_compact() + buttonList_compact(dts) } else if buttonListTruncationDetected_standard { - buttonList_squeezed() + buttonList_squeezed(dts) } else { - buttonList_standard() + buttonList_standard(dts) } } // .confirmationDialog("Delete payment?", @@ -534,7 +547,7 @@ struct SummaryView: View { } @ViewBuilder - func buttonList_standard() -> some View { + func buttonList_standard(_ dts: DynamicTypeSize) -> some View { // We're making all the buttons the same size. // @@ -556,8 +569,8 @@ struct SummaryView: View { .read(buttonWidthReader) .read(buttonHeightReader) } wasTruncated: { - log.debug("buttonListTruncationDetected_standard = true (details)") - buttonListTruncationDetected_standard = true + log.debug("buttonListTruncationDetection_standard[\(dts)] = true (details)") + buttonListTruncationDetection_standard[dts] = true } if let buttonHeight = buttonHeight { @@ -574,8 +587,8 @@ struct SummaryView: View { .read(buttonWidthReader) .read(buttonHeightReader) } wasTruncated: { - log.debug("buttonListTruncationDetected_standard = true (edit)") - buttonListTruncationDetected_standard = true + log.debug("buttonListTruncationDetection_standard[\(dts)] = true (edit)") + buttonListTruncationDetection_standard[dts] = true } if let buttonHeight = buttonHeight { @@ -592,8 +605,8 @@ struct SummaryView: View { .read(buttonWidthReader) .read(buttonHeightReader) } wasTruncated: { - log.debug("buttonListTruncationDetected_standard = true (delete)") - buttonListTruncationDetected_standard = true + log.debug("buttonListTruncationDetection_standard[\(dts)] = true (delete)") + buttonListTruncationDetection_standard[dts] = true } } .padding(.all) @@ -602,7 +615,7 @@ struct SummaryView: View { } @ViewBuilder - func buttonList_squeezed() -> some View { + func buttonList_squeezed(_ dts: DynamicTypeSize) -> some View { // There's not enough space to make all the buttons the same size. // So we're just making the left & right buttons the same size. @@ -626,8 +639,8 @@ struct SummaryView: View { .read(buttonWidthReader) .read(buttonHeightReader) } wasTruncated: { - log.debug("buttonListTruncationDetected_squeezed = true (edit)") - buttonListTruncationDetected_squeezed = true + log.debug("buttonListTruncationDetection_squeezed[\(dts)] = true (edit)") + buttonListTruncationDetection_squeezed[dts] = true } if let buttonHeight = buttonHeight { @@ -643,8 +656,8 @@ struct SummaryView: View { } .read(buttonHeightReader) } wasTruncated: { - log.debug("buttonListTruncationDetected_squeezed = true (edit)") - buttonListTruncationDetected_squeezed = true + log.debug("buttonListTruncationDetection_squeezed[\(dts)] = true (edit)") + buttonListTruncationDetection_squeezed[dts] = true } if let buttonHeight = buttonHeight { @@ -662,8 +675,8 @@ struct SummaryView: View { .read(buttonWidthReader) .read(buttonHeightReader) } wasTruncated: { - log.debug("buttonListTruncationDetected_squeezed = true (delete)") - buttonListTruncationDetected_squeezed = true + log.debug("buttonListTruncationDetection_squeezed[\(dts)] = true (delete)") + buttonListTruncationDetection_squeezed[dts] = true } } .padding(.horizontal, 10) // allow content to be closer to edges @@ -673,7 +686,7 @@ struct SummaryView: View { } @ViewBuilder - func buttonList_compact() -> some View { + func buttonList_compact(_ dts: DynamicTypeSize) -> some View { // There's a large font being used, and possibly a small screen too. // Thus horizontal space is tight. @@ -696,8 +709,8 @@ struct SummaryView: View { } .read(buttonHeightReader) } wasTruncated: { - log.debug("buttonListTruncationDetected_compact = true (details)") - buttonListTruncationDetected_compact = true + log.debug("buttonListTruncationDetection_compact[\(dts)] = true (details)") + buttonListTruncationDetection_compact[dts] = true } if let buttonHeight = buttonHeight { @@ -713,8 +726,8 @@ struct SummaryView: View { } .read(buttonHeightReader) } wasTruncated: { - log.debug("buttonListTruncationDetected_compact = true (edit)") - buttonListTruncationDetected_compact = true + log.debug("buttonListTruncationDetection_compact[\(dts)] = true (edit)") + buttonListTruncationDetection_compact[dts] = true } if let buttonHeight = buttonHeight { @@ -730,8 +743,8 @@ struct SummaryView: View { } .read(buttonHeightReader) } wasTruncated: { - log.debug("buttonListTruncationDetected_compact = true (delete)") - buttonListTruncationDetected_compact = true + log.debug("buttonListTruncationDetection_compact[\(dts)] = true (delete)") + buttonListTruncationDetection_compact[dts] = true } } .padding(.horizontal, 4) // allow content to be closer to edges @@ -936,28 +949,60 @@ struct SummaryView: View { // MARK: Tasks // -------------------------------------------------- - func updateConfirmations(_ onChainPayment: Lightning_kmpOnChainOutgoingPayment) async -> Int { - log.trace("checkConfirmations()") + func monitorBlockchain() async { - do { - let result = try await Biz.business.electrumClient.kotlin_getConfirmations(txid: onChainPayment.txId) - - let confirmations = result?.intValue ?? 0 - log.debug("checkConfirmations(): => \(confirmations)") - - self.blockchainConfirmations = confirmations - return confirmations - } catch { - log.error("checkConfirmations(): error: \(error)") - return 0 + // Architecture note: + // We need the ability to reset the View to display a completely different payment. + // This means our Task to monitor the blockchain needs to properly respond whenever + // the `paymentInfo` property is changed. + + // This is needed when re-displaying the View. + // We have code to ignore duplicates below so that extra fires don't interrupt our work. + blockchainMonitorState.paymentInfoPublisher.send(self.paymentInfo) + + var lastPaymentId: WalletPaymentId? = nil + for await paymentInfo in blockchainMonitorState.paymentInfoPublisher.values { + + guard !Task.isCancelled else { + log.debug("monitorBlockchain(): Task.isCancelled") + return + } + + if let paymentInfo, paymentInfo.id() == lastPaymentId { + log.debug("monitorBlockchain: ignoring duplicate paymentInfo") + continue + } + lastPaymentId = paymentInfo?.id() + + if let currentTask = blockchainMonitorState.currentTaskPublisher.value { + log.debug("monitorBlockchain: currentTask.cancel()") + currentTask.cancel() + } + + if let paymentInfo { + log.debug("monitorBlockchain: processing new paymentInfo") + + let newTask = Task { @MainActor in + await monitorBlockchain(paymentInfo) + } + blockchainMonitorState.currentTaskPublisher.send(newTask) + + } else { + log.debug("monitorBlockchain: paymentInfo is nil") + blockchainMonitorState.currentTaskPublisher.send(nil) + } } + + log.debug("monitorBlockchain: terminated") } - func monitorBlockchain() async { - log.trace("monitorBlockchain()") + func monitorBlockchain(_ paymentInfo: WalletPaymentInfo) async { + + let pid: String = paymentInfo.id().dbId.prefix(maxLength: 8) + log.trace("monitorBlockchain(\(pid))") guard let onChainPayment = paymentInfo.payment as? Lightning_kmpOnChainOutgoingPayment else { - log.debug("monitorBlockchain(): not an on-chain payment") + log.debug("monitorBlockchain(\(pid)): not an on-chain payment") return } @@ -966,43 +1011,85 @@ struct SummaryView: View { if elapsed > 24.hours() { // It was marked as mined more than 24 hours ago. // So there's really no need to check the exact confirmation count anymore. - log.debug("monitorBlockchain(): confirmedAt > 24.hours.ago") + log.debug("monitorBlockchain(\(pid)): confirmedAt > 24.hours.ago") self.blockchainConfirmations = 7 return } } - let confirmations = await updateConfirmations(onChainPayment) - if confirmations > 6 { - // No need to continue checking confirmation count, - // because the UI displays "6+" from this point forward. - log.debug("monitorBlockchain(): confirmations > 6") + let isDone = await updateConfirmations(onChainPayment, pid) + guard !isDone else { + log.debug("monitorBlockchain(\(pid)): done") return } for await notification in Biz.business.electrumClient.notificationsPublisher().values { - if notification is Lightning_kmpHeaderSubscriptionResponse { - // A new block was mined ! - // Update confirmation count if needed. - let confirmations = await updateConfirmations(onChainPayment) - if confirmations > 6 { - // No need to continue checking confirmation count, - // because the UI displays "6+" from this point forward. - log.debug("monitorBlockchain(): confirmations > 6") - break - } - - } else { - log.debug("monitorBlockchain(): notification isNot HeaderSubscriptionResponse") + guard !Task.isCancelled else { + log.debug("monitorBlockchain(\(pid)): Task.isCancelled") + return } - if Task.isCancelled { - log.debug("monitorBlockchain(): Task.isCancelled") - break - } else { - log.debug("monitorBlockchain(): Waiting for next electrum notification...") + if !(notification is Lightning_kmpHeaderSubscriptionResponse) { + log.debug("monitorBlockchain(\(pid)): notification isNot HeaderSubscriptionResponse") + continue + } + + // A new block was mined ! + // Update confirmation count if needed. + + let isDone = await updateConfirmations(onChainPayment, pid) + guard !isDone else { + log.debug("monitorBlockchain(\(pid)): done") + return } + + log.debug("monitorBlockchain(\(pid)): Waiting for next electrum notification...") + } + + log.debug("monitorBlockchain(\(pid)): terminated") + } + + func updateConfirmations( + _ onChainPayment: Lightning_kmpOnChainOutgoingPayment, + _ pid: String + ) async -> Bool { + + log.trace("updateConfirmations(\(pid))") + + let confirmations = await fetchConfirmations(onChainPayment, pid) + guard !Task.isCancelled else { + log.debug("updateConfirmations(\(pid)): Task.isCancelled") + return true + } + self.blockchainConfirmations = confirmations + + if confirmations > 6 { + // No need to continue checking confirmation count, + // because the UI displays "6+" from this point forward. + log.debug("updateConfirmations(\(pid)): confirmations > 6") + return true + } else { + return false + } + } + + func fetchConfirmations( + _ onChainPayment: Lightning_kmpOnChainOutgoingPayment, + _ pid: String + ) async -> Int { + + log.trace("fetchConfirmations(\(pid))") + + do { + let result = try await Biz.business.electrumClient.kotlin_getConfirmations(txid: onChainPayment.txId) + + let confirmations = result?.intValue ?? 0 + log.debug("fetchConfirmations(\(pid)): => \(confirmations)") + return confirmations + } catch { + log.error("checkConfirmations(\(pid)): error: \(error)") + return 0 } } @@ -1109,6 +1196,19 @@ struct SummaryView: View { navigateTo(.ContactView(contact: contact)) } + func switchToPayment(_ paymentId: WalletPaymentId) { + log.trace("switchToPayment: \(paymentId.dbId)") + + Biz.business.paymentsManager.getPayment(id: paymentId, options: fetchOptions) { + (result: WalletPaymentInfo?, _) in + + if let result { + paymentInfo = result + blockchainConfirmations = nil + } + } + } + func exploreTx(_ txId: Bitcoin_kmpTxId, website: BlockchainExplorer.Website) { log.trace("exploreTX()") @@ -1149,3 +1249,12 @@ struct SummaryView: View { } } } + +// -------------------------------------------------- +// MARK: - +// -------------------------------------------------- + +class BlockchainMonitorState: ObservableObject { + let paymentInfoPublisher = CurrentValueSubject(nil) + let currentTaskPublisher = CurrentValueSubject?, Never>(nil) +} diff --git a/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift b/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift index 1974bc38f..844a107b6 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift @@ -1,6 +1,12 @@ import Foundation import PhoenixShared +fileprivate let filename = "WalletPaymentExtensions" +#if DEBUG && true +fileprivate var log = LoggerFactory.shared.logger(filename, .trace) +#else +fileprivate var log = LoggerFactory.shared.logger(filename, .warning) +#endif extension Lightning_kmpWalletPayment { @@ -84,11 +90,38 @@ extension Lightning_kmpWalletPayment { // An incomingPayment may have service fees if a new channel was automatically opened if let received = incomingPayment.received { +// received.receivedWith.forEach { rw in +// if let _ = rw as? Lightning_kmpIncomingPayment.ReceivedWith_LightningPayment { +// log.debug("ReceivedWith_LightningPayment") +// +// } else if let _ = rw as? Lightning_kmpIncomingPayment.ReceivedWith_AddedToFeeCredit { +// log.debug("ReceivedWith_AddedToFeeCredit") +// +// } else if let _ = rw as? Lightning_kmpIncomingPayment.ReceivedWith_SpliceIn { +// log.debug("ReceivedWith_SpliceIn") +// +// } else if let _ = rw as? Lightning_kmpIncomingPayment.ReceivedWith_NewChannel { +// log.debug("ReceivedWith_NewChannel") +// +// } else { +// log.debug("ReceivedWith_???") +// } +// } + let msat = received.receivedWith.map { - if let newChannel = $0 as? Lightning_kmpIncomingPayment.ReceivedWithNewChannel { + if let lightning = $0 as? Lightning_kmpIncomingPayment.ReceivedWith_LightningPayment { + if lightning.fundingFee?.fundingTxId != nil { + return 0 // should be separated into miner & service fees + } else { + return lightning.fees.msat + } + + } else if let newChannel = $0 as? Lightning_kmpIncomingPayment.ReceivedWithNewChannel { return newChannel.serviceFee.msat + } else if let spliceIn = $0 as? Lightning_kmpIncomingPayment.ReceivedWithSpliceIn { return spliceIn.serviceFee.msat + } else { return $0.fees.msat } @@ -96,8 +129,8 @@ extension Lightning_kmpWalletPayment { if msat > 0 { - let title = NSLocalizedString("Service Fees", comment: "Label in SummaryInfoGrid") - let exp = NSLocalizedString( + let title = String(localized: "Service Fees (1)", comment: "Label in SummaryInfoGrid") + let exp = String(localized: """ In order to receive this payment, a new payment channel was opened. \ This is not always required. @@ -106,13 +139,13 @@ extension Lightning_kmpWalletPayment { ) return (msat, title, exp) - } - else if !incomingPayment.isSpliceIn { + + } else if !incomingPayment.isSpliceIn && !incomingPayment.isLightningPaymentWithFundingTxId { // I think it's nice to see "Fees: 0 sat" :) let msat = Int64(0) - let title = NSLocalizedString("Fees", comment: "Label in SummaryInfoGrid") + let title = String(localized: "Fees", comment: "Label in SummaryInfoGrid") let exp = "" return (msat, title, exp) @@ -135,27 +168,28 @@ extension Lightning_kmpWalletPayment { hops += part.route.count } - let title = NSLocalizedString("Lightning Fees", comment: "Label in SummaryInfoGrid") + let title = String(localized: "Lightning Fees", comment: "Label in SummaryInfoGrid") let exp: String if parts == 1 { if hops == 1 { - exp = NSLocalizedString( - "Lightning fees for routing the payment. Payment required 1 hop.", + exp = String( + localized: "Lightning fees for routing the payment. Payment required 1 hop.", comment: "Fees explanation" ) } else { - exp = String(format: NSLocalizedString( - "Lightning fees for routing the payment. Payment required %d hops.", - comment: "Fees explanation"), - hops + exp = String( + localized: "Lightning fees for routing the payment. Payment required \(hops) hops.", + comment: "Fees explanation" ) } } else { - exp = String(format: NSLocalizedString( - "Lightning fees for routing the payment. Payment was divided into %d parts, using %d hops.", - comment: "Fees explanation"), - parts, hops + exp = String(localized: + """ + Lightning fees for routing the payment. \ + Payment was divided into \(parts) parts, using \(hops) hops. + """, + comment: "Fees explanation" ) } @@ -175,9 +209,9 @@ extension Lightning_kmpWalletPayment { // An incomingPayment may have minerFees if a new channel was opened using dual-funding let sat = received.receivedWith.map { - if let newChannel = $0 as? Lightning_kmpIncomingPayment.ReceivedWithNewChannel { + if let newChannel = $0 as? Lightning_kmpIncomingPayment.ReceivedWith_NewChannel { return newChannel.miningFee.sat - } else if let spliceIn = $0 as? Lightning_kmpIncomingPayment.ReceivedWithSpliceIn { + } else if let spliceIn = $0 as? Lightning_kmpIncomingPayment.ReceivedWith_SpliceIn { return spliceIn.miningFee.sat } else { return Int64(0) @@ -187,9 +221,9 @@ extension Lightning_kmpWalletPayment { if sat > 0 { let msat = Utils.toMsat(sat: sat) - let title = NSLocalizedString("Miner Fees", comment: "Label in SummaryInfoGrid") - let exp = NSLocalizedString( - "Bitcoin network fees paid for on-chain transaction.", + let title = String(localized: "Miner Fees", comment: "Label in SummaryInfoGrid") + let exp = String( + localized: "Bitcoin network fees paid for on-chain transaction.", comment: "Fees explanation" ) @@ -212,9 +246,9 @@ extension Lightning_kmpWalletPayment { let sat = onChainOutgoingPayment.miningFees.sat let msat = Utils.toMsat(sat: sat) - let title = NSLocalizedString("Miner Fees", comment: "Label in SummaryInfoGrid") - let exp = NSLocalizedString( - "Bitcoin network fees paid for on-chain transaction.", + let title = String(localized: "Miner Fees", comment: "Label in SummaryInfoGrid") + let exp = String( + localized: "Bitcoin network fees paid for on-chain transaction.", comment: "Fees explanation" ) @@ -231,9 +265,9 @@ extension Lightning_kmpWalletPayment { let sat = il.purchase.fees.serviceFee let msat = Utils.toMsat(sat: sat) - let title = NSLocalizedString("Service Fees", comment: "Label in SummaryInfoGrid") - let exp = NSLocalizedString( - "Fees paid for the liquidity service.", + let title = String(localized: "Service Fees (2)", comment: "Label in SummaryInfoGrid") + let exp = String( + localized: "Fees paid for the liquidity service.", comment: "Fees explanation" ) @@ -244,9 +278,9 @@ extension Lightning_kmpWalletPayment { { let msat = outgoingPayment.fees.msat - outgoingPayment.routingFee.msat - let title = NSLocalizedString("Swap Fees", comment: "Label in SummaryInfoGrid") - let exp = NSLocalizedString( - "Includes Bitcoin network miner fees, and the fee for the Swap-Out service.", + let title = String(localized: "Swap Fees", comment: "Label in SummaryInfoGrid") + let exp = String( + localized: "Includes Bitcoin network miner fees, and the fee for the Swap-Out service.", comment: "Fees explanation" ) diff --git a/phoenix-ios/phoenix-ios/views/receive/ReceiveView.swift b/phoenix-ios/phoenix-ios/views/receive/ReceiveView.swift index 35ddf79ba..9af4aff8f 100644 --- a/phoenix-ios/phoenix-ios/views/receive/ReceiveView.swift +++ b/phoenix-ios/phoenix-ios/views/receive/ReceiveView.swift @@ -33,7 +33,6 @@ struct ReceiveView: MVIView { @StateObject var toast = Toast() @Environment(\.colorScheme) var colorScheme - @Environment(\.dynamicTypeSize) var dynamicTypeSize: DynamicTypeSize @EnvironmentObject var deviceInfo: DeviceInfo @EnvironmentObject var popoverState: PopoverState diff --git a/phoenix-ios/phoenix-ios/views/transactions/PaymentCell.swift b/phoenix-ios/phoenix-ios/views/transactions/PaymentCell.swift index 3b849da8e..31d6d3ad0 100644 --- a/phoenix-ios/phoenix-ios/views/transactions/PaymentCell.swift +++ b/phoenix-ios/phoenix-ios/views/transactions/PaymentCell.swift @@ -166,7 +166,12 @@ struct PaymentCell : View { func paymentAmount() -> some View { let (amount, isFailure, isOutgoing) = paymentAmountInfo() - if currencyPrefs.hideAmounts { + if isLiquidityPaidInTheFuture() { + + Text(verbatim: "") + .accessibilityHidden(true) + + } else if currencyPrefs.hideAmounts { HStack(alignment: VerticalAlignment.firstTextBaseline, spacing: 0) { @@ -241,16 +246,38 @@ struct PaymentCell : View { if let contact = fetched?.contact { if let payment = fetched?.payment, payment.isIncoming() { - return String(localized: "\(timestamp) - from \(contact.name)") + return String(localized: "\(timestamp) ∙ from \(contact.name)") } else { - return String(localized: "\(timestamp) - to \(contact.name)") + return String(localized: "\(timestamp) ∙ to \(contact.name)") } + } else if + let payment = fetched?.payment, + let liquidity = payment as? Lightning_kmpInboundLiquidityOutgoingPayment + { + let amount = Utils.formatBitcoin(sat: liquidity.purchase.amount, bitcoinUnit: .sat) + return "\(timestamp) ∙ +\(amount.string)" + } else { return timestamp } } + func line2HasExtraInfo() -> Bool { + + if fetched?.contact != nil { + // Also going to display contact name + return true + } + + if fetched?.payment is Lightning_kmpInboundLiquidityOutgoingPayment { + // Also going to display liquidity amount + return true + } + + return false + } + func stringForDate(_ completedAtDate: Date) -> String { let calendar = Calendar.current @@ -260,7 +287,7 @@ struct PaymentCell : View { let yearA = compsA.year ?? 0 let yearB = compsB.year ?? 0 - let preferShortDate = (textScaling > 100) || (fetched?.contact != nil) + let preferShortDate = (textScaling > 100) || line2HasExtraInfo() let formatter = DateFormatter() if yearA == yearB { @@ -320,6 +347,17 @@ struct PaymentCell : View { } } + func isLiquidityPaidInTheFuture() -> Bool { + + if let payment = fetched?.payment, + let liquidity = payment as? Lightning_kmpInboundLiquidityOutgoingPayment + { + return liquidity.isPaidInTheFuture() + } else { + return false + } + } + // -------------------------------------------------- // MARK: Notifications // -------------------------------------------------- From a764b4cba0abfb5355903bc98110235ab7b8bd25 Mon Sep 17 00:00:00 2001 From: Robbie Hanson <304604+robbiehanson@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:30:09 -0500 Subject: [PATCH 17/25] (ios) WIP: Updating UI --- phoenix-ios/phoenix-ios/Localizable.xcstrings | 7 -- .../kotlin/KotlinExtensions+Payments.swift | 20 +++-- .../views/inspect/SummaryInfoGrid.swift | 88 +++++++++++++++---- .../views/inspect/SummaryView.swift | 65 ++++++++++---- .../inspect/WalletPaymentExtensions.swift | 22 +---- 5 files changed, 136 insertions(+), 66 deletions(-) diff --git a/phoenix-ios/phoenix-ios/Localizable.xcstrings b/phoenix-ios/phoenix-ios/Localizable.xcstrings index ebe4a3c7a..58c01d3fb 100644 --- a/phoenix-ios/phoenix-ios/Localizable.xcstrings +++ b/phoenix-ios/phoenix-ios/Localizable.xcstrings @@ -35935,7 +35935,6 @@ }, "Service Fees" : { "comment" : "Label in SummaryInfoGrid", - "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -35975,12 +35974,6 @@ } } }, - "Service Fees (1)" : { - "comment" : "Label in SummaryInfoGrid" - }, - "Service Fees (2)" : { - "comment" : "Label in SummaryInfoGrid" - }, "Service returned unreadable response" : { "comment" : "Error message - scanning lightning invoice", "extractionState" : "manual", diff --git a/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift b/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift index c8a0b16b2..340cbc40d 100644 --- a/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift +++ b/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift @@ -283,19 +283,25 @@ extension Lightning_kmpIncomingPayment { } } - var isLightningPaymentWithFundingTxId: Bool { + var lightningPaymentFundingTxId: Bitcoin_kmpTxId? { guard let received else { - return false + return nil } - return received.receivedWith.contains { rw in - if let lp = rw as? Lightning_kmpIncomingPayment.ReceivedWith_LightningPayment { - return lp.fundingFee?.fundingTxId != nil - } else { - return false + for rw in received.receivedWith { + if let lp = rw as? Lightning_kmpIncomingPayment.ReceivedWith_LightningPayment, + let txId = lp.fundingFee?.fundingTxId + { + return txId } } + + return nil + } + + var isLightningPaymentWithFundingTxId: Bool { + return lightningPaymentFundingTxId != nil } } diff --git a/phoenix-ios/phoenix-ios/views/inspect/SummaryInfoGrid.swift b/phoenix-ios/phoenix-ios/views/inspect/SummaryInfoGrid.swift index 0ae924a0e..c4c5dd952 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/SummaryInfoGrid.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/SummaryInfoGrid.swift @@ -13,6 +13,8 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc @Binding var paymentInfo: WalletPaymentInfo @Binding var showOriginalFiatValue: Bool + @Binding var liquidityPayment: Lightning_kmpInboundLiquidityOutgoingPayment? + let showContactView: (_ contact: ContactInfo) -> Void let switchToPayment: (_ paymentId: WalletPaymentId) -> Void @@ -48,6 +50,10 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc @Environment(\.openURL) var openURL @EnvironmentObject var currencyPrefs: CurrencyPrefs + // -------------------------------------------------- + // MARK: View Builders + // -------------------------------------------------- + @ViewBuilder var infoGridRows: some View { @@ -73,9 +79,7 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc paymentFeesRow_MinerFees() paymentFeesRow_ServiceFees() - if isLiquidityPaidInTheFuture() { - causedByRow() - } + causedByRow() // How do we detect leased liquidity now ? // paymentDurationRow() @@ -83,6 +87,9 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc paymentErrorRow() } .padding([.leading, .trailing]) + .onAppear { + onAppear() + } } @ViewBuilder @@ -478,7 +485,7 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc @ViewBuilder func paymentFeesRow_StandardFees() -> some View { - if let standardFees = paymentInfo.payment.standardFees() { + if let standardFees = standardFees() { paymentFeesRow( msat: standardFees.0, title: standardFees.1, @@ -491,7 +498,7 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc @ViewBuilder func paymentFeesRow_MinerFees() -> some View { - if let minerFees = paymentInfo.payment.minerFees(), !isLiquidityPaidInTheFuture() { + if let minerFees = minerFees() { paymentFeesRow( msat: minerFees.0, title: minerFees.1, @@ -504,7 +511,7 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc @ViewBuilder func paymentFeesRow_ServiceFees() -> some View { - if let serviceFees = paymentInfo.payment.serviceFees(), !isLiquidityPaidInTheFuture() { + if let serviceFees = serviceFees() { paymentFeesRow( msat: serviceFees.0, title: serviceFees.1, @@ -686,10 +693,70 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc } } + // -------------------------------------------------- + // MARK: Notifications + // -------------------------------------------------- + + func onAppear() { + log.trace("onAppear()") + + if let liquidity = paymentInfo.payment as? Lightning_kmpInboundLiquidityOutgoingPayment { + log.debug("is: Lightning_kmpInboundLiquidityOutgoingPayment") + + if let paymentId = liquidity.relatedPaymentIds().first { + log.debug("paymentId = \(paymentId.dbId)") + } else { + log.debug("paymentId = nil") + } + + } else { + log.debug("is NOT: Lightning_kmpInboundLiquidityOutgoingPayment") + } + } + // -------------------------------------------------- // MARK: Utilities // -------------------------------------------------- + func standardFees() -> (Int64, String, String)? { + + return paymentInfo.payment.standardFees() + } + + func minerFees() -> (Int64, String, String)? { + + if let liquidity = paymentInfo.payment as? Lightning_kmpInboundLiquidityOutgoingPayment, + liquidity.isPaidInTheFuture() { + // We don't display the fees here. + // Instead we're displaying the fees on the corresponding IncomingPayment. + return nil + } else if let result = paymentInfo.payment.minerFees() { + return result + } else if let liquidityPayment, liquidityPayment.isPaidInTheFuture() { + // This is the corresponding IncomingPayment, and we have the linked liquidityPayment. + return liquidityPayment.minerFees() + } else { + return nil + } + } + + func serviceFees() -> (Int64, String, String)? { + + if let liquidity = paymentInfo.payment as? Lightning_kmpInboundLiquidityOutgoingPayment, + liquidity.isPaidInTheFuture() { + // We don't display the fees here. + // Instead we're displaying the fees on the corresponding IncomingPayment. + return nil + } else if let result = paymentInfo.payment.serviceFees() { + return result + } else if let liquidityPayment, liquidityPayment.isPaidInTheFuture() { + // This is the corresponding IncomingPayment, and we have the linked liquidityPayment. + return liquidityPayment.serviceFees() + } else { + return nil + } + } + func formattedAmount(msat: Int64) -> FormattedAmount { if showOriginalFiatValue && currencyPrefs.currencyType == .fiat { @@ -737,15 +804,6 @@ struct SummaryInfoGrid: InfoGridView { // See InfoGridView for architecture disc return nil } - - func isLiquidityPaidInTheFuture() -> Bool { - - if let liquidity = paymentInfo.payment as? Lightning_kmpInboundLiquidityOutgoingPayment { - return liquidity.isPaidInTheFuture() - } else { - return false - } - } func toggleCurrencyType() -> Void { currencyPrefs.toggleCurrencyType() diff --git a/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift b/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift index 021bf4d74..967b2fe5d 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift @@ -33,6 +33,8 @@ struct SummaryView: View { @State var paymentInfo: WalletPaymentInfo @State var paymentInfoIsStale: Bool + @State var liquidityPayment: Lightning_kmpInboundLiquidityOutgoingPayment? = nil + let fetchOptions = WalletPaymentFetchOptions.companion.All @State var blockchainConfirmations: Int? = nil @@ -168,8 +170,8 @@ struct SummaryView: View { .onAppear { onAppear() } - .onChange(of: paymentInfo) { - blockchainMonitorState.paymentInfoPublisher.send($0) + .onChange(of: paymentInfo) { _ in + paymentInfoChanged() } .task { await monitorBlockchain() @@ -512,6 +514,7 @@ struct SummaryView: View { SummaryInfoGrid( paymentInfo: $paymentInfo, showOriginalFiatValue: $showOriginalFiatValue, + liquidityPayment: $liquidityPayment, showContactView: showContactView, switchToPayment: switchToPayment ) @@ -956,17 +959,11 @@ struct SummaryView: View { // This means our Task to monitor the blockchain needs to properly respond whenever // the `paymentInfo` property is changed. - // This is needed when re-displaying the View. - // We have code to ignore duplicates below so that extra fires don't interrupt our work. - blockchainMonitorState.paymentInfoPublisher.send(self.paymentInfo) - var lastPaymentId: WalletPaymentId? = nil + + // Note: When the task is cancelled, the `values` stream returns nil, and we exit the loop + for await paymentInfo in blockchainMonitorState.paymentInfoPublisher.values { - - guard !Task.isCancelled else { - log.debug("monitorBlockchain(): Task.isCancelled") - return - } if let paymentInfo, paymentInfo.id() == lastPaymentId { log.debug("monitorBlockchain: ignoring duplicate paymentInfo") @@ -992,7 +989,12 @@ struct SummaryView: View { blockchainMonitorState.currentTaskPublisher.send(nil) } } - + + if let currentTask = blockchainMonitorState.currentTaskPublisher.value { + log.debug("monitorBlockchain: currentTask.cancel()") + currentTask.cancel() + } + log.debug("monitorBlockchain: terminated") } @@ -1023,13 +1025,10 @@ struct SummaryView: View { return } + // Note: When the task is cancelled, the `values` stream returns nil, and we exit the loop + for await notification in Biz.business.electrumClient.notificationsPublisher().values { - guard !Task.isCancelled else { - log.debug("monitorBlockchain(\(pid)): Task.isCancelled") - return - } - if !(notification is Lightning_kmpHeaderSubscriptionResponse) { log.debug("monitorBlockchain(\(pid)): notification isNot HeaderSubscriptionResponse") continue @@ -1129,9 +1128,13 @@ struct SummaryView: View { } } } + } else { + // Not triggered in this particular case, so we need to trigger it manually. + paymentInfoChanged() } } else { + log.trace("subsequent appearance") // We are returning from the DetailsView/EditInfoView (via the NavigationController) // The payment metadata may have changed (e.g. description/notes modified). @@ -1163,6 +1166,33 @@ struct SummaryView: View { } } + func paymentInfoChanged() { + log.trace("paymentInfoChanged()") + + blockchainMonitorState.paymentInfoPublisher.send(paymentInfo) + + if let incomingPayment = paymentInfo.payment as? Lightning_kmpIncomingPayment, + let fundingTxId = incomingPayment.lightningPaymentFundingTxId + { + Task { @MainActor in + do { + let paymentsManager = Biz.business.paymentsManager + let payment = try await paymentsManager.getLiquidityPurchaseForTxId(txId: fundingTxId) + if let payment { + log.debug("liquidityPayment = \(payment.walletPaymentId().dbId)") + liquidityPayment = payment + } else { + log.debug("liquidityPayment = nil") + } + + } catch { + log.error("getLiquidityPurchaseForTxId(): error: \(error)") + } + + } + } + } + // -------------------------------------------------- // MARK: Actions // -------------------------------------------------- @@ -1204,6 +1234,7 @@ struct SummaryView: View { if let result { paymentInfo = result + liquidityPayment = nil blockchainConfirmations = nil } } diff --git a/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift b/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift index 844a107b6..4578f8395 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/WalletPaymentExtensions.swift @@ -90,24 +90,6 @@ extension Lightning_kmpWalletPayment { // An incomingPayment may have service fees if a new channel was automatically opened if let received = incomingPayment.received { -// received.receivedWith.forEach { rw in -// if let _ = rw as? Lightning_kmpIncomingPayment.ReceivedWith_LightningPayment { -// log.debug("ReceivedWith_LightningPayment") -// -// } else if let _ = rw as? Lightning_kmpIncomingPayment.ReceivedWith_AddedToFeeCredit { -// log.debug("ReceivedWith_AddedToFeeCredit") -// -// } else if let _ = rw as? Lightning_kmpIncomingPayment.ReceivedWith_SpliceIn { -// log.debug("ReceivedWith_SpliceIn") -// -// } else if let _ = rw as? Lightning_kmpIncomingPayment.ReceivedWith_NewChannel { -// log.debug("ReceivedWith_NewChannel") -// -// } else { -// log.debug("ReceivedWith_???") -// } -// } - let msat = received.receivedWith.map { if let lightning = $0 as? Lightning_kmpIncomingPayment.ReceivedWith_LightningPayment { if lightning.fundingFee?.fundingTxId != nil { @@ -129,7 +111,7 @@ extension Lightning_kmpWalletPayment { if msat > 0 { - let title = String(localized: "Service Fees (1)", comment: "Label in SummaryInfoGrid") + let title = String(localized: "Service Fees", comment: "Label in SummaryInfoGrid") let exp = String(localized: """ In order to receive this payment, a new payment channel was opened. \ @@ -265,7 +247,7 @@ extension Lightning_kmpWalletPayment { let sat = il.purchase.fees.serviceFee let msat = Utils.toMsat(sat: sat) - let title = String(localized: "Service Fees (2)", comment: "Label in SummaryInfoGrid") + let title = String(localized: "Service Fees", comment: "Label in SummaryInfoGrid") let exp = String( localized: "Fees paid for the liquidity service.", comment: "Fees explanation" From 58e1d47f693cb5aefad52db3d73c6071709fa062 Mon Sep 17 00:00:00 2001 From: Robbie Hanson <304604+robbiehanson@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:29:34 -0500 Subject: [PATCH 18/25] (ios) Fixing compiler error post-rebase --- phoenix-ios/phoenix-ios/Localizable.xcstrings | 3 --- .../phoenix-ios/views/notifications/BizNotificationCell.swift | 2 -- 2 files changed, 5 deletions(-) diff --git a/phoenix-ios/phoenix-ios/Localizable.xcstrings b/phoenix-ios/phoenix-ios/Localizable.xcstrings index 58c01d3fb..4cbf66ce8 100644 --- a/phoenix-ios/phoenix-ios/Localizable.xcstrings +++ b/phoenix-ios/phoenix-ios/Localizable.xcstrings @@ -9955,9 +9955,6 @@ } } } - }, - "Channel funding in progress." : { - }, "channel id" : { "comment" : "Label in DetailsView_IncomingPayment", diff --git a/phoenix-ios/phoenix-ios/views/notifications/BizNotificationCell.swift b/phoenix-ios/phoenix-ios/views/notifications/BizNotificationCell.swift index 4dc3a04c6..fbdfe7dae 100644 --- a/phoenix-ios/phoenix-ios/views/notifications/BizNotificationCell.swift +++ b/phoenix-ios/phoenix-ios/views/notifications/BizNotificationCell.swift @@ -145,8 +145,6 @@ struct BizNotificationCell: View { Text("Automated incoming liquidity is disabled in your incoming fee settings.") case .missingOffChainAmountTooLow(_): Text("Missing off-chain amount too low.") - case .channelFundingInProgress(_): - Text("Channel funding in progress.") default: Text("Unknown reason.") } From 85474422294c5b2ca657bcf57281582d42fcd6ab Mon Sep 17 00:00:00 2001 From: Robbie Hanson <304604+robbiehanson@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:29:54 -0500 Subject: [PATCH 19/25] (ios) WIP: Updating UI --- phoenix-ios/phoenix-ios/Localizable.xcstrings | 19 ++ .../views/inspect/DetailsView.swift | 164 ++++++++++++++---- .../views/inspect/SummaryView.swift | 4 +- 3 files changed, 153 insertions(+), 34 deletions(-) diff --git a/phoenix-ios/phoenix-ios/Localizable.xcstrings b/phoenix-ios/phoenix-ios/Localizable.xcstrings index 4cbf66ce8..bfae8aff1 100644 --- a/phoenix-ios/phoenix-ios/Localizable.xcstrings +++ b/phoenix-ios/phoenix-ios/Localizable.xcstrings @@ -7651,6 +7651,12 @@ }, "Automated liquidity" : { "comment" : "Payment description for inbound liquidity" + }, + "Automatic [FromChannelBalance]" : { + + }, + "Automatic [FromFutureHtlc]" : { + }, "Automatic Channel Creation" : { "extractionState" : "manual", @@ -9146,6 +9152,9 @@ } } } + }, + "Blockchain Info" : { + }, "blockchain tx" : { "extractionState" : "manual", @@ -9748,6 +9757,9 @@ } } } + }, + "caused by" : { + }, "Caused by" : { @@ -25814,6 +25826,9 @@ } } } + }, + "Manual" : { + }, "Manual Backup" : { "comment" : "Navigation bar title", @@ -26995,6 +27010,7 @@ } }, "miner fees" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -32614,6 +32630,9 @@ } } } + }, + "purchase type" : { + }, "QR code" : { "localizations" : { diff --git a/phoenix-ios/phoenix-ios/views/inspect/DetailsView.swift b/phoenix-ios/phoenix-ios/views/inspect/DetailsView.swift index ead1948b5..2d4d41195 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/DetailsView.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/DetailsView.swift @@ -12,10 +12,13 @@ struct DetailsView: View { let location: PaymentView.Location @Binding var paymentInfo: WalletPaymentInfo + @Binding var liquidityPayment: Lightning_kmpInboundLiquidityOutgoingPayment? @Binding var showOriginalFiatValue: Bool @Binding var showFiatValueExplanation: Bool + let switchToPayment: (_ paymentId: WalletPaymentId) -> Void + @Environment(\.presentationMode) var presentationMode: Binding @ViewBuilder @@ -48,8 +51,10 @@ struct DetailsView: View { DetailsInfoGrid( paymentInfo: $paymentInfo, + liquidityPayment: $liquidityPayment, showOriginalFiatValue: $showOriginalFiatValue, - showFiatValueExplanation: $showFiatValueExplanation + showFiatValueExplanation: $showFiatValueExplanation, + switchToPayment: switchToPayment ) } .background(Color.primaryBackground) @@ -91,9 +96,13 @@ struct DetailsView: View { fileprivate struct DetailsInfoGrid: InfoGridView { @Binding var paymentInfo: WalletPaymentInfo + @Binding var liquidityPayment: Lightning_kmpInboundLiquidityOutgoingPayment? + @Binding var showOriginalFiatValue: Bool @Binding var showFiatValueExplanation: Bool + let switchToPayment: (_ paymentId: WalletPaymentId) -> Void + @State var showBlockchainExplorerOptions = false @State var truncatedText: [String: Bool] = [:] @@ -119,6 +128,8 @@ fileprivate struct DetailsInfoGrid: InfoGridView { } // + @Environment(\.presentationMode) var presentationMode: Binding + @EnvironmentObject var currencyPrefs: CurrencyPrefs // -------------------------------------------------- @@ -177,6 +188,7 @@ fileprivate struct DetailsInfoGrid: InfoGridView { common_amountReceived(msat: received.amount) payment_standardFees(incomingPayment) payment_minerFees(incomingPayment) + payment_serviceFees(incomingPayment) } let receivedWithArray = received.receivedWith.sorted { $0.hash < $1.hash } @@ -268,7 +280,7 @@ fileprivate struct DetailsInfoGrid: InfoGridView { onChain_broadcastAt(spliceOut) onChain_confirmedAt(spliceOut) common_amountSent(msat: outgoingPayment.amount) - onChain_minerFees(spliceOut) + payment_minerFees(spliceOut) common_amountReceived(sat: spliceOut.recipientAmount) onChain_btcTxid(spliceOut) } @@ -301,7 +313,7 @@ fileprivate struct DetailsInfoGrid: InfoGridView { } content: { onChain_broadcastAt(spliceCpfp) onChain_confirmedAt(spliceCpfp) - onChain_minerFees(spliceCpfp) + payment_minerFees(spliceCpfp) onChain_btcTxid(spliceCpfp) } @@ -310,12 +322,21 @@ fileprivate struct DetailsInfoGrid: InfoGridView { InlineSection { header("Inbound Liquidity") } content: { + liquidityPayment_purchaseType(liquidityPayment) + liquidityPayment_causedBy(liquidityPayment) liquidityPayment_liqudityAmount(liquidityPayment) payment_minerFees(outgoingPayment) payment_serviceFees(outgoingPayment) - liquidityPayment_spliceTxid(liquidityPayment) liquidityPayment_channelId(liquidityPayment) } + + InlineSection { + header("Blockchain Info") + } content: { + onChain_broadcastAt(liquidityPayment) + onChain_confirmedAt(liquidityPayment) + liquidityPayment_spliceTxid(liquidityPayment) + } } } @@ -357,7 +378,7 @@ fileprivate struct DetailsInfoGrid: InfoGridView { } // -------------------------------------------------- - // MARK: View Builders: Rows + // MARK: View Builders: Detailed Rows // -------------------------------------------------- @ViewBuilder @@ -689,7 +710,7 @@ fileprivate struct DetailsInfoGrid: InfoGridView { ) -> some View { let identifier: String = #function - if let standardFees = payment.standardFees(), standardFees.0 > 0 { + if let standardFees = standardFees(), standardFees.0 > 0 { InfoGridRowWrapper( identifier: identifier, @@ -717,7 +738,7 @@ fileprivate struct DetailsInfoGrid: InfoGridView { ) -> some View { let identifier: String = #function - if let minerFees = payment.minerFees(), minerFees.0 > 0 { + if let minerFees = minerFees(), minerFees.0 > 0 { InfoGridRowWrapper( identifier: identifier, @@ -745,7 +766,7 @@ fileprivate struct DetailsInfoGrid: InfoGridView { ) -> some View { let identifier: String = #function - if let serviceFees = payment.serviceFees(), serviceFees.0 > 0 { + if let serviceFees = serviceFees(), serviceFees.0 > 0 { InfoGridRowWrapper( identifier: identifier, @@ -983,30 +1004,6 @@ fileprivate struct DetailsInfoGrid: InfoGridView { } } - @ViewBuilder - func onChain_minerFees( - _ onChain: Lightning_kmpOnChainOutgoingPayment - ) -> some View { - let identifier: String = #function - - InfoGridRowWrapper( - identifier: identifier, - keyColumnWidth: keyColumnWidth(identifier: identifier) - ) { - keyColumn("miner fees") - - } valueColumn: { - - commonValue_amounts( - identifier: identifier, - displayAmounts: displayAmounts( - sat: onChain.miningFees, - originalFiat: paymentInfo.metadata.originalFiat - ) - ) - } - } - @ViewBuilder func onChain_btcTxid( _ onChain: Lightning_kmpOnChainOutgoingPayment @@ -1073,6 +1070,57 @@ fileprivate struct DetailsInfoGrid: InfoGridView { } // } + @ViewBuilder + func liquidityPayment_purchaseType( + _ payment: Lightning_kmpInboundLiquidityOutgoingPayment + ) -> some View { + let identifier: String = #function + + InfoGridRowWrapper( + identifier: identifier, + keyColumnWidth: keyColumnWidth(identifier: identifier) + ) { + keyColumn("purchase type") + + } valueColumn: { + + if payment.isManualPurchase() { + Text("Manual") + } else if payment.isPaidInTheFuture() { + Text("Automatic [FromFutureHtlc]") + } else { + Text("Automatic [FromChannelBalance]") + } + } + } + + @ViewBuilder + func liquidityPayment_causedBy( + _ payment: Lightning_kmpInboundLiquidityOutgoingPayment + ) -> some View { + let identifier: String = #function + + if let paymentId = payment.relatedPaymentIds().first { + + InfoGridRowWrapper( + identifier: identifier, + keyColumnWidth: keyColumnWidth(identifier: identifier) + ) { + keyColumn("caused by") + + } valueColumn: { + + Button { + requestSwitchToPayment(paymentId) + } label: { + Text(paymentId.dbId) + .lineLimit(1) + .truncationMode(.middle) + } + } + } + } + @ViewBuilder func liquidityPayment_liqudityAmount( _ payment: Lightning_kmpInboundLiquidityOutgoingPayment @@ -1113,6 +1161,10 @@ fileprivate struct DetailsInfoGrid: InfoGridView { common_channelId(payment.channelId) } + // -------------------------------------------------- + // MARK: View Builders: Common Rows + // -------------------------------------------------- + @ViewBuilder func common_amountSent( msat: Lightning_kmpMilliSatoshi @@ -1260,7 +1312,7 @@ fileprivate struct DetailsInfoGrid: InfoGridView { } // -------------------------------------------------- - // MARK: View Builders: Values + // MARK: View Builders: Common Values // -------------------------------------------------- @ViewBuilder @@ -1433,6 +1485,45 @@ fileprivate struct DetailsInfoGrid: InfoGridView { // MARK: View Helpers // -------------------------------------------------- + func standardFees() -> (Int64, String, String)? { + + return paymentInfo.payment.standardFees() + } + + func minerFees() -> (Int64, String, String)? { + + if let liquidity = paymentInfo.payment as? Lightning_kmpInboundLiquidityOutgoingPayment, + liquidity.isPaidInTheFuture() { + // We don't display the fees here. + // Instead we're displaying the fees on the corresponding IncomingPayment. + return nil + } else if let result = paymentInfo.payment.minerFees() { + return result + } else if let liquidityPayment, liquidityPayment.isPaidInTheFuture() { + // This is the corresponding IncomingPayment, and we have the linked liquidityPayment. + return liquidityPayment.minerFees() + } else { + return nil + } + } + + func serviceFees() -> (Int64, String, String)? { + + if let liquidity = paymentInfo.payment as? Lightning_kmpInboundLiquidityOutgoingPayment, + liquidity.isPaidInTheFuture() { + // We don't display the fees here. + // Instead we're displaying the fees on the corresponding IncomingPayment. + return nil + } else if let result = paymentInfo.payment.serviceFees() { + return result + } else if let liquidityPayment, liquidityPayment.isPaidInTheFuture() { + // This is the corresponding IncomingPayment, and we have the linked liquidityPayment. + return liquidityPayment.serviceFees() + } else { + return nil + } + } + func displayTimes(date: Date) -> (String, String) { let df = DateFormatter() @@ -1599,6 +1690,13 @@ fileprivate struct DetailsInfoGrid: InfoGridView { // MARK: Actions // -------------------------------------------------- + func requestSwitchToPayment(_ paymentId: WalletPaymentId) { + log.trace("requestSwitchToPayment()") + + presentationMode.wrappedValue.dismiss() + switchToPayment(paymentId) + } + func exploreTx(_ txId: Bitcoin_kmpTxId, website: BlockchainExplorer.Website) { log.trace("exploreTX()") diff --git a/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift b/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift index 967b2fe5d..03614c68d 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift @@ -851,8 +851,10 @@ struct SummaryView: View { DetailsView( location: wrappedLocation(), paymentInfo: $paymentInfo, + liquidityPayment: $liquidityPayment, showOriginalFiatValue: $showOriginalFiatValue, - showFiatValueExplanation: $showFiatValueExplanation + showFiatValueExplanation: $showFiatValueExplanation, + switchToPayment: switchToPayment ) case .EditInfoView: From 9fc7789c2d8baa73cc49b2b53385e6ee33249a4d Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:36:36 +0200 Subject: [PATCH 20/25] Fix duplicate fee in CSV export --- .../payments/receive/ReceiveViewModel.kt | 2 -- .../fr.acinq.phoenix/utils/CsvWriter.kt | 33 +++++++++++++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveViewModel.kt index f93b03133..860839e52 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveViewModel.kt @@ -131,11 +131,9 @@ class ReceiveViewModel( val startAddress = keyManager.swapInOnChainWallet.getSwapInProtocol(startIndex).address(chain) val image = BitmapHelper.generateBitmap(startAddress).asImageBitmap() currentSwapAddress = BitcoinAddressState.Show(startIndex, startAddress, image) - log.info("starting with swap-in address $startAddress:$startIndex") // monitor the actual address from the swap-in wallet -- might take some time since the wallet must check all previous addresses peerManager.getPeer().phoenixSwapInWallet.swapInAddressFlow.filterNotNull().collect { (newAddress, newIndex) -> - log.info("swap-in wallet current address update: $newAddress:$newIndex") val newImage = BitmapHelper.generateBitmap(newAddress).asImageBitmap() internalDataRepository.saveLastUsedSwapIndex(newIndex) currentSwapAddress = BitcoinAddressState.Show(newIndex, newAddress, newImage) 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 2f0c5a48b..31530ad87 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 @@ -2,6 +2,8 @@ package fr.acinq.phoenix.utils import fr.acinq.lightning.db.* import fr.acinq.lightning.payment.OfferPaymentMetadata +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sum import fr.acinq.phoenix.data.WalletPaymentInfo import fr.acinq.phoenix.utils.extensions.isManualPurchase import kotlinx.datetime.Instant @@ -62,12 +64,28 @@ class CsvWriter { config: Configuration ): String { - val date = info.payment.completedAt ?: info.payment.createdAt + val payment = info.payment + + val date = payment.completedAt ?: info.payment.createdAt val dateStr = Instant.fromEpochMilliseconds(date).toString() // ISO-8601 format var row = processField(dateStr) - val amtMsat = info.payment.amount.msat - val feesMsat = info.payment.fees.msat + val amtMsat = payment.amount.msat + // for the fee, we should ignore the fee returned by lightning parts, because it may contain a funding fee which is already accounted for in the liquidity payments + val feesMsat = when (payment) { + is IncomingPayment -> when (payment.origin) { + is IncomingPayment.Origin.Invoice, is IncomingPayment.Origin.Offer -> { + payment.received?.receivedWith.orEmpty().map { part -> + when (part) { + is IncomingPayment.ReceivedWith.LightningPayment, is IncomingPayment.ReceivedWith.AddedToFeeCredit -> 0.msat + else -> part.fees + } + }.sum().msat + } + else -> payment.fees.msat + } + else -> payment.fees.msat + } val isOutgoing = info.payment is OutgoingPayment val amtMsatStr = if (isOutgoing) "-$amtMsat" else "$amtMsat" @@ -124,14 +142,15 @@ class CsvWriter { is IncomingPayment.Origin.Invoice -> "Incoming LN payment" is IncomingPayment.Origin.SwapIn -> "Swap-in to ${origin.address ?: "N/A"}" is IncomingPayment.Origin.OnChain -> "On-chain deposit" - is IncomingPayment.Origin.Offer -> when (origin.metadata) { - is OfferPaymentMetadata.V1 -> "Incoming payment to your offer" - } + is IncomingPayment.Origin.Offer -> "Incoming LN payment (offer)" } is LightningOutgoingPayment -> when (val details = payment.details) { is LightningOutgoingPayment.Details.Normal -> "Outgoing LN payment to ${details.paymentRequest.nodeId.toHex()}" is LightningOutgoingPayment.Details.SwapOut -> "Outgoing Swap to ${details.address}" - is LightningOutgoingPayment.Details.Blinded -> "Outgoing LN payment to ${details.paymentRequest.invoiceRequest.offer.encode()}" + is LightningOutgoingPayment.Details.Blinded -> { + details.paymentRequest.invoiceRequest.offer.contactNodeIds.firstOrNull()?.let { "Outgoing LN payment (offer) to ${it.toHex()}" } + ?: "Outgoing LN payment (offer)" + } } is SpliceOutgoingPayment -> "Outgoing splice to ${payment.address}" is ChannelCloseOutgoingPayment -> "Channel closing to ${payment.address}" From f03a74625389ff3163e1be39d4679ee4c30b4ce5 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:48:51 +0200 Subject: [PATCH 21/25] (android) Fix isolated business initialization The business variable should be set asap to allow proper cancelling if the worker is cancelled early. --- .../fr/acinq/phoenix/android/services/DailyConnect.kt | 11 +++++------ .../android/services/InflightPaymentsWatcher.kt | 3 ++- .../fr/acinq/phoenix/android/services/WorkerHelper.kt | 5 +---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/DailyConnect.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/DailyConnect.kt index 5a9d7be30..cd26c3117 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/DailyConnect.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/DailyConnect.kt @@ -39,6 +39,7 @@ import fr.acinq.phoenix.android.security.SeedManager import fr.acinq.phoenix.legacy.utils.LegacyAppStatus import fr.acinq.phoenix.legacy.utils.LegacyPrefsDatastore import fr.acinq.phoenix.managers.AppConnectionsDaemon +import fr.acinq.phoenix.utils.PlatformContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -53,14 +54,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.slf4j.LoggerFactory import java.util.concurrent.TimeUnit -import kotlin.time.Duration.Companion.minutes -import kotlin.time.toJavaDuration /** * This worker is scheduled to run roughly every day. It simply connects to the LSP, wait for 1 minute, * then shuts down. The purpose is to settle pending payments that may have been missed by the * [InflightPaymentsWatcher], to complete closings properly, etc... - * */ class DailyConnect(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { @@ -113,6 +111,7 @@ class DailyConnect(context: Context, workerParams: WorkerParameters) : Coroutine var jobWatchingChannels: Job? = null val jobMain = launch { + business = PhoenixBusiness(PlatformContext(applicationContext)) service.filterNotNull().flatMapLatest { it.state.asFlow() }.collect { state -> when (state) { is NodeServiceState.Init, is NodeServiceState.Running, is NodeServiceState.Error, NodeServiceState.Disconnected -> { @@ -126,7 +125,7 @@ class DailyConnect(context: Context, workerParams: WorkerParameters) : Coroutine log.info("node service in state=${state.name}, starting an isolated business") jobWatchingChannels = launch { - business = WorkerHelper.startIsolatedBusiness(application, encryptedSeed, userPrefs) + WorkerHelper.startIsolatedBusiness(application, business!!, encryptedSeed, userPrefs) business?.connectionsManager?.connections?.first { it.global is Connection.ESTABLISHED } log.debug("connections established") @@ -184,8 +183,8 @@ class DailyConnect(context: Context, workerParams: WorkerParameters) : Coroutine } fun scheduleASAP(context: Context) { - log.info("scheduling $name") - val work = OneTimeWorkRequest.Builder(DailyConnect::class.java).setInitialDelay(1.minutes.toJavaDuration()).addTag(TAG).build() + log.info("scheduling $name once") + val work = OneTimeWorkRequest.Builder(DailyConnect::class.java).addTag(TAG).build() WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, work) } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/InflightPaymentsWatcher.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/InflightPaymentsWatcher.kt index 45ab1dace..51b6dc893 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/InflightPaymentsWatcher.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/InflightPaymentsWatcher.kt @@ -134,6 +134,7 @@ class InflightPaymentsWatcher(context: Context, workerParams: WorkerParameters) // Start the monitoring process. If the main app starts, we interrupt this job to prevent concurrent access. withContext(Dispatchers.Default) { + business = PhoenixBusiness(PlatformContext(applicationContext)) val stopJobs = MutableStateFlow(false) var jobChannelsWatcher: Job? = null @@ -152,7 +153,7 @@ class InflightPaymentsWatcher(context: Context, workerParams: WorkerParameters) log.info("node service in state=${state.name}, starting an isolated business") jobChannelsWatcher = launch { - business = WorkerHelper.startIsolatedBusiness(application, encryptedSeed, userPrefs) + WorkerHelper.startIsolatedBusiness(application, business!!, encryptedSeed, userPrefs) business?.connectionsManager?.connections?.first { it.global is Connection.ESTABLISHED } log.debug("connections established, watching channels for in-flight payments...") diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/WorkerHelper.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/WorkerHelper.kt index 05a5251d6..639f767a6 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/WorkerHelper.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/services/WorkerHelper.kt @@ -25,15 +25,13 @@ import fr.acinq.phoenix.data.StartupParams import fr.acinq.phoenix.legacy.utils.LegacyPrefsDatastore import fr.acinq.phoenix.managers.AppConfigurationManager import fr.acinq.phoenix.utils.MnemonicLanguage -import fr.acinq.phoenix.utils.PlatformContext import kotlinx.coroutines.flow.first object WorkerHelper { - suspend fun startIsolatedBusiness(context: Context, encryptedSeed: EncryptedSeed.V2.NoAuth, userPrefs: UserPrefsRepository): PhoenixBusiness { + suspend fun startIsolatedBusiness(context: Context, business: PhoenixBusiness, encryptedSeed: EncryptedSeed.V2.NoAuth, userPrefs: UserPrefsRepository) { val mnemonics = encryptedSeed.decrypt() // retrieve preferences before starting business - val business = PhoenixBusiness(PlatformContext(context)) val electrumServer = userPrefs.getElectrumServer.first() val isTorEnabled = userPrefs.getIsTorEnabled.first() val liquidityPolicy = userPrefs.getLiquidityPolicy.first() @@ -60,6 +58,5 @@ object WorkerHelper { // start the swap-in wallet watcher business.peerManager.getPeer().startWatchSwapInWallet() - return business } } \ No newline at end of file From e0ad073c6a3839930123a84a342809e8b42c0cbd Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:02:41 +0200 Subject: [PATCH 22/25] Use lightning-kmp v1.8.0 --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 8fbdee2dc..c0b0ab3e5 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.7.4-SNAPSHOT" + const val lightningKmp = "1.8.0" const val secp256k1 = "0.14.0" const val torMobile = "0.2.0" From 57a6bc5884758d88227e2aec1d7687d1b71c872a Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:07:31 +0200 Subject: [PATCH 23/25] (android) Resize payment link button in liquidity details --- .../android/payments/details/splash/SplashLiquidityPurchase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt index 50d88e73b..1566b09bc 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashLiquidityPurchase.kt @@ -137,7 +137,7 @@ private fun SplashRelatedPayments(payment: InboundLiquidityOutgoingPayment) { space = 4.dp, shape = RoundedCornerShape(12.dp), backgroundColor = mutedBgColor, - modifier = Modifier.widthIn(max = 170.dp) + modifier = Modifier.widthIn(max = 130.dp) ) } } From 900f8608206b234d4a6c6af4e0d0aa0eb6ed95f4 Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:55:03 +0200 Subject: [PATCH 24/25] (android) Added missing translations for new strings --- .../android/payments/details/splash/PaymentSplashView.kt | 2 +- .../android/payments/history/PaymentsHistoryView.kt | 2 +- .../android/payments/liquidity/RequestLiquidityView.kt | 2 +- .../phoenix/android/payments/spliceout/SpliceOutView.kt | 4 ++-- .../src/main/res/values-b+es+419/important_strings.xml | 2 ++ phoenix-android/src/main/res/values-b+es+419/strings.xml | 1 + .../src/main/res/values-cs/important_strings.xml | 2 ++ phoenix-android/src/main/res/values-cs/strings.xml | 1 + .../src/main/res/values-de/important_strings.xml | 2 ++ phoenix-android/src/main/res/values-de/strings.xml | 1 + .../src/main/res/values-es/important_strings.xml | 2 ++ .../src/main/res/values-fr/important_strings.xml | 8 +++++--- phoenix-android/src/main/res/values-fr/strings.xml | 1 + .../src/main/res/values-pt-rBR/important_strings.xml | 2 ++ phoenix-android/src/main/res/values-pt-rBR/strings.xml | 1 + .../src/main/res/values-sk/important_strings.xml | 2 ++ phoenix-android/src/main/res/values-sk/strings.xml | 1 + .../src/main/res/values-sw/important_strings.xml | 2 ++ .../src/main/res/values-vi/important_strings.xml | 2 ++ phoenix-android/src/main/res/values/important_strings.xml | 2 ++ phoenix-android/src/main/res/values/strings.xml | 3 +++ 21 files changed, 37 insertions(+), 8 deletions(-) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt index d9c0b31be..687df1bde 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/PaymentSplashView.kt @@ -151,7 +151,7 @@ fun SplashDescription( } } } - SplashLabelRow(label = if (userDescription.isNullOrBlank()) "" else "Note") { + SplashLabelRow(label = if (userDescription.isNullOrBlank()) "" else stringResource(id = R.string.paymentdetails_note_label)) { SplashClickableContent(onClick = { showEditDescriptionDialog = true }) { if (!userDescription.isNullOrBlank()) { Text(text = userDescription) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/PaymentsHistoryView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/PaymentsHistoryView.kt index 74d08839c..662f96449 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/PaymentsHistoryView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/history/PaymentsHistoryView.kt @@ -155,7 +155,7 @@ fun PaymentsHistoryView( val hasMorePaymentsToFetch = payments.size < allPaymentsCount if (hasMorePaymentsToFetch) { // Subscribe to a bit more payments. Ideally would be the screen height / height of each payment. - paymentsViewModel.subscribeToPayments(offset = 0, count = index + 16) + paymentsViewModel.subscribeToPayments(offset = 0, count = index + 14) } } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt index c7e679ff1..245b9881b 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/liquidity/RequestLiquidityView.kt @@ -257,7 +257,7 @@ private fun RequestLiquidityBottomSection( is RequestLiquidityState.Error.InvalidFundingAmount -> { ErrorMessage( header = stringResource(id = R.string.liquidityads_error_header), - details = "Invalid amount requested. Please try again." + details = stringResource(id = R.string.liquidityads_error_invalid_funding_amount) ) } is RequestLiquidityState.Error.Thrown -> { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutView.kt index 94004ca59..502634b50 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/spliceout/SpliceOutView.kt @@ -431,6 +431,6 @@ fun spliceFailureDetails(spliceFailure: ChannelFundingResponse.Failure): String is ChannelFundingResponse.Failure.InvalidSpliceOutPubKeyScript -> stringResource(id = R.string.splice_error_invalid_pubkey) is ChannelFundingResponse.Failure.SpliceAlreadyInProgress -> stringResource(id = R.string.splice_error_splice_in_progress) is ChannelFundingResponse.Failure.InvalidLiquidityAds -> stringResource(id = R.string.splice_error_invalid_liquidity_ads, spliceFailure.reason.details()) - is ChannelFundingResponse.Failure.InvalidChannelParameters -> TODO() - is ChannelFundingResponse.Failure.UnexpectedMessage -> TODO() + is ChannelFundingResponse.Failure.InvalidChannelParameters -> stringResource(id = R.string.splice_error_invalid_channel_params, spliceFailure.reason.details()) + is ChannelFundingResponse.Failure.UnexpectedMessage -> stringResource(id = R.string.splice_error_unexpected, spliceFailure.msg.type.toString()) } diff --git a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml index 74b22891f..8287c0c1a 100644 --- a/phoenix-android/src/main/res/values-b+es+419/important_strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/important_strings.xml @@ -246,6 +246,7 @@ + Liquidez Comisiones de minería Comisiones pagadas a los mineros de la red Bitcoin para procesar la transacción en la cadena. Comisiones de servicio @@ -382,6 +383,7 @@ Error al solicitar liquidez Los canales no están disponibles. Vuelve a intentarlo más tarde. + El importe solicitado no es válido. diff --git a/phoenix-android/src/main/res/values-b+es+419/strings.xml b/phoenix-android/src/main/res/values-b+es+419/strings.xml index a04d13546..e7a46989f 100644 --- a/phoenix-android/src/main/res/values-b+es+419/strings.xml +++ b/phoenix-android/src/main/res/values-b+es+419/strings.xml @@ -331,6 +331,7 @@ Desencriptando mensaje… Descripción + Nota Enviado a Mineros de Bitcoin Comisiones diff --git a/phoenix-android/src/main/res/values-cs/important_strings.xml b/phoenix-android/src/main/res/values-cs/important_strings.xml index e6d577e41..d762abf01 100644 --- a/phoenix-android/src/main/res/values-cs/important_strings.xml +++ b/phoenix-android/src/main/res/values-cs/important_strings.xml @@ -248,6 +248,7 @@ + Likvidity Poplatky těžařům Poplatky placené těžařům Bitcoinové sítě za zpracování On-Chain transakce. Poplatky za službu @@ -393,6 +394,7 @@ Žádost o likviditu se nezdařila Kanály nejsou k dispozici. Zkuste to později. + Požadovaná částka je neplatná. diff --git a/phoenix-android/src/main/res/values-cs/strings.xml b/phoenix-android/src/main/res/values-cs/strings.xml index b7042e1c2..caeb09af9 100644 --- a/phoenix-android/src/main/res/values-cs/strings.xml +++ b/phoenix-android/src/main/res/values-cs/strings.xml @@ -315,6 +315,7 @@ Dešifrování zprávy… Popis + Poznámka Odesláno Bitcoinoví těžaři Poplatky diff --git a/phoenix-android/src/main/res/values-de/important_strings.xml b/phoenix-android/src/main/res/values-de/important_strings.xml index af1f54b64..7494866d5 100644 --- a/phoenix-android/src/main/res/values-de/important_strings.xml +++ b/phoenix-android/src/main/res/values-de/important_strings.xml @@ -245,6 +245,7 @@ + Liquidität Miner-Gebühren Gebühren, die an die Miner des Bitcoin-Netzwerks für die Verarbeitung der On-Chain-Transaktion gezahlt werden. Service-Gebühr @@ -391,6 +392,7 @@ Liquiditätsanfrage ist fehlgeschlagen Kanäle sind nicht verfügbar. Versuchen Sie es später erneut. + Der angeforderte Betrag ist ungültig. diff --git a/phoenix-android/src/main/res/values-de/strings.xml b/phoenix-android/src/main/res/values-de/strings.xml index 9b9fb89a8..7ffa72092 100644 --- a/phoenix-android/src/main/res/values-de/strings.xml +++ b/phoenix-android/src/main/res/values-de/strings.xml @@ -338,6 +338,7 @@ Nachricht entschlüsseln.. Beschreibung + Hinweis Gesendet an Bitcoin-Miner Gebühren diff --git a/phoenix-android/src/main/res/values-es/important_strings.xml b/phoenix-android/src/main/res/values-es/important_strings.xml index fc8f138d9..3dbd4a79c 100644 --- a/phoenix-android/src/main/res/values-es/important_strings.xml +++ b/phoenix-android/src/main/res/values-es/important_strings.xml @@ -249,6 +249,7 @@ + Liquidez Tasas mineras Tasas pagadas a los mineros de la red Bitcoin para procesar la transacción en la cadena. Tasas de servicio @@ -394,6 +395,7 @@ Solicitud de liquidez fallida Los canales no están disponibles. Vuelva a intentarlo más tarde. + El importe solicitado no es válido. diff --git a/phoenix-android/src/main/res/values-fr/important_strings.xml b/phoenix-android/src/main/res/values-fr/important_strings.xml index 2750fc855..121c69fc0 100644 --- a/phoenix-android/src/main/res/values-fr/important_strings.xml +++ b/phoenix-android/src/main/res/values-fr/important_strings.xml @@ -45,7 +45,7 @@ Les frais étaient de %1$s et dépassent %2$s%% du montant. Cette configuration peut être changée. Les frais étaient de %1$s et dépassent %2$s%% du montant. Le dépôt expirera le %3$s. Le montant du paiement est trop faible. - Une erreur s\'est produite lors du financement. Veuillez réessayer plus tard. + Une erreur est survenue lors de l\'ajout de fonds. Veuillez réessayer plus tard. Veuillez démarrer Phoenix Certains de vos canaux pourraient avoir fermé. @@ -249,9 +249,10 @@ - Frais de\nminage + Liquidité + Frais de minage Frais payés aux mineurs du réseau Bitcoin pour traiter la transaction on-chain. - Frais de\nservice + Frais de service Frais payés pour la création d\'un nouveau canal de paiement. Cette action n\'est pas toujours nécessaire. Frais de\nservice Frais payés pour le service de liquidité. @@ -394,6 +395,7 @@ La demande de liquidité a échoué Vos canaux ne sont pas disponibles. Réessayez plus tard. + Le montant demandé n\'est pas valide. diff --git a/phoenix-android/src/main/res/values-fr/strings.xml b/phoenix-android/src/main/res/values-fr/strings.xml index 9fae5c7b3..c84f0e7be 100644 --- a/phoenix-android/src/main/res/values-fr/strings.xml +++ b/phoenix-android/src/main/res/values-fr/strings.xml @@ -328,6 +328,7 @@ Déchiffrement du message… Description + Note Envoyé à Mineurs Bitcoin Frais diff --git a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml index 42e914c7c..57163ca31 100644 --- a/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml +++ b/phoenix-android/src/main/res/values-pt-rBR/important_strings.xml @@ -248,6 +248,7 @@ + Liquidez Taxas do minerador Taxas pagas aos mineradores da rede Bitcoin para processar a transação na cadeia. Taxas de serviço @@ -389,6 +390,7 @@ A solicitação de liquidez falhou Os canais não estão disponíveis. Tente novamente mais tarde. + O valor solicitado é inválido. diff --git a/phoenix-android/src/main/res/values-pt-rBR/strings.xml b/phoenix-android/src/main/res/values-pt-rBR/strings.xml index 510d5fdf1..310c62f0d 100644 --- a/phoenix-android/src/main/res/values-pt-rBR/strings.xml +++ b/phoenix-android/src/main/res/values-pt-rBR/strings.xml @@ -328,6 +328,7 @@ Descriptografando mensagem… Descrição + Nota Enviado para Mineradores de Bitcoin Taxas diff --git a/phoenix-android/src/main/res/values-sk/important_strings.xml b/phoenix-android/src/main/res/values-sk/important_strings.xml index bceba38c0..eb7505d1a 100644 --- a/phoenix-android/src/main/res/values-sk/important_strings.xml +++ b/phoenix-android/src/main/res/values-sk/important_strings.xml @@ -248,6 +248,7 @@ + Likvidity Poplatky ťažiarom Poplatky platené ťažiarom Bitcoinovej siete za spracovanie on-chain transakcie. Poplatky za službu @@ -394,6 +395,7 @@ Žiadosť o likviditu zlyhala Kanály nie sú k dispozícii. Skúste to neskôr. + Požadovaná suma je neplatná. diff --git a/phoenix-android/src/main/res/values-sk/strings.xml b/phoenix-android/src/main/res/values-sk/strings.xml index 3e0b3ce76..fc10da61e 100644 --- a/phoenix-android/src/main/res/values-sk/strings.xml +++ b/phoenix-android/src/main/res/values-sk/strings.xml @@ -358,6 +358,7 @@ Dešifrovanie správy… Popis + Poznámka Poslané Bitcoinoví ťažiari Poplatky diff --git a/phoenix-android/src/main/res/values-sw/important_strings.xml b/phoenix-android/src/main/res/values-sw/important_strings.xml index 341984669..3c6764456 100644 --- a/phoenix-android/src/main/res/values-sw/important_strings.xml +++ b/phoenix-android/src/main/res/values-sw/important_strings.xml @@ -251,6 +251,7 @@ + Ukwasi Ada za wachimba madini Ada zilizolipwa kwa wachimba madini wa mtandao wa Bitcoin kushughulikia muamala wa mtandaoni. Ada za huduma @@ -397,6 +398,7 @@ Ombi la ukwasi limeshindwa Njia hazipatikani. Jaribu tena baadaye. + Kiasi kilichoombwa ni batili. diff --git a/phoenix-android/src/main/res/values-vi/important_strings.xml b/phoenix-android/src/main/res/values-vi/important_strings.xml index 78daeb64c..0395967c5 100644 --- a/phoenix-android/src/main/res/values-vi/important_strings.xml +++ b/phoenix-android/src/main/res/values-vi/important_strings.xml @@ -255,6 +255,7 @@ + Thanh khoản Các khoản phí đào Phí thanh toán cho mạng lưới thợ đào Bitcoin để xử lý giao dịch on-chain. Phí dịch vụ @@ -401,6 +402,7 @@ Yêu cầu thanh khoản không thành công Các kênh đang bận. Vui lòng thử lại sau. + Số tiền được yêu cầu không hợp lệ. diff --git a/phoenix-android/src/main/res/values/important_strings.xml b/phoenix-android/src/main/res/values/important_strings.xml index f311449a6..800d87a01 100644 --- a/phoenix-android/src/main/res/values/important_strings.xml +++ b/phoenix-android/src/main/res/values/important_strings.xml @@ -256,6 +256,7 @@ Service fees Fees paid for the creation of a new payment channel. This is not always required. + Liquidity Service fees Fees paid for the liquidity service. Miner fees @@ -398,6 +399,7 @@ Liquidity request has failed Channels are not available. Try again later. + The requested amount is invalid. diff --git a/phoenix-android/src/main/res/values/strings.xml b/phoenix-android/src/main/res/values/strings.xml index f9a3e800d..de170940c 100644 --- a/phoenix-android/src/main/res/values/strings.xml +++ b/phoenix-android/src/main/res/values/strings.xml @@ -380,6 +380,7 @@ Decrypting message… Description + Note Sent to Bitcoin miners Fees @@ -821,6 +822,8 @@ Invalid splice-out pubkey script A splice payment is already in progress Invalid liquidity-ads request: [%1$s] + Invalid channel parameters: [%1$s] + Unexpected error: [%1$s] From 99cf2c7f921df5fcead4f97ad66dcd06b5e85ada Mon Sep 17 00:00:00 2001 From: Dominique Padiou <5765435+dpad85@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:50:42 +0200 Subject: [PATCH 25/25] Fix tests --- .../phoenix/utils/LegacyMigrationHelperTest.kt | 2 +- .../kotlin/fr/acinq/phoenix/utils/LnurlAuthTest.kt | 4 ++-- .../fr/acinq/phoenix/data/lnurl/LnurlAuthTest.kt | 2 +- .../acinq/phoenix/db/SqlitePaymentsDatabaseTest.kt | 2 +- .../fr/acinq/phoenix/utils/CsvWriterTests.kt | 8 ++++---- .../kotlin/fr/acinq/phoenix/utils/ParserTest.kt | 14 +++++++------- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/phoenix-android/src/test/kotlin/fr/acinq/phoenix/utils/LegacyMigrationHelperTest.kt b/phoenix-android/src/test/kotlin/fr/acinq/phoenix/utils/LegacyMigrationHelperTest.kt index b9824914c..10773f037 100644 --- a/phoenix-android/src/test/kotlin/fr/acinq/phoenix/utils/LegacyMigrationHelperTest.kt +++ b/phoenix-android/src/test/kotlin/fr/acinq/phoenix/utils/LegacyMigrationHelperTest.kt @@ -91,7 +91,7 @@ class LegacyMigrationHelperTest { // transform legacy payments to modern OutgoingPayment objects val newOutgoingPayments = legacyOutgoingPayments.map { LegacyMigrationHelper.modernizeLegacyOutgoingPayment( - chain = Chain.Testnet, + chain = Chain.Testnet3, parentId = it.key, listOfParts = it.value, paymentMeta = paymentMetaRepository.get(it.key.toString()) diff --git a/phoenix-android/src/test/kotlin/fr/acinq/phoenix/utils/LnurlAuthTest.kt b/phoenix-android/src/test/kotlin/fr/acinq/phoenix/utils/LnurlAuthTest.kt index 921f0f007..7e3ca418b 100644 --- a/phoenix-android/src/test/kotlin/fr/acinq/phoenix/utils/LnurlAuthTest.kt +++ b/phoenix-android/src/test/kotlin/fr/acinq/phoenix/utils/LnurlAuthTest.kt @@ -26,7 +26,7 @@ class LnurlAuthTest { val legacyKeyManager = fr.acinq.eclair.crypto.LocalKeyManager(seed, Block.TestnetGenesisBlock().hash()) val kmpKeyManager = LocalKeyManager( seed = seed.toArray().byteVector64(), - chain = Chain.Testnet, + chain = Chain.Testnet3, remoteSwapInExtendedPublicKey = "tpubDDt5vQap1awkyDXx1z1cP7QFKSZHDCCpbU8nSq9jy7X2grTjUVZDePexf6gc6AHtRRzkgfPW87K6EKUVV6t3Hu2hg7YkHkmMeLSfrP85x41" ) @@ -65,7 +65,7 @@ class LnurlAuthTest { val legacyKeyManager = fr.acinq.eclair.crypto.LocalKeyManager(seed, Block.LivenetGenesisBlock().hash()) val kmpKeyManager = LocalKeyManager( seed = seed.toArray().byteVector64(), - chain = Chain.Testnet, + chain = Chain.Testnet3, remoteSwapInExtendedPublicKey = "tpubDDt5vQap1awkyDXx1z1cP7QFKSZHDCCpbU8nSq9jy7X2grTjUVZDePexf6gc6AHtRRzkgfPW87K6EKUVV6t3Hu2hg7YkHkmMeLSfrP85x41" ) diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/data/lnurl/LnurlAuthTest.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/data/lnurl/LnurlAuthTest.kt index d29c6864c..8c621305a 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/data/lnurl/LnurlAuthTest.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/data/lnurl/LnurlAuthTest.kt @@ -27,7 +27,7 @@ import kotlin.test.assertEquals class LnurlAuthTest { private val mnemonics = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" private val seed = MnemonicCode.toSeed(mnemonics, passphrase = "").toByteVector() - private val keyManager = LocalKeyManager(seed, Chain.Testnet, remoteSwapInExtendedPublicKey = "tpubDDt5vQap1awkyDXx1z1cP7QFKSZHDCCpbU8nSq9jy7X2grTjUVZDePexf6gc6AHtRRzkgfPW87K6EKUVV6t3Hu2hg7YkHkmMeLSfrP85x41") + private val keyManager = LocalKeyManager(seed, Chain.Testnet3, remoteSwapInExtendedPublicKey = "tpubDDt5vQap1awkyDXx1z1cP7QFKSZHDCCpbU8nSq9jy7X2grTjUVZDePexf6gc6AHtRRzkgfPW87K6EKUVV6t3Hu2hg7YkHkmMeLSfrP85x41") @Test fun specs_test_vectors() { diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/SqlitePaymentsDatabaseTest.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/SqlitePaymentsDatabaseTest.kt index 37d2dc85d..6e468fcc7 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/SqlitePaymentsDatabaseTest.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/db/SqlitePaymentsDatabaseTest.kt @@ -151,7 +151,7 @@ class SqlitePaymentsDatabaseTest { fun incoming__purge_expired() = runTest { val expiredPreimage = randomBytes32() val expiredInvoice = Bolt11Invoice.create( - chain = Chain.Testnet, + chain = Chain.Testnet3, amount = 150_000.msat, paymentHash = Crypto.sha256(expiredPreimage).toByteVector32(), privateKey = Lightning.randomKey(), diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/CsvWriterTests.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/CsvWriterTests.kt index 27bcdb78d..46d3491a1 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/CsvWriterTests.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/CsvWriterTests.kt @@ -94,7 +94,7 @@ class CsvWriterTests { userNotes = null ) - val expected = "2023-02-01T16:54:44.965Z,2173929,-2000,0.4999 USD,-0.0004 USD,Incoming LN payment,Cafécito,\r\n" + val expected = "2023-02-01T16:54:44.965Z,2173929,0,0.4999 USD,0.0000 USD,Incoming LN payment,Cafécito,\r\n" val actual = CsvWriter.makeRow( info = WalletPaymentInfo(payment, metadata, null, WalletPaymentFetchOptions.All), localizedDescription = "Cafécito", @@ -258,7 +258,7 @@ class CsvWriterTests { userNotes = "Via dual-funding flow" ) - val expected = "2023-02-01T17:14:43.668Z,12000000,-3000000,2.7599 USD,-0.6899 USD,Swap-in with inputs: [${input.txid}],L1 Top-up,Via dual-funding flow\r\n" + val expected = "2023-02-01T17:14:43.668Z,12000000,-3000000,2.7599 USD,-0.6899 USD,On-chain deposit,L1 Top-up,Via dual-funding flow\r\n" val actual = CsvWriter.makeRow( info = WalletPaymentInfo(payment, metadata, null, WalletPaymentFetchOptions.All), localizedDescription = "L1 Top-up", @@ -293,7 +293,7 @@ class CsvWriterTests { userNotes = null ) - val expected = "2023-02-01T22:16:54.498Z,-12820000,-2820000,-3.0366 USD,-0.6679 USD,Swap-out to tb1qlywh0dk40k87gqphpfs8kghd96hmnvus7r8hhf,Swap for cash,\r\n" + val expected = "2023-02-01T22:16:54.498Z,-12820000,-2820000,-3.0366 USD,-0.6679 USD,Outgoing Swap to tb1qlywh0dk40k87gqphpfs8kghd96hmnvus7r8hhf,Swap for cash,\r\n" val actual = CsvWriter.makeRow( info = WalletPaymentInfo(payment, metadata, null, WalletPaymentFetchOptions.All), localizedDescription = "Swap for cash", @@ -339,7 +339,7 @@ class CsvWriterTests { */ private fun makePaymentRequest() = Bolt11Invoice.create( - chain = Chain.Testnet, + chain = Chain.Testnet3, amount = 10_000.msat, paymentHash = randomBytes32(), privateKey = PrivateKey(value = randomBytes32()), diff --git a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt index 8c8a96a3c..7cd194de2 100644 --- a/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt +++ b/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/ParserTest.kt @@ -41,18 +41,18 @@ class ParserTest { assertIs>(Parser.parseBip21Uri(Chain.Mainnet, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) assertIs>(Parser.parseBip21Uri(Chain.Mainnet, "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) - assertIs>(Parser.parseBip21Uri(Chain.Testnet, "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn")) - assertIs>(Parser.parseBip21Uri(Chain.Testnet, "2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc")) - assertIs>(Parser.parseBip21Uri(Chain.Testnet, "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx")) - assertIs>(Parser.parseBip21Uri(Chain.Testnet, "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7")) - assertIs>(Parser.parseBip21Uri(Chain.Testnet, "tb1p607g5ea77m370pey3y5rg58fz7542hnpg40rs2cqw6w69yt5lf2qlktj2a")) + assertIs>(Parser.parseBip21Uri(Chain.Testnet3, "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn")) + assertIs>(Parser.parseBip21Uri(Chain.Testnet3, "2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc")) + assertIs>(Parser.parseBip21Uri(Chain.Testnet3, "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx")) + assertIs>(Parser.parseBip21Uri(Chain.Testnet3, "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7")) + assertIs>(Parser.parseBip21Uri(Chain.Testnet3, "tb1p607g5ea77m370pey3y5rg58fz7542hnpg40rs2cqw6w69yt5lf2qlktj2a")) } @Test fun parse_bitcoin_uri_chain_mismatch() { assertEquals( expected = Either.Left(BitcoinUriError.InvalidScript(error = BitcoinError.ChainHashMismatch)), - actual = Parser.parseBip21Uri(Chain.Testnet, "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") + actual = Parser.parseBip21Uri(Chain.Testnet3, "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") ) assertEquals( expected = Either.Left(BitcoinUriError.InvalidScript(error = BitcoinError.ChainHashMismatch)), @@ -77,7 +77,7 @@ class ParserTest { ) assertIs>( Parser.parseBip21Uri( - Chain.Testnet, + Chain.Testnet3, "bitcoin:?lno=lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqsespexwyy4tcadvgg89l9aljus6709kx235hhqrk6n8dey98uyuftzdqrt2gkjvf2rj2vnt7m7chnmazen8wpur2h65ttgftkqaugy6ql9dcsyq39xc2g084xfn0s50zlh2ex22vvaqxqz3vmudklz453nns4d0624sqr8ux4p5usm22qevld4ydfck7hwgcg9wc3f78y7jqhc6hwdq7e9dwkhty3svq5ju4dptxtldjumlxh5lw48jsz6pnagtwrmeus7uq9rc5g6uddwcwldpklxexvlezld8egntua4gsqqy8auz966nksacdac8yv3maq6elp" ) )