diff --git a/build.gradle.kts b/build.gradle.kts index 5389a86ff..410e1f383 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ allprojects { google() mavenCentral() maven("https://oss.sonatype.org/content/repositories/snapshots") + maven("https://central.sonatype.com/repository/maven-snapshots") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c91acfa40..3f5a69f4f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,12 @@ [versions] -lightningkmp = "1.10.8" -secp256k1 = "0.19.0" # keep in check with lightning-kmp secp version +lightningkmp = "1.11.0" +secp256k1 = "0.21.0" # keep in check with lightning-kmp secp version kotlin = "2.2.10" ktor = "3.1.0" sqldelight = "2.1.0" okio = "3.15.0" +serialization = "1.9.0" # keep in check with lightning-kmp serialization version # iOS skie = "0.10.6" diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/CalendarView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/CalendarView.kt index 08ad21c0f..08c0c7bb1 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/CalendarView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/components/CalendarView.kt @@ -35,11 +35,13 @@ import fr.acinq.phoenix.android.components.dialogs.Dialog import fr.acinq.phoenix.android.utils.converters.DateFormatter.toAbsoluteDateString import fr.acinq.phoenix.android.utils.mutedBgColor import kotlinx.datetime.* +import kotlin.time.ExperimentalTime /** * Calendar component to pick a day. [onDateSelected] returns the timestamp in millis at the * **start** of day. */ +@OptIn(ExperimentalTime::class) @Composable fun CalendarView( label: String, diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/navigation/NavGraphSettingsChannels.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/navigation/NavGraphSettingsChannels.kt index 3ac878af5..cd16fb113 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/navigation/NavGraphSettingsChannels.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/navigation/NavGraphSettingsChannels.kt @@ -24,7 +24,6 @@ import fr.acinq.phoenix.android.AppViewModel import fr.acinq.phoenix.android.settings.MutualCloseView import fr.acinq.phoenix.android.settings.channels.ChannelDetailsView import fr.acinq.phoenix.android.settings.channels.ChannelsView -import fr.acinq.phoenix.android.settings.channels.ImportChannelsData import fr.acinq.phoenix.android.settings.channels.SpendFromChannelAddress fun NavGraphBuilder.channelsNavGraph(navController: NavController, appViewModel: AppViewModel) { @@ -56,12 +55,8 @@ fun NavGraphBuilder.channelsNavGraph(navController: NavController, appViewModel: ChannelDetailsView(business = business, onBackClick = { navController.popBackStack() }, channelId = channelId) } - businessComposable(Screen.BusinessNavGraph.ImportChannelsData.route, appViewModel) { _, _, business -> - ImportChannelsData(business = business, onBackClick = { navController.popBackStack() }) - } - businessComposable(Screen.BusinessNavGraph.SpendChannelAddress.route, appViewModel) { _, _, business -> - SpendFromChannelAddress(business = business, onBackClick = { navController.popBackStack() }) + // SpendFromChannelAddress(business = business, onBackClick = { navController.popBackStack() }) } businessComposable(Screen.BusinessNavGraph.MutualClose.route, appViewModel) { _, walletId, business -> 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 597edd5cb..a6855c63f 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 @@ -31,8 +31,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentTechnicalView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentTechnicalView.kt index 16c46bde3..e226e9ca3 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentTechnicalView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/PaymentTechnicalView.kt @@ -160,7 +160,7 @@ fun Bolt11InvoiceSection( TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_invoice_requested_label), amount = it, rateThen = originalFiatRate) } (invoice.description ?: invoice.descriptionHash?.toHex())?.takeIf { it.isNotBlank() }?.let { - TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_bolt11_description_label), value = it) + TechnicalRowWithCopy(label = stringResource(id = R.string.paymentdetails_bolt11_description_label), value = it) } TechnicalRowWithCopy(label = stringResource(id = R.string.paymentdetails_payment_hash_label), value = invoice.paymentHash.toHex()) preimage?.let { TechnicalRowWithCopy(label = stringResource(id = R.string.paymentdetails_preimage_label), value = preimage.toHex(), helpMessage = stringResource(id = R.string.paymentdetails_preimage_help)) } @@ -178,13 +178,13 @@ fun Bolt12InvoiceSection( TechnicalRowAmount(label = stringResource(id = R.string.paymentdetails_invoice_requested_label), amount = it, rateThen = originalFiatRate) } invoice.description?.takeIf { it.isNotBlank() }?.let { - TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_bolt11_description_label), value = it) + TechnicalRowWithCopy(label = stringResource(id = R.string.paymentdetails_bolt11_description_label), value = it) } TechnicalRow(label = stringResource(id = R.string.paymentdetails_payerkey_label)) { Text(text = payerKey.toHex()) val nodeParamsManager = LocalBusiness.current?.nodeParamsManager val offerPayerKey by produceState(initialValue = null, key1 = nodeParamsManager) { - value = nodeParamsManager?.defaultOffer()?.payerKey + value = nodeParamsManager?.defaultOffer()?.privateKey } if (offerPayerKey != null && payerKey == offerPayerKey) { Spacer(modifier = Modifier.heightIn(4.dp)) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncomingBolt12.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncomingBolt12.kt index cdc2bf2b0..4481bc33b 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncomingBolt12.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/splash/SplashIncomingBolt12.kt @@ -33,7 +33,6 @@ import fr.acinq.bitcoin.PublicKey import fr.acinq.lightning.db.Bolt12IncomingPayment import fr.acinq.lightning.utils.UUID import fr.acinq.phoenix.PhoenixBusiness -import fr.acinq.phoenix.android.LocalBusiness import fr.acinq.phoenix.android.R import fr.acinq.phoenix.android.components.layouts.SplashLabelRow import fr.acinq.phoenix.android.components.contact.ContactCompactView diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/technical/TechnicalIncomingBolt12.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/technical/TechnicalIncomingBolt12.kt index 9e7e21e89..702b331f6 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/technical/TechnicalIncomingBolt12.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/details/technical/TechnicalIncomingBolt12.kt @@ -30,6 +30,7 @@ import fr.acinq.phoenix.android.payments.details.TechnicalRowWithCopy import fr.acinq.phoenix.android.payments.details.TimestampSection import fr.acinq.phoenix.android.utils.converters.MSatDisplayPolicy import fr.acinq.phoenix.data.ExchangeRate +import fr.acinq.phoenix.utils.extensions.description @Composable fun TechnicalIncomingBolt12( @@ -75,10 +76,13 @@ fun IncomingBolt12Details( amount = metadata.amount, rateThen = originalFiatRate ) + metadata.description?.let { description -> + TechnicalRowWithCopy(label = stringResource(id = R.string.paymentdetails_bolt12_description_label), value = description) + } TechnicalRowWithCopy(label = stringResource(id = R.string.paymentdetails_payment_hash_label), value = metadata.paymentHash.toHex()) TechnicalRowWithCopy(label = stringResource(id = R.string.paymentdetails_preimage_label), value = metadata.preimage.toHex()) TechnicalRowWithCopy(label = stringResource(id = R.string.paymentdetails_offer_metadata_label), value = metadata.encode().toHex()) - if (metadata is OfferPaymentMetadata.V1) { - TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_payerkey_label), value = metadata.payerKey.toHex()) + metadata.payerKey?.let { payerKey -> + TechnicalRowSelectable(label = stringResource(id = R.string.paymentdetails_payerkey_label), value = payerKey.toHex()) } } \ No newline at end of file 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 bd414c943..9d15df4f3 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 @@ -52,15 +52,18 @@ import fr.acinq.phoenix.android.utils.logger import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map -import kotlinx.datetime.Instant +import kotlin.time.Instant import kotlinx.datetime.Month import kotlinx.datetime.TimeZone import kotlinx.datetime.daysUntil import kotlinx.datetime.toInstant +import kotlinx.datetime.toJavaDayOfWeek +import kotlinx.datetime.toJavaMonth import kotlinx.datetime.toKotlinLocalDateTime import kotlinx.datetime.toLocalDateTime import java.time.format.TextStyle import java.util.Locale +import kotlin.time.ExperimentalTime private sealed class PaymentsGroup { @@ -94,6 +97,7 @@ private sealed class PaymentsGroup { } } +@OptIn(ExperimentalTime::class) @Composable fun PaymentsHistoryView( onBackClick: () -> Unit, @@ -111,7 +115,7 @@ fun PaymentsHistoryView( val groupedPayments = remember(payments) { val timezone = TimeZone.currentSystemDefault() - val (todaysDayOfWeek, today) = java.time.LocalDate.now().atTime(23, 59, 59).toKotlinLocalDateTime().let { it.dayOfWeek to it.toInstant(timezone) } + val (todaysDayOfWeek, today) = java.time.LocalDate.now().atTime(23, 59, 59).toKotlinLocalDateTime().let { it.dayOfWeek.toJavaDayOfWeek() to it.toInstant(timezone) } payments.values.groupBy { val paymentInstant = Instant.fromEpochMilliseconds(it.payment.createdAt) val daysElapsed = paymentInstant.daysUntil(today, timezone) @@ -189,7 +193,7 @@ fun PaymentsHistoryView( PaymentsGroup.Yesterday -> stringResource(id = R.string.payments_history_yesterday) PaymentsGroup.ThisWeek -> stringResource(id = R.string.payments_history_thisweek) PaymentsGroup.LastWeek -> stringResource(id = R.string.payments_history_lastweek) - is PaymentsGroup.Other -> "${header.month.getDisplayName(TextStyle.FULL, Locale.getDefault()).uppercase()} ${header.year}" + is PaymentsGroup.Other -> "${header.month.toJavaMonth().getDisplayName(TextStyle.FULL, Locale.getDefault()).uppercase()} ${header.year}" }, ) Spacer(modifier = Modifier.height(8.dp)) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveLightningView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveLightningView.kt index 495caf2d8..7bb6a7558 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveLightningView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/receive/ReceiveLightningView.kt @@ -383,7 +383,7 @@ private fun EditInvoiceView( onTextChange = onDescriptionChange, staticLabel = stringResource(id = R.string.receive_lightning_edit_desc_label), placeholder = { Text(text = stringResource(id = R.string.receive_lightning_edit_desc_placeholder), maxLines = 2, overflow = TextOverflow.Ellipsis) }, - maxChars = 140, + maxChars = if (isReusable) 64 else 140, minLines = 2, maxLines = Int.MAX_VALUE, modifier = Modifier.fillMaxWidth(), 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 c3ebbcc18..01142b431 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 @@ -112,7 +112,7 @@ class ReceiveViewModel( if (isReusable) { val nodeParams = nodeParamsManager.nodeParams.filterNotNull().first() - val bolt12Offer = nodeParams.randomOffer(trampolineNodeId = NodeParamsManager.trampolineNodeId, amount = amount, description = description).first + val bolt12Offer = nodeParams.randomOffer(trampolineNodeId = NodeParamsManager.trampolineNodeId, amount = amount, description = description).offer lightningQRBitmap = QRCodeHelper.generateBitmap(bolt12Offer.encode()).asImageBitmap() log.debug("generated new bolt12 offer=${bolt12Offer.encode()}") lightningInvoiceState = LightningInvoiceState.Done.Bolt12(bolt12Offer) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/send/bolt11/SendToBolt11.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/send/bolt11/SendToBolt11.kt index 37d213126..98ac0e9ca 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/send/bolt11/SendToBolt11.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/send/bolt11/SendToBolt11.kt @@ -47,7 +47,6 @@ import fr.acinq.phoenix.android.components.layouts.SplashLabelRow import fr.acinq.phoenix.android.components.layouts.SplashLayout import fr.acinq.phoenix.android.utils.converters.AmountFormatter.toPrettyString import fr.acinq.phoenix.android.utils.extensions.safeLet -import fr.acinq.phoenix.utils.extensions.isAmountlessTrampoline import kotlinx.coroutines.launch @Composable @@ -149,12 +148,6 @@ fun SendToBolt11View( Text(text = invoice.nodeId.toHex(), maxLines = 2, overflow = TextOverflow.MiddleEllipsis) } } - if (invoice.isAmountlessTrampoline()) { - Spacer(modifier = Modifier.height(16.dp)) - SplashLabelRow(label = "", helpMessage = stringResource(id = R.string.send_trampoline_amountless_warning_details)) { - Text(text = stringResource(id = R.string.send_trampoline_amountless_warning_label)) - } - } Spacer(modifier = Modifier.height(16.dp)) SplashLabelRow(label = stringResource(id = R.string.send_trampoline_fee_label)) { val amt = amount diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/send/offer/SendOfferViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/send/offer/SendOfferViewModel.kt index e2e2657e2..018dd677a 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/send/offer/SendOfferViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/payments/send/offer/SendOfferViewModel.kt @@ -76,7 +76,7 @@ class SendOfferViewModel( val useRandomKey = contact == null || !contact.useOfferKey val payerKey = when (useRandomKey) { true -> Lightning.randomKey() - false -> nodeParamsManager.defaultOffer().payerKey + false -> nodeParamsManager.defaultOffer().privateKey } val peer = peerManager.getPeer() val payerNote = message.takeIf { it.isNotBlank() } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/SeedManager.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/SeedManager.kt index 9a2acf469..5ec4b8536 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/SeedManager.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/security/SeedManager.kt @@ -55,7 +55,7 @@ object SeedManager { val encryptedSeed = try { loadEncryptedSeedFromDisk(context) } catch (e: Exception) { - log.error("could read seed file: ", e) + log.error("could not read seed file: ", e) return DecryptSeedResult.Failure.SeedFileUnreadable } 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 08d458d1c..85a541328 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 @@ -82,22 +82,19 @@ fun ChannelsView( var showAdvancedMenuPopIn by remember { mutableStateOf(false) } Text(text = stringResource(id = R.string.channelsview_title)) Spacer(modifier = Modifier.weight(1f)) - Box(contentAlignment = Alignment.TopEnd) { - DropdownMenu(expanded = showAdvancedMenuPopIn, onDismissRequest = { showAdvancedMenuPopIn = false }) { - DropdownMenuItem(onClick = onImportChannelsDataClick, contentPadding = PaddingValues(horizontal = 12.dp)) { - Text(text = stringResource(R.string.channelsview_menu_import_channels), style = MaterialTheme.typography.body1) - } - DropdownMenuItem(onClick = onSpendFromChannelBalance, contentPadding = PaddingValues(horizontal = 12.dp)) { - Text(text = stringResource(R.string.channelsview_menu_spend_channel_balance), style = MaterialTheme.typography.body1) - } - } - Button( - icon = R.drawable.ic_menu_dots, - iconTint = MaterialTheme.colors.onSurface, - padding = PaddingValues(12.dp), - onClick = { showAdvancedMenuPopIn = true } - ) - } +// Box(contentAlignment = Alignment.TopEnd) { +// DropdownMenu(expanded = showAdvancedMenuPopIn, onDismissRequest = { showAdvancedMenuPopIn = false }) { +// DropdownMenuItem(onClick = onSpendFromChannelBalance, contentPadding = PaddingValues(horizontal = 12.dp)) { +// Text(text = stringResource(R.string.channelsview_menu_spend_channel_balance), style = MaterialTheme.typography.body1) +// } +// } +// Button( +// icon = R.drawable.ic_menu_dots, +// iconTint = MaterialTheme.colors.onSurface, +// padding = PaddingValues(12.dp), +// onClick = { showAdvancedMenuPopIn = true } +// ) +// } } ) if (!channelsState?.values?.filter { it.isUsable }.isNullOrEmpty()) { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ImportChannelsData.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ImportChannelsData.kt deleted file mode 100644 index a96f76d57..000000000 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ImportChannelsData.kt +++ /dev/null @@ -1,138 +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.settings.channels - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -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.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.remember -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.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.viewmodel.compose.viewModel -import fr.acinq.phoenix.PhoenixBusiness -import fr.acinq.phoenix.android.R -import fr.acinq.phoenix.android.components.* -import fr.acinq.phoenix.android.components.buttons.Button -import fr.acinq.phoenix.android.components.TextWithIcon -import fr.acinq.phoenix.android.components.dialogs.Dialog -import fr.acinq.phoenix.android.components.feedback.ErrorMessage -import fr.acinq.phoenix.android.components.inputs.TextInput -import fr.acinq.phoenix.android.components.layouts.Card -import fr.acinq.phoenix.android.components.layouts.DefaultScreenHeader -import fr.acinq.phoenix.android.components.layouts.DefaultScreenLayout -import fr.acinq.phoenix.android.utils.positiveColor -import fr.acinq.phoenix.utils.channels.ChannelsImportResult - -@Composable -fun ImportChannelsData( - business: PhoenixBusiness, - onBackClick: () -> Unit, -) { - val peerManager = business.peerManager - val nodeParamsManager = business.nodeParamsManager - val vm = viewModel(factory = ImportChannelsDataViewModel.Factory(peerManager, nodeParamsManager)) - - var dataInput by remember { mutableStateOf("") } - - DefaultScreenLayout { - DefaultScreenHeader(onBackClick = onBackClick, title = stringResource(id = R.string.channelimport_title)) - Card(internalPadding = PaddingValues(16.dp)) { - Text(text = stringResource(id = R.string.channelimport_instructions)) - Spacer(modifier = Modifier.height(24.dp)) - TextInput( - text = dataInput, - onTextChange = { - if (it != dataInput) { - vm.state.value = ImportChannelsDataState.Init - } - dataInput = it - }, - staticLabel = stringResource(id = R.string.channelimport_input_label), - maxLines = 6, - enabled = vm.state.value !is ImportChannelsDataState.Importing - ) - } - Card( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - val business = business - when (val state = vm.state.value) { - ImportChannelsDataState.Init -> { - Button( - text = stringResource(id = R.string.channelimport_import_button), - icon = R.drawable.ic_restore, - onClick = { vm.importData(dataInput.trim(), business) }, - modifier = Modifier.fillMaxWidth() - ) - } - ImportChannelsDataState.Importing -> { - ProgressView(text = stringResource(id = R.string.channelimport_importing),) - } - is ImportChannelsDataState.Done -> when (val result = state.result) { - is ChannelsImportResult.Success -> { - Dialog( - onDismiss = {}, - properties = DialogProperties(usePlatformDefaultWidth = false, dismissOnBackPress = false, dismissOnClickOutside = false), - buttons = null, - buttonsTopMargin = 0.dp - ) { - Column( - modifier = Modifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - TextWithIcon( - text = stringResource(id = R.string.channelimport_success), - textStyle = MaterialTheme.typography.body2, - icon = R.drawable.ic_check, - iconTint = positiveColor, - ) - Text(text = stringResource(id = R.string.channelimport_success_restart), textAlign = TextAlign.Center) - } - } - } - is ChannelsImportResult.Failure -> { - ErrorMessage( - header = stringResource(id = R.string.channelimport_error_title), - details = when (result) { - is ChannelsImportResult.Failure.Generic -> result.error.message - is ChannelsImportResult.Failure.MalformedData -> stringResource(id = R.string.channelimport_error_malformed) - is ChannelsImportResult.Failure.DecryptionError -> stringResource(id = R.string.channelimport_error_decryption) - is ChannelsImportResult.Failure.UnknownVersion -> stringResource(id = R.string.channelimport_error_unknown_version, result.version) - }, - alignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - } - } -} \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ImportChannelsDataViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ImportChannelsDataViewModel.kt deleted file mode 100644 index ee313f5d5..000000000 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/ImportChannelsDataViewModel.kt +++ /dev/null @@ -1,72 +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.settings.channels - -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import fr.acinq.phoenix.PhoenixBusiness -import fr.acinq.phoenix.managers.NodeParamsManager -import fr.acinq.phoenix.managers.PeerManager -import fr.acinq.phoenix.utils.channels.ChannelsImportHelper -import fr.acinq.phoenix.utils.channels.ChannelsImportResult -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.slf4j.LoggerFactory - -sealed class ImportChannelsDataState { - object Init : ImportChannelsDataState() - object Importing : ImportChannelsDataState() - data class Done(val result: ChannelsImportResult): ImportChannelsDataState() -} - -class ImportChannelsDataViewModel(val peerManager: PeerManager, val nodeParamsManager: NodeParamsManager) : ViewModel() { - - private val log = LoggerFactory.getLogger(this::class.java) - val state = mutableStateOf(ImportChannelsDataState.Init) - - fun importData(data: String, business: PhoenixBusiness) { - if (state.value == ImportChannelsDataState.Importing) return - viewModelScope.launch( - Dispatchers.Default + CoroutineExceptionHandler { _, e -> - log.error("failed to import channels data: ", e) - state.value = ImportChannelsDataState.Done(ChannelsImportResult.Failure.Generic(e)) - } - ) { - state.value = ImportChannelsDataState.Importing - delay(300) - val result = ChannelsImportHelper.doImportChannels( - data = data, - biz = business - ) - state.value = ImportChannelsDataState.Done(result) - } - } - - class Factory( - private val peerManager: PeerManager, - private val nodeParamsManager: NodeParamsManager, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return ImportChannelsDataViewModel(peerManager, nodeParamsManager) as T - } - } -} \ No newline at end of file diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/SpendFromChannelAddress.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/SpendFromChannelAddress.kt index 7b83f2d4b..35c67c064 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/SpendFromChannelAddress.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/SpendFromChannelAddress.kt @@ -164,17 +164,8 @@ fun SpendFromChannelAddress( is SpendFromChannelAddressViewState.Error.TxIndexMalformed -> { stringResource(id = R.string.spendchanneladdress_error_tx_index) } - is SpendFromChannelAddressViewState.Error.ChannelDataMalformed -> { - stringResource(id = R.string.spendchanneladdress_error_channel_data) - } - is SpendFromChannelAddressViewState.Error.ChannelDataDecryption -> { - stringResource(id = R.string.spendchanneladdress_error_channel_data) - } - is SpendFromChannelAddressViewState.Error.ChannelDataUnhandledState -> { - stringResource(id = R.string.spendchanneladdress_error_channel_data_state, state.stateClassName ?: "??") - } - is SpendFromChannelAddressViewState.Error.ChannelDataUnhandledVersion -> { - stringResource(id = R.string.spendchanneladdress_error_channel_data_version, state.version) + is SpendFromChannelAddressViewState.Error.InvalidChannelKeyPath -> { + stringResource(id = R.string.spendchanneladdress_error_channel_keypath) } is SpendFromChannelAddressViewState.Error.PublicKeyMalformed -> { stringResource(id = R.string.spendchanneladdress_error_remote_funding_pubkey, state.details) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/SpendFromChannelAddressViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/SpendFromChannelAddressViewModel.kt index 6868ab1ce..9323240be 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/SpendFromChannelAddressViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/channels/SpendFromChannelAddressViewModel.kt @@ -44,10 +44,7 @@ sealed class SpendFromChannelAddressViewState { data class Generic(val cause: Throwable) : Error() data object AmountMissing : Error() data object TxIndexMalformed : Error() - data object ChannelDataMalformed : Error() - data object ChannelDataDecryption : Error() - data class ChannelDataUnhandledState(val stateClassName: String?) : Error() - data class ChannelDataUnhandledVersion(val version: Int) : Error() + data object InvalidChannelKeyPath: Error() data class PublicKeyMalformed(val details: String) : Error() data class TransactionMalformed(val details: String) : Error() data class InvalidSig(val txId: TxId, val publicKey: PublicKey, val fundingScript: ByteVector, val signature: ByteVector64): Error() @@ -92,7 +89,7 @@ class SpendFromChannelAddressViewModel( business = business, amount = amount, fundingTxIndex = fundingTxIndex, - channelData = channelData, + channelKeyPath = "FIXME", remoteFundingPubkey = remoteFundingPubkey, unsignedTx = unsignedTx, ) @@ -102,17 +99,8 @@ class SpendFromChannelAddressViewModel( delay(300.milliseconds) state.value = SpendFromChannelAddressViewState.SignedTransaction(result.publicKey, result.signature) } - is SpendChannelAddressResult.Failure.ChannelDataDecryption -> { - state.value = SpendFromChannelAddressViewState.Error.ChannelDataDecryption - } - is SpendChannelAddressResult.Failure.ChannelDataMalformed -> { - state.value = SpendFromChannelAddressViewState.Error.ChannelDataMalformed - } - is SpendChannelAddressResult.Failure.ChannelDataUnhandledState -> { - state.value = SpendFromChannelAddressViewState.Error.ChannelDataUnhandledState(result.stateName) - } - is SpendChannelAddressResult.Failure.ChannelDataUnhandledVersion -> { - state.value = SpendFromChannelAddressViewState.Error.ChannelDataUnhandledVersion(result.version) + is SpendChannelAddressResult.Failure.InvalidChannelKeyPath -> { + state.value = SpendFromChannelAddressViewState.Error.InvalidChannelKeyPath } is SpendChannelAddressResult.Failure.Generic -> { state.value = SpendFromChannelAddressViewState.Error.Generic(result.error) diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/reset/ResetWallet.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/reset/ResetWallet.kt index 4b10c7016..e99f85ed2 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/reset/ResetWallet.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/reset/ResetWallet.kt @@ -37,7 +37,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -54,7 +53,6 @@ import fr.acinq.phoenix.android.application import fr.acinq.phoenix.android.components.AmountWithFiatBeside import fr.acinq.phoenix.android.components.ProgressView import fr.acinq.phoenix.android.components.TextWithIcon -import fr.acinq.phoenix.android.components.buttons.BorderButton import fr.acinq.phoenix.android.components.buttons.Checkbox import fr.acinq.phoenix.android.components.buttons.Clickable import fr.acinq.phoenix.android.components.buttons.FilledButton @@ -102,7 +100,16 @@ fun ResetWallet( ResetWalletStep.Confirm -> { ReviewWalletBeforeDeletion( business = business, - onConfirmClick = vm::deleteWalletData, + onConfirmClick = { + vm.deleteWalletData(onWalletDeleted = { context -> + BusinessManager.stopBusiness(walletId) + context.startActivity( + Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + ) + }) + }, onLightningBalanceClick = onLightningBalanceClick, onSwapInBalanceClick = onSwapInBalanceClick, onFinalBalanceClick = onFinalBalanceClick ) } @@ -288,26 +295,12 @@ private fun DeletingWallet(state: ResetWalletStep.Deleting) { @Composable private fun WalletDeleted(walletId: WalletId) { - val context = LocalContext.current Column( modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), verticalArrangement = Arrangement.spacedBy(2.dp), horizontalAlignment = Alignment.CenterHorizontally ) { SuccessMessage(header = stringResource(id = R.string.reset_wallet_success)) - Spacer(modifier = Modifier.height(16.dp)) - BorderButton( - text = stringResource(id = R.string.btn_ok), - icon = R.drawable.ic_check, - onClick = { - BusinessManager.stopBusiness(walletId) - context.startActivity( - Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - ) - } - ) } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/reset/ResetWalletViewModel.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/reset/ResetWalletViewModel.kt index de85280f0..67e04a270 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/reset/ResetWalletViewModel.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/reset/ResetWalletViewModel.kt @@ -16,6 +16,7 @@ package fr.acinq.phoenix.android.settings.reset +import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -61,7 +62,9 @@ class ResetWalletViewModel(val application: PhoenixApplication, val walletId: Wa val state = mutableStateOf(ResetWalletStep.Init) - fun deleteWalletData() { + fun deleteWalletData( + onWalletDeleted: (Context) -> Unit, + ) { if (state.value != ResetWalletStep.Confirm) return state.value = ResetWalletStep.Deleting.Init @@ -69,7 +72,7 @@ class ResetWalletViewModel(val application: PhoenixApplication, val walletId: Wa log.error("failed to reset wallet data: ", e) state.value = ResetWalletStep.Result.Failure.Error(e) }) { - log.info("resetting wallet with wallet=$walletId") + log.info("resetting wallet=$walletId") delay(350) val context = application.applicationContext @@ -108,7 +111,7 @@ class ResetWalletViewModel(val application: PhoenixApplication, val walletId: Wa delay(300) log.info("successfully deleted wallet=$walletId") - + onWalletDeleted(context) state.value = ResetWalletStep.Result.Success } } diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupView.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupView.kt index 08c717ff4..35f319f6d 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupView.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/startup/StartupView.kt @@ -96,15 +96,14 @@ fun StartupView( onWalletReady: () -> Unit, forceWalletId: WalletId?, ) { - val showIntro = application.globalPrefs.getShowIntro.collectAsState(initial = null) - if (showIntro.value == true) { + val showIntro by application.globalPrefs.getShowIntro.collectAsState(initial = null) + if (showIntro == true) { LaunchedEffect(Unit) { onShowIntro() } + return } Box( - modifier = Modifier - .fillMaxSize() - .imePadding(), + modifier = Modifier.fillMaxSize().imePadding(), contentAlignment = Alignment.Center ) { diff --git a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions/PaymentExtensions.kt b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions/PaymentExtensions.kt index 5745256ad..f6fd5419e 100644 --- a/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions/PaymentExtensions.kt +++ b/phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/extensions/PaymentExtensions.kt @@ -37,6 +37,7 @@ import fr.acinq.phoenix.android.R import fr.acinq.phoenix.android.utils.converters.AmountFormatter.toPrettyString import fr.acinq.phoenix.data.BitcoinUnit import fr.acinq.phoenix.utils.extensions.desc +import fr.acinq.phoenix.utils.extensions.description @Composable fun LightningOutgoingPayment.smartDescription(): String? = when (val details = this.details) { @@ -66,7 +67,7 @@ fun AutomaticLiquidityPurchasePayment.smartDescription(): String = @Composable fun IncomingPayment.smartDescription() : String? = when (this) { is Bolt11IncomingPayment -> paymentRequest.description - is Bolt12IncomingPayment -> null + is Bolt12IncomingPayment -> metadata.description is OnChainIncomingPayment -> stringResource(id = R.string.paymentdetails_desc_swapin) is LegacySwapInIncomingPayment -> stringResource(id = R.string.paymentdetails_desc_swapin) is LegacyPayToOpenIncomingPayment -> when (val origin = origin) { 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 e20fe14c2..12a6c676b 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 @@ -131,8 +131,6 @@ Comisión N/D Cargando comisión… - Factura sin importe - La factura de este pago no solicita un importe concreto. Los nodos malintencionados pueden aprovecharse de esto durante el pago.\n\nPara mayor seguridad, pídele al destinatario que especifique un importe al generar la factura. Esperando canales… Pagar @@ -209,7 +207,6 @@ Canales de pago - Importar canales Saldo El saldo es el monto total de los canales activos. Es lo que puedes gastar en Lightning. Liquidez entrante @@ -237,20 +234,6 @@ Datos de canales Compartir datos de canales - - - Importar datos de canales sin procesar - Esta pantalla es una herramienta de depuración que puede usarse para importar manualmente los datos de canales encriptados.\n\nUtilizar con precaución. - Blob de datos - Importar - Importando datos… - Importación correcta - Debes reiniciar Phoenix ahora. - Error de importación - El formato de los datos es incorrecto. Se espera un blob hexadecimal encriptado. - No se pudieron desencriptar los datos con esta billetera. - La versión %1$d no es compatible - Cargando… diff --git a/phoenix-android/src/main/res/values-cs/strings.xml b/phoenix-android/src/main/res/values-cs/strings.xml index fc0dc1ea0..db9b23989 100644 --- a/phoenix-android/src/main/res/values-cs/strings.xml +++ b/phoenix-android/src/main/res/values-cs/strings.xml @@ -159,8 +159,6 @@ Poplatek N/A Načítání poplatku… - Faktura bez částky - Faktura pro tuto platbu nevyžaduje konkrétní částku. Toho mohou zneužít škodlivé uzly během platby. \n\nPro jistotu požádejte příjemce, aby při generování faktury uvedl částku. Připojování… Zaplatit Potvrdit & Zaplatit @@ -270,7 +268,6 @@ Platební kanály - Importovat kanály Utratit z adresy kanálu Přehled Zůstatek @@ -301,20 +298,6 @@ Data kanálů Sdílet data kanálu - - - Importovat holá data kanálu - Tato obrazovka slouží jako debugovací nástroj, který umožňuje ručně importovat zašifrovaná data kanálů.\n\nPoužívejte opatrně. - Blob dat - Importovat - Importování dat… - Import proběhl úspěšně - Nyní musíte restartovat Phoenix. - Import selhal - Data jsou chybná. Očekává se zašifrovaný hexadecimální blob. - Data se nepodařilo rozšifrovat touto peněženkou. - Verze %1$d není podporovaná - Utratit z adresy kanálu @@ -334,10 +317,6 @@ Nepodařilo se podepsat data Neplatná částka Neplatný index transakce - Chybná data kanálu - Nepodařilo se rozšifrovat data kanálu - Neošetřený stav kanálu [%1$s] - Chybná verze kanálu [%1$s] Chybný vzdálený financující funding veřejný klíč [%1$s] Chybná nepodepsaná transakce [%1$s] Chybný vzdálený financující funding veřejný klíč [%1$s] diff --git a/phoenix-android/src/main/res/values-de/strings.xml b/phoenix-android/src/main/res/values-de/strings.xml index 8b7c589f4..120857576 100644 --- a/phoenix-android/src/main/res/values-de/strings.xml +++ b/phoenix-android/src/main/res/values-de/strings.xml @@ -126,8 +126,6 @@ Gebühr N/A Lade Gebühr… - Rechnung ohne Betrag - Die Rechnung für diese Zahlung hat keinen festgelegten Betrag. Dies könnte von bösartigen Nodes ausgenutzt werden.\n\nUm kein Risiko einzugehen, bitten Sie den Empfänger, bei der Erstellung der Rechnung einen Betrag festzulegen. Verbinde… Zahlen @@ -204,7 +202,6 @@ Zahlungs-Kanäle - Kanäle importieren Übersicht Guthaben Guthaben ist das gesamte Guthaben Ihrer aktiven Kanäle. Diesen Betrag können Sie über Lightning ausgeben. @@ -234,20 +231,6 @@ Kanal-Daten Teile Kanal-Daten - - - Rohdaten der Kanäle importieren - Dieser Bildschirm ist ein Debugging-Tool, mit dem du verschlüsselte Kanaldaten manuell importieren kannst. - Datenblob - Importieren - Daten importieren.. - Import erfolgreich - Du musst Phoenix jetzt neu starten. - Der Import ist fehlgeschlagen - Die Daten sind missgebildet. Es wird ein verschlüsselter Hex-Blob erwartet. - Die Daten konnten von dieser Wallet nicht entschlüsselt werden. - Version %1$d wird nicht unterstützt - Lädt… diff --git a/phoenix-android/src/main/res/values-fr/strings.xml b/phoenix-android/src/main/res/values-fr/strings.xml index 758ffc8bf..8abb8516d 100644 --- a/phoenix-android/src/main/res/values-fr/strings.xml +++ b/phoenix-android/src/main/res/values-fr/strings.xml @@ -129,8 +129,6 @@ Frais N/A Calcul des frais… - Requête sans montant - La requête pour ce paiement ne précise pas de montant. Cela peut être exploité par des noeuds malicieux pendant le paiement.\n\nPour plus de sécurité, demandez au destinataire de spécifier un montant lorsqu\'il crée sa requête de paiement. En attente des canaux… Payer @@ -219,7 +217,6 @@ Canaux de paiements - Importer des canaux Solde Votre solde est le total du solde de vos canaux actifs. C\'est ce que vous pouvez dépenser via Lightning. Liquidité entrante @@ -247,20 +244,6 @@ Données du canal Partager les données du canal - - - Importer des données de canal - Cet écran est un outil de debug utilisé pour importer manuellement des données de canaux chiffrées.\n\nUtiliser avec prudence. - Données - Importer - Import des données… - Import réussi - Vous devez maintenant redémarrer Phoenix. - Import échoué - Les données sont malformées. Un blob hexa est attendu. - Les données n\'ont pas pu être déchiffrées. - La version %1$d n\'est pas supportée - Chargement… @@ -394,6 +377,7 @@ Clé publique du destinataire Description de la requête de paiement + Description de l\'offer Hash du paiement Requête de paiement Préimage 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 f7225ced4..d4fd18e4b 100644 --- a/phoenix-android/src/main/res/values-pt-rBR/strings.xml +++ b/phoenix-android/src/main/res/values-pt-rBR/strings.xml @@ -129,8 +129,6 @@ 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 @@ -206,7 +204,6 @@ 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 @@ -234,20 +231,6 @@ 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… diff --git a/phoenix-android/src/main/res/values-sk/strings.xml b/phoenix-android/src/main/res/values-sk/strings.xml index 232566f30..6251061f9 100644 --- a/phoenix-android/src/main/res/values-sk/strings.xml +++ b/phoenix-android/src/main/res/values-sk/strings.xml @@ -127,8 +127,6 @@ Poplatok N/A Načítavanie poplatku… - Faktúra bez sumy - Faktúra pre túto platbu nevyžaduje konkrétnu sumu. Toto môžu zneužiť škodlivé uzly počas platby. \n\nPre istotu požiadajte príjemcu, aby pri generovaní faktúry uviedol sumu. Pripájanie… Zaplatiť Skúsiť znova @@ -215,7 +213,6 @@ Platobné kanály - Importovať kanály Prehľad Zostatok Zostatok: Toto je súhrnný zostatok vašich aktívnych kanálov. Toľko môžete poslať cez Lightning. @@ -245,20 +242,6 @@ Dáta kanálov Zdieľať dáta kanálov - - - Importovať surové údaje kanála - Táto obrazovka je nástroj na ladenie, ktorý možno použiť na ručný import zašifrovaných údajov kanálov.\n\nPoužívajte s opatrnosťou. - Dátový blok - Importovať - Importujú sa údaje… - Import úspešný - Teraz musíte reštartovať Phoenix. - Import zlyhal - Údaje sú poškodené. Očakáva sa zašifrovaný hex blob. - Údaje nemohla dešifrovať táto peňaženka. - Verzia %1$d nie je podporovaná - Načítavanie… diff --git a/phoenix-android/src/main/res/values-sw/strings.xml b/phoenix-android/src/main/res/values-sw/strings.xml index 1d72cdd51..ae0b386b7 100644 --- a/phoenix-android/src/main/res/values-sw/strings.xml +++ b/phoenix-android/src/main/res/values-sw/strings.xml @@ -136,8 +136,6 @@ Gharama N/A Inapakia gharama… - Hati bila kiasi - Hati ya malipo kwa malipo haya haitaji kiasi maalum. Hii inaweza kutumiwa vibaya na nodi hatari wakati wa malipo.\n\nIli kuwa salama, uliza mpokeaji aweke kiasi wakati wa kutengeneza hati. Inasubiri njia za malipo… Lipa Jaribu tena @@ -229,7 +227,6 @@ Kanali za malipo - Ingiza kanali Muonekano Salio Salio ni jumla ya salio la kanali zako za kazi. Hii ndiyo unaweza kutumia kwa Lightning. @@ -259,20 +256,6 @@ Data za kanali Shiriki data za kanali - - - Ingiza data za kanali za raw - Skrini hii ni zana ya uundaji inayoweza kutumika kwa kuingiza data za kanali zilizowekwa siri kwa mkono.\n\nTumia kwa tahadhari. - Data blob - Ingiza - Inaingiza data… - Ingizo limefanikiwa - Sasa lazima upige upya Phoenix. - Ingizo limekosa - Data zimeharibika. Blob ya hex yenye usalama inatarajiwa. - Data haiwezi kufunguliwa na pochi hii. - Toleo %1$d halisaidiwi - Inapakia… diff --git a/phoenix-android/src/main/res/values-vi/strings.xml b/phoenix-android/src/main/res/values-vi/strings.xml index 2f4645f03..90c843d4e 100644 --- a/phoenix-android/src/main/res/values-vi/strings.xml +++ b/phoenix-android/src/main/res/values-vi/strings.xml @@ -128,8 +128,6 @@ Phí Không áp dụng Đang tải phí… - Hoá đơn không có số tiền - Hóa đơn cho khoản thanh toán này không điền số tiền cụ thể. Các nút mạng khác có thể lợi dụng điều này và lừa đảo bạn trong quá trình thanh toán.\n\nĐể tránh nguy cơ lừa đảo, hãy yêu cầu người nhận ghi rõ số tiền khi lập hóa đơn. Đang chờ các kênh… Thanh toán @@ -207,7 +205,6 @@ Kênh thanh toán - Nhập các kênh Tổng quan Số dư Số dư là số dư tổng hợp của tất các kênh đang hoạt động của bạn. Đó là số tiền bạn có thể dùng trên Lightning. @@ -237,20 +234,6 @@ Dữ liệu các kênh Chia sẻ dữ liệu kênh - - - Nhập dữ liệu thô của kênh - Màn hình này là một công cụ gỡ lỗi mà bạn có thể nhập bằng tay các dữ liệu mã hóa của các kênh.\n\nHãy thận trọng khi sử dụng. - Dữ liệu đối tượng nhị phân lớn - Nhập - Đang nhập dữ liệu… - Nhập thành công - Bạn cần khởi động lại Phoenix ngay bây giờ. - Nhập không thành công - Dữ liệu không đúng định dạng. Phải là định dạng hex đối tượng nhị phân lớn đã được mã hoá. - Dữ liệu không thể giải mã được bằng ví này. - Phiên bản %1$d không đuợc hỗ trợ - Đang tải… diff --git a/phoenix-android/src/main/res/values/strings.xml b/phoenix-android/src/main/res/values/strings.xml index a5083c492..1e88ae63a 100644 --- a/phoenix-android/src/main/res/values/strings.xml +++ b/phoenix-android/src/main/res/values/strings.xml @@ -165,8 +165,6 @@ Fee N/A Loading fee… - Amountless invoice - The invoice for this payment does not request a specific amount. This may be exploited by malicious nodes during the payment.\n\nTo be safe, ask the recipient to specify an amount when generating the invoice. Waiting for channels… Pay Confirm & Pay @@ -279,7 +277,6 @@ Payment channels - Import channels Spend channel address Overview Balance @@ -310,20 +307,6 @@ Channels data Share channel data - - - Import raw channel data - This screen is a debugging tool that can be used to manually import encrypted channels data.\n\nUse with caution. - Data blob - Import - Importing data… - Import successful - You must now restart Phoenix. - Import has failed - Data are malformed. A encrypted hex blob is expected. - Data could not be decrypted by this wallet. - Version %1$d is not supported - Spend channel address @@ -343,10 +326,7 @@ Failed to sign data Invalid amount Invalid tx index - Malformed channel data - Cannot decrypt channel data - Unhandled channel state [%1$s] - Malformed channel version [%1$s] + Invalid channel keypath Malformed remote funding pubkey [%1$s] Malformed unsigned tx [%1$s] Malformed remote funding pubkey [%1$s] @@ -498,8 +478,9 @@ Preimage Cryptographic proof that the recipient successfully received the payment. Bolt11 invoice - Invoice description Bolt12 invoice + Invoice description + Offer description Offer Metadata Payer key diff --git a/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj b/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj index 8c2d3dd48..dc0e28b04 100644 --- a/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj +++ b/phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj @@ -449,7 +449,6 @@ DCFB8DF72A94066100947698 /* Task+Sleep.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFB8DF62A94066100947698 /* Task+Sleep.swift */; }; DCFB8DF92A94112A00947698 /* Dictionary+MapKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFB8DF82A94112A00947698 /* Dictionary+MapKeys.swift */; }; DCFBC5592AE2CFEF00E3A418 /* BizNotificationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFBC5582AE2CFEF00E3A418 /* BizNotificationCell.swift */; }; - DCFBC55B2AEAC2B000E3A418 /* ImportChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFBC55A2AEAC2B000E3A418 /* ImportChannelsView.swift */; }; DCFC72042862237400D6B293 /* Asserts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFC72032862237400D6B293 /* Asserts.swift */; }; DCFD079126D84A380020DD8E /* HorizontalActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFD079026D84A380020DD8E /* HorizontalActivity.swift */; }; F4AED298257A50CD009485C1 /* LogsConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4AED296257A50CD009485C1 /* LogsConfigurationView.swift */; }; @@ -897,7 +896,6 @@ DCFB8DF62A94066100947698 /* Task+Sleep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Sleep.swift"; sourceTree = ""; }; DCFB8DF82A94112A00947698 /* Dictionary+MapKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+MapKeys.swift"; sourceTree = ""; }; DCFBC5582AE2CFEF00E3A418 /* BizNotificationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BizNotificationCell.swift; sourceTree = ""; }; - DCFBC55A2AEAC2B000E3A418 /* ImportChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportChannelsView.swift; sourceTree = ""; }; DCFC72032862237400D6B293 /* Asserts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Asserts.swift; sourceTree = ""; }; DCFD079026D84A380020DD8E /* HorizontalActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalActivity.swift; sourceTree = ""; }; F4AED296257A50CD009485C1 /* LogsConfigurationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogsConfigurationView.swift; sourceTree = ""; }; @@ -1854,7 +1852,6 @@ children = ( 53BEF648B3A03C66B611BC06 /* ChannelsConfigurationView.swift */, DCA5391B29F7202F001BD3D5 /* ChannelInfoPopup.swift */, - DCFBC55A2AEAC2B000E3A418 /* ImportChannelsView.swift */, ); path = channels; sourceTree = ""; @@ -2439,7 +2436,6 @@ DC46CB1628D9F30500C4EAC7 /* LoadingView.swift in Sources */, DC2ABAD92BED142900C11C9C /* ShakeEffect.swift in Sources */, DC72CEFD2C9B25CC00C810A8 /* PaymentDetails.swift in Sources */, - DCFBC55B2AEAC2B000E3A418 /* ImportChannelsView.swift in Sources */, DCFB8DF72A94066100947698 /* Task+Sleep.swift in Sources */, DC4CF3CA2BE91FED003A957F /* SetNewPinView.swift in Sources */, DC39D4EF287497440030F18D /* SmartModal.swift in Sources */, diff --git a/phoenix-ios/phoenix-ios/Localizable.xcstrings b/phoenix-ios/phoenix-ios/Localizable.xcstrings index eae4f2520..98f829fe2 100644 --- a/phoenix-ios/phoenix-ios/Localizable.xcstrings +++ b/phoenix-ios/phoenix-ios/Localizable.xcstrings @@ -5865,6 +5865,7 @@ } }, "An unknown error has occurred." : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -13084,6 +13085,7 @@ } }, "Data could not be decrypted by this wallet." : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -13124,6 +13126,7 @@ } }, "Data is malformed. An encrypted hex blob is expected." : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -21220,6 +21223,7 @@ }, "Import channels" : { "comment" : "Navigation bar title", + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -21260,6 +21264,7 @@ } }, "Import has failed" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -21300,6 +21305,7 @@ } }, "Import successful" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -21380,6 +21386,7 @@ } }, "Importing data…" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -29066,6 +29073,7 @@ } }, "Paste encrypted data blob here" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -41516,6 +41524,7 @@ } }, "This screen is a debugging tool that can be used to manually import encrypted channels data.\n\nUse with caution." : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -44511,6 +44520,7 @@ } }, "Version %d is not supported" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -47713,6 +47723,7 @@ } }, "You must now restart Phoenix." : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { diff --git a/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift b/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift index 8998133f7..ab15bda27 100644 --- a/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift +++ b/phoenix-ios/phoenix-ios/kotlin/KotlinExtensions+Payments.swift @@ -70,6 +70,8 @@ extension WalletPaymentInfo { if let bolt11 = incomingPayment as? Lightning_kmpBolt11IncomingPayment { return sanitize(bolt11.paymentRequest.description_) + } else if let bolt12 = incomingPayment as? Lightning_kmpBolt12IncomingPayment { + return sanitize(bolt12.metadata.description__) } } else if let outgoingPayment = payment as? Lightning_kmpOutgoingPayment { diff --git a/phoenix-ios/phoenix-ios/views/configuration/advanced/channels/ChannelsConfigurationView.swift b/phoenix-ios/phoenix-ios/views/configuration/advanced/channels/ChannelsConfigurationView.swift index 63b7ceffe..801f34b4d 100644 --- a/phoenix-ios/phoenix-ios/views/configuration/advanced/channels/ChannelsConfigurationView.swift +++ b/phoenix-ios/phoenix-ios/views/configuration/advanced/channels/ChannelsConfigurationView.swift @@ -10,10 +10,6 @@ fileprivate var log = LoggerFactory.shared.logger(filename, .warning) struct ChannelsConfigurationView: View { - enum NavLinkTag: String, Codable { - case ImportChannels - } - @State var sharing: String? = nil @State var channels: [LocalChannelInfo] = [] @@ -24,17 +20,12 @@ struct ChannelsConfigurationView: View { ) @State var capacityHeight: CGFloat? = nil - // - @State var navLinkTag: NavLinkTag? = nil - // - @StateObject var toast = Toast() @ObservedObject var currencyPrefs = CurrencyPrefs.current @Environment(\.presentationMode) var presentationMode: Binding - @EnvironmentObject var navCoordinator: NavigationCoordinator @EnvironmentObject var popoverState: PopoverState @EnvironmentObject var deepLinkManager: DeepLinkManager @@ -49,12 +40,6 @@ struct ChannelsConfigurationView: View { .navigationTitle(NSLocalizedString("Payment channels", comment: "Navigation bar title")) .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: menuButton()) - .navigationStackDestination(isPresented: navLinkTagBinding()) { - navLinkView() - } - .navigationStackDestination(for: NavLinkTag.self) { tag in // iOS 17+ - navLinkView(tag) - } } @ViewBuilder @@ -202,9 +187,7 @@ struct ChannelsConfigurationView: View { .foregroundColor(Color.primary) } } // - // .padding([.leading], 10) .padding([.top, .bottom], 8) - // .padding(.trailing) } // } @@ -212,15 +195,6 @@ struct ChannelsConfigurationView: View { func menuButton() -> some View { Menu { - Button { - importChannels() - } label: { - Label { - Text("Import channels") - } icon: { - Image(systemName: "square.and.arrow.down") - } - } if !channels.isEmpty { Button { closeAllChannels() @@ -247,37 +221,10 @@ struct ChannelsConfigurationView: View { } } - @ViewBuilder - func navLinkView() -> some View { - - if let tag = self.navLinkTag { - navLinkView(tag) - } else { - EmptyView() - } - } - - @ViewBuilder - func navLinkView(_ tag: NavLinkTag) -> some View { - - switch tag { - case .ImportChannels: - ImportChannelsView() - } - } - // -------------------------------------------------- // MARK: View Helpers // -------------------------------------------------- - func navLinkTagBinding() -> Binding { - - return Binding( - get: { navLinkTag != nil }, - set: { if !$0 { navLinkTag = nil }} - ) - } - func hasUsableChannels() -> Bool { return channels.contains { $0.isUsable } @@ -335,18 +282,8 @@ struct ChannelsConfigurationView: View { // MARK: Actions // -------------------------------------------------- - func navigateTo(_ tag: NavLinkTag) { - log.trace("navigateTo(\(tag.rawValue))") - - if #available(iOS 17, *) { - navCoordinator.path.append(tag) - } else { - navLinkTag = tag - } - } - func showChannelInfoPopover(_ channel: LocalChannelInfo) { - log.trace("showChannelInfoPopover()") + log.trace(#function) popoverState.display(dismissable: true) { ChannelInfoPopup( @@ -357,14 +294,8 @@ struct ChannelsConfigurationView: View { } } - func importChannels() { - log.trace("importChannels()") - - navigateTo(.ImportChannels) - } - func closeAllChannels() { - log.trace("closeAllChannels()") + log.trace(#function) presentationMode.wrappedValue.dismiss() DispatchQueue.main.asyncAfter(deadline: .now() + 0.55) { @@ -373,7 +304,7 @@ struct ChannelsConfigurationView: View { } func forceCloseAllChannels() { - log.trace("forceCloseAllChannels()") + log.trace(#function) presentationMode.wrappedValue.dismiss() DispatchQueue.main.asyncAfter(deadline: .now() + 0.55) { diff --git a/phoenix-ios/phoenix-ios/views/configuration/advanced/channels/ImportChannelsView.swift b/phoenix-ios/phoenix-ios/views/configuration/advanced/channels/ImportChannelsView.swift deleted file mode 100644 index 1f72d7b43..000000000 --- a/phoenix-ios/phoenix-ios/views/configuration/advanced/channels/ImportChannelsView.swift +++ /dev/null @@ -1,225 +0,0 @@ -import SwiftUI -import PhoenixShared - -fileprivate let filename = "ImportChannelsView" -#if DEBUG && true -fileprivate var log = LoggerFactory.shared.logger(filename, .trace) -#else -fileprivate var log = LoggerFactory.shared.logger(filename, .warning) -#endif - -fileprivate enum ImportResultFailure { - case Generic(error: KotlinThrowable) - case UnknownVersion(version: Int32) - case MalformedData - case DecryptionError - case Unknown -} - -fileprivate enum ImportResult { - case Success - case Failure(reason: ImportResultFailure) -} - - -struct ImportChannelsView: View { - - @State var dataBlobText: String = "" - - @State var importInProgress: Bool = false - @State var importResult: ChannelsImportResult? = nil - - @ViewBuilder - var body: some View { - - content() - .navigationTitle(NSLocalizedString("Import channels", comment: "Navigation bar title")) - .navigationBarTitleDisplayMode(.inline) - } - - @ViewBuilder - func content() -> some View { - - List { - section_info() - section_status() - } - .listStyle(.insetGrouped) - .listBackgroundColor(.primaryBackground) - } - - @ViewBuilder - func section_info() -> some View { - - Section { - VStack(alignment: HorizontalAlignment.leading, spacing: 0) { - Text( - """ - This screen is a debugging tool that can be used to manually import \ - encrypted channels data. - - Use with caution. - """ - ) - .padding(.bottom, 20) - - HStack(alignment: VerticalAlignment.center, spacing: 0) { - TextField("Paste encrypted data blob here", text: $dataBlobText) - - // Clear button (appears when TextField's text is non-empty) - Button { - dataBlobText = "" - } label: { - Image(systemName: "multiply.circle.fill") - .foregroundColor(.secondary) - } - .isHidden(dataBlobText == "") - } // - .padding(.all, 8) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color(UIColor.systemBackground)) - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.textFieldBorder, lineWidth: 1) - ) - } // - } // - } - - @ViewBuilder - func section_status() -> some View { - - Section { - VStack(alignment: HorizontalAlignment.center, spacing: 15) { - - Button { - importButtonTapped() - } label: { - HStack(alignment: VerticalAlignment.center, spacing: 5) { - Text("Import") - Image(systemName: "square.and.arrow.down") - .imageScale(.small) - } - } - .disabled(importInProgress || dataBlobTextIsEmpty()) - .font(.title3.weight(.medium)) - - if importInProgress { - - HStack(alignment: VerticalAlignment.center, spacing: 5) { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - Text("Importing data…") - } // - - } else if let result = importResult { - - switch toEnum(result) { - case .Success: - - Group { - HStack(alignment: VerticalAlignment.center, spacing: 5) { - Image(systemName: "checkmark.circle") - Text("Import successful") - } - Text("You must now restart Phoenix.").bold() - } - .foregroundColor(.appPositive) - - case .Failure(let reason): - - Group { - HStack(alignment: VerticalAlignment.center, spacing: 5) { - Image(systemName: "xmark.circle") - Text("Import has failed") - } - switch reason { - case .Generic(let error): - Text(verbatim: error.message ?? "Unknown error thrown") - case .UnknownVersion(let version): - Text("Version \(version) is not supported") - case .MalformedData: - Text("Data is malformed. An encrypted hex blob is expected.") - case .DecryptionError: - Text("Data could not be decrypted by this wallet.") - case .Unknown: - Text("An unknown error has occurred.") - } // - } - .foregroundColor(.appNegative) - - } // - } - - } // - .frame(maxWidth: .infinity) - - } // - } - - func dataBlobTextIsEmpty() -> Bool { - - return trimmedDataBlobText().isEmpty - } - - func trimmedDataBlobText() -> String { - - return dataBlobText.trimmingCharacters(in: .whitespacesAndNewlines) - } - - func importButtonTapped() { - log.trace("importButtonTapped()") - - importInProgress = true - - let data = dataBlobText - let biz = Biz.business - Task { @MainActor in - - // Give the UI time to update and display "importing..." - // It looks better that way. - try await Task.sleep(seconds: 0.5) - - let result: ChannelsImportResult - do { - result = try await ChannelsImportHelper.shared.doImportChannels(data: data, biz: biz) - } catch { - // Errors SHOULD be caught in Kotlin, and returned as a Failure result. - // So if an error is thrown, it's a bug in Kotlin. - log.error("ChannelsImportHelper.shared.doImportChannels(): threw error: \(error)") - - let message = "Threw error: \(error)" - let kotlinError = KotlinThrowable(message: message) - result = ChannelsImportResult.Failure.FailureGeneric(error: kotlinError) - } - - importResult = result - importInProgress = false - } - } - - private func toEnum(_ result: ChannelsImportResult) -> ImportResult { - - switch result { - case _ as ChannelsImportResult.Success: - return .Success - - case let r as ChannelsImportResult.FailureGeneric: - return .Failure(reason: .Generic(error: r.error)) - - case let r as ChannelsImportResult.FailureUnknownVersion: - return .Failure(reason: .UnknownVersion(version: r.version)) - - case _ as ChannelsImportResult.FailureMalformedData: - return .Failure(reason: .MalformedData) - - case _ as ChannelsImportResult.FailureDecryptionError: - return .Failure(reason: .DecryptionError) - - default: - return .Failure(reason: .Unknown) - } - } -} diff --git a/phoenix-ios/phoenix-ios/views/receive/LightningDualView.swift b/phoenix-ios/phoenix-ios/views/receive/LightningDualView.swift index d820a1b0f..e561d2405 100644 --- a/phoenix-ios/phoenix-ios/views/receive/LightningDualView.swift +++ b/phoenix-ios/phoenix-ios/views/receive/LightningDualView.swift @@ -707,7 +707,7 @@ struct LightningDualView: View { fixedDesc = finalDesc ?? "" } - let offerPair = Lightning_kmpOfferManagerCompanion.shared.deterministicOffer( + let offerAndKey = Lightning_kmpOfferManagerCompanion.shared.deterministicOffer( chainHash: NodeParamsManager.companion.chain.chainHash, nodePrivateKey: nodeParams.nodePrivateKey, trampolineNodeId: NodeParamsManager.companion.trampolineNodeId, @@ -715,7 +715,7 @@ struct LightningDualView: View { description: fixedDesc, pathId: nil ) - offerStr = offerPair.first!.encode() + offerStr = offerAndKey.offer.encode() } } diff --git a/phoenix-ios/phoenix-ios/views/send/ValidateView.swift b/phoenix-ios/phoenix-ios/views/send/ValidateView.swift index 6bad8846c..2c8e344d1 100644 --- a/phoenix-ios/phoenix-ios/views/send/ValidateView.swift +++ b/phoenix-ios/phoenix-ios/views/send/ValidateView.swift @@ -1902,8 +1902,8 @@ struct ValidateView: View { let payerKey: Bitcoin_kmpPrivateKey if contact?.useOfferKey ?? false { - let offerData = try await Biz.business.nodeParamsManager.defaultOffer() - payerKey = offerData.payerKey + let offerAndKey = try await Biz.business.nodeParamsManager.defaultOffer() + payerKey = offerAndKey.privateKey } else { payerKey = Lightning_randomKey() } diff --git a/phoenix-ios/phoenix-ios/views/tools/MergeChannelsView.swift b/phoenix-ios/phoenix-ios/views/tools/MergeChannelsView.swift index 0fdca7c43..d51805835 100644 --- a/phoenix-ios/phoenix-ios/views/tools/MergeChannelsView.swift +++ b/phoenix-ios/phoenix-ios/views/tools/MergeChannelsView.swift @@ -545,7 +545,7 @@ struct MergeChannelsView: View { } if !operationInProgress { - let allChannelsReady = channels.allSatisfy { $0.isTerminated || $0.isUsable || $0.isLegacyWait } + let allChannelsReady = channels.allSatisfy { $0.isTerminated || $0.isUsable } if !allChannelsReady { return NSLocalizedString("restoring connections", comment: "") } diff --git a/phoenix-shared/build.gradle.kts b/phoenix-shared/build.gradle.kts index 5fb514529..2b709c1fa 100644 --- a/phoenix-shared/build.gradle.kts +++ b/phoenix-shared/build.gradle.kts @@ -95,10 +95,12 @@ kotlin { implementation("io.ktor:ktor-client-json:${libs.versions.ktor.get()}") implementation("io.ktor:ktor-serialization-kotlinx-json:${libs.versions.ktor.get()}") implementation("io.ktor:ktor-client-content-negotiation:${libs.versions.ktor.get()}") + // serialization + implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:${libs.versions.serialization.get()}") // sqldelight implementation("app.cash.sqldelight:runtime:${libs.versions.sqldelight.get()}") implementation("app.cash.sqldelight:coroutines-extensions:${libs.versions.sqldelight.get()}") - // SKEI + // SKIE implementation("co.touchlab.skie:configuration-annotations:${libs.versions.skie.get()}") } } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/csv/WalletPaymentCsvWriter.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/csv/WalletPaymentCsvWriter.kt index 6d4f7cf95..08a8e74c9 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/csv/WalletPaymentCsvWriter.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/csv/WalletPaymentCsvWriter.kt @@ -28,7 +28,8 @@ import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.sum import fr.acinq.lightning.utils.toMilliSatoshi import fr.acinq.phoenix.data.WalletPaymentMetadata -import kotlinx.datetime.Instant +import kotlin.time.ExperimentalTime +import kotlin.time.Instant class WalletPaymentCsvWriter(val configuration: Configuration) : CsvWriter() { @@ -99,6 +100,7 @@ class WalletPaymentCsvWriter(val configuration: Configuration) : CsvWriter() { val description: String? = null, ) + @OptIn(ExperimentalTime::class) private fun addRow( timestamp: Long, id: UUID, diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/DefaultOffer.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/DefaultOffer.kt deleted file mode 100644 index 7b09af6e7..000000000 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/DefaultOffer.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.data - -import fr.acinq.bitcoin.PrivateKey -import fr.acinq.lightning.wire.OfferTypes - -/** - * @param defaultOffer The default offer for a node. - * @param payerKey A private key attached to a node. It can be used to sign payments to offers of - * third parties and prove the origin of that payment. The recipient of that payment can then - * decide that this origin is trusted, and show/hide the `payerNote` attached to that payment. - * - */ -data class OfferData( - val defaultOffer: OfferTypes.Offer, - val payerKey: PrivateKey -) \ No newline at end of file diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/ExchangeRates.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/ExchangeRates.kt index ce6dfc734..04693cbf3 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/ExchangeRates.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/ExchangeRates.kt @@ -1,8 +1,5 @@ package fr.acinq.phoenix.data -import fr.acinq.phoenix.controllers.MVI -import fr.acinq.phoenix.controllers.main.Home -import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/LocalChannelInfo.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/LocalChannelInfo.kt index 7d70862fd..909ca6fea 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/LocalChannelInfo.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/LocalChannelInfo.kt @@ -46,8 +46,6 @@ data class LocalChannelInfo( val isTerminated by lazy { state.isTerminated() } /** True if the channel can be used to send/receive payments. */ val isUsable by lazy { state is Normal && !isBooting } - /** True if the channel is `LegacyWaitForFundingConfirmed`, i.e., it may be a zombie channel. */ - val isLegacyWait by lazy { state.isLegacyWait() } /** A string version of the state's class. */ val stateName by lazy { state.stateName } // FIXME: we should also expose the raw channel's balance, which is what should be used in the channel's details screen, rather than the "smart" spendable balance returned by `localBalance()` @@ -70,7 +68,7 @@ data class LocalChannelInfo( val commitmentsInfo: List by lazy { when (state) { is ChannelStateWithCommitments -> { - val params = state.commitments.params + val params = state.commitments.channelParams val changes = state.commitments.changes state.commitments.active.map { CommitmentInfo( @@ -88,7 +86,7 @@ data class LocalChannelInfo( val inactiveCommitmentsInfo: List by lazy { when (state) { is ChannelStateWithCommitments -> { - val params = state.commitments.params + val params = state.commitments.channelParams val changes = state.commitments.changes state.commitments.inactive.map { CommitmentInfo( @@ -110,7 +108,7 @@ data class LocalChannelInfo( buildSet { state.commitments.latest.localCommit.spec.htlcs.forEach { add(it.add.paymentHash) } state.commitments.latest.remoteCommit.spec.htlcs.forEach { add(it.add.paymentHash) } - state.commitments.latest.nextRemoteCommit?.commit?.spec?.htlcs?.forEach { add(it.add.paymentHash) } + state.commitments.latest.nextRemoteCommit?.spec?.htlcs?.forEach { add(it.add.paymentHash) } }.size } else -> 0 diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/contacts/SqliteContactsDb.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/contacts/SqliteContactsDb.kt index fb0cd886c..f32b5512b 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/contacts/SqliteContactsDb.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/contacts/SqliteContactsDb.kt @@ -138,7 +138,9 @@ class SqliteContactsDb( private fun contactIdForPayment(payment: WalletPayment, metadata: WalletPaymentMetadata?): UUID? { return if (payment is Bolt12IncomingPayment) { payment.incomingOfferMetadata()?.let { offerMetadata -> - contactIdForPayerPubKey(offerMetadata.payerKey) + offerMetadata.payerKey?.let { payerKey -> + contactIdForPayerPubKey(payerKey) + } } } else { metadata?.lightningAddress?.let { address -> diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/migrations/v11/queries/InboundLiquidityQueries.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/migrations/v11/queries/InboundLiquidityQueries.kt index 217b6c412..383ac3a32 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/migrations/v11/queries/InboundLiquidityQueries.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/migrations/v11/queries/InboundLiquidityQueries.kt @@ -66,8 +66,6 @@ object InboundLiquidityQueries { is Bolt12IncomingPayment -> incomingPayment.copy( liquidityPurchaseDetails = liquidityPurchaseDetails ) to incomingPayment.completedAt - - else -> null to null } val liquidityPayment = AutomaticLiquidityPurchasePayment( id = UUID.fromString(id), 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 141f9c313..1d3af423f 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 @@ -16,26 +16,22 @@ package fr.acinq.phoenix.managers -import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Chain import fr.acinq.bitcoin.PublicKey -import fr.acinq.bitcoin.Satoshi import fr.acinq.lightning.NodeParams import fr.acinq.lightning.NodeUri import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.payment.LiquidityPolicy import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat -import fr.acinq.lightning.wire.LiquidityAds import fr.acinq.phoenix.PhoenixBusiness import fr.acinq.phoenix.shared.BuildVersions import fr.acinq.lightning.logging.info -import fr.acinq.phoenix.data.OfferData +import fr.acinq.lightning.wire.OfferTypes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlin.time.Duration.Companion.hours class NodeParamsManager( @@ -81,11 +77,9 @@ class NodeParamsManager( } } - /** See [NodeParams.defaultOffer]. Returns an [OfferData] object. */ - suspend fun defaultOffer(): OfferData { - return nodeParams.filterNotNull().first().defaultOffer(trampolineNodeId).let { - OfferData(it.first, it.second) - } + /** See [NodeParams.defaultOffer]. Returns an [OfferTypes.OfferAndKey] object. */ + suspend fun defaultOffer(): OfferTypes.OfferAndKey { + return nodeParams.filterNotNull().first().defaultOffer(trampolineNodeId) } companion object { diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PaymentsPageFetcher.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PaymentsPageFetcher.kt index 94183a6bb..0531b9364 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PaymentsPageFetcher.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/PaymentsPageFetcher.kt @@ -6,9 +6,10 @@ import fr.acinq.phoenix.data.WalletPaymentInfo import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant +import kotlin.time.Clock +import kotlin.time.Instant import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime data class PaymentsPage( /** The offset value you passed to the `subscribeToX()` function. */ @@ -154,6 +155,7 @@ class PaymentsPageFetcher( resetSubscribeToRecentJob(subscriptionIdx) } + @OptIn(ExperimentalTime::class) private fun resetSubscribeToRecentJob(idx: Int) { log.debug { "resetSubscribeToRecentJob(idx=$idx)" } @@ -191,6 +193,7 @@ class PaymentsPageFetcher( } } + @OptIn(ExperimentalTime::class) private fun resetRefreshJob(idx: Int, rows: List) { log.debug { "resetRefreshJob(idx=$idx, rows=${rows.size})" } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/WalletManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/WalletManager.kt index d87bbc14f..5989ec77e 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/WalletManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/WalletManager.kt @@ -17,7 +17,7 @@ package fr.acinq.phoenix.managers import fr.acinq.bitcoin.* -import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.Bip84OnChainKeys import fr.acinq.lightning.crypto.LocalKeyManager import fr.acinq.lightning.crypto.div import kotlinx.coroutines.CoroutineScope @@ -100,4 +100,4 @@ fun LocalKeyManager.cloudKeyHash(): String { fun LocalKeyManager.isMainnet() = chain == Chain.Mainnet val LocalKeyManager.finalOnChainWalletPath: String - get() = (KeyManager.Bip84OnChainKeys.bip84BasePath(chain) / finalOnChainWallet.account).toString() + get() = (Bip84OnChainKeys.bip84BasePath(chain) / finalOnChainWallet.account).toString() diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/CurrencyManager.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/CurrencyManager.kt index 256649cb4..e0656b7a2 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/CurrencyManager.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/CurrencyManager.kt @@ -48,12 +48,13 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant +import kotlin.time.Clock +import kotlin.time.Instant import kotlin.collections.plus import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime /** * Manages the routines fetching the btc exchange rates. The frontend app must add fiat currencies they @@ -75,6 +76,7 @@ import kotlin.time.Duration.Companion.seconds * However, for the time being, we rely on the more liquid USD-FIAT exchange rates. * Thus, if we fetch both BTC-USD & USD-COP, we can easily convert between any of the 3 currencies. */ +@OptIn(ExperimentalTime::class) class CurrencyManager( loggerFactory: LoggerFactory, val appDb: SqliteAppDb, diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/BlockchainInfoApi.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/BlockchainInfoApi.kt index 9cdd32daa..589c195e8 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/BlockchainInfoApi.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/BlockchainInfoApi.kt @@ -18,13 +18,13 @@ package fr.acinq.phoenix.managers.global.fiatapis import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.logging.error +import fr.acinq.lightning.utils.currentTimestampMillis import fr.acinq.phoenix.data.BlockchainInfoResponse import fr.acinq.phoenix.data.ExchangeRate import fr.acinq.phoenix.data.FiatCurrency import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText -import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.minutes /** @@ -57,7 +57,7 @@ class BlockchainInfoApi(loggerFactory: LoggerFactory) : ExchangeRateApi { } } - val timestampMillis = Clock.System.now().toEpochMilliseconds() + val timestampMillis = currentTimestampMillis() val fetchedRates: List = parsedResponse?.let { targets.mapNotNull { fiatCurrency -> parsedResponse[fiatCurrency.name]?.let { priceObject -> diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/BluelyticsApi.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/BluelyticsApi.kt index 2b9849897..c6f4bf230 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/BluelyticsApi.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/BluelyticsApi.kt @@ -18,13 +18,13 @@ package fr.acinq.phoenix.managers.global.fiatapis import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.logging.error +import fr.acinq.lightning.utils.currentTimestampMillis import fr.acinq.phoenix.data.BluelyticsResponse import fr.acinq.phoenix.data.ExchangeRate import fr.acinq.phoenix.data.FiatCurrency import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText -import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.minutes /** @@ -56,7 +56,7 @@ class BluelyticsAPI(loggerFactory: LoggerFactory) : ExchangeRateApi { } } - val timestampMillis = Clock.System.now().toEpochMilliseconds() + val timestampMillis = currentTimestampMillis() val fetchedRates: List = parsedResponse?.let { targets.filter { it == FiatCurrency.ARS_BM }.map { ExchangeRate.UsdPriceRate( diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/CoinbaseApi.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/CoinbaseApi.kt index 3e842cb60..fbf8e3445 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/CoinbaseApi.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/CoinbaseApi.kt @@ -18,13 +18,13 @@ package fr.acinq.phoenix.managers.global.fiatapis import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.logging.error +import fr.acinq.lightning.utils.currentTimestampMillis import fr.acinq.phoenix.data.CoinbaseResponse import fr.acinq.phoenix.data.ExchangeRate import fr.acinq.phoenix.data.FiatCurrency import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText -import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.minutes /** @@ -38,7 +38,7 @@ class CoinbaseAPI(loggerFactory: LoggerFactory) : ExchangeRateApi { override val name = "coinbase" override val refreshDelay = 60.minutes override val fiatCurrencies = FiatCurrency.Companion.values.filter { - // bascially, everything except USD, EURO, and special markets + // basically, everything except USD, EURO, and special markets !ExchangeRateApi.highLiquidityMarkets.contains(it) && !ExchangeRateApi.specialMarkets.contains(it) && !ExchangeRateApi.missingFromCoinbase.contains(it) }.toSet() @@ -58,7 +58,7 @@ class CoinbaseAPI(loggerFactory: LoggerFactory) : ExchangeRateApi { } } - val timestampMillis = Clock.System.now().toEpochMilliseconds() + val timestampMillis = currentTimestampMillis() val fetchedRates: List = parsedResponse?.let { targets.mapNotNull { fiatCurrency -> parsedResponse.data.rates[fiatCurrency.name]?.let { valueAsString -> diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/YadioApi.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/YadioApi.kt index 9e743159f..0ad17dc82 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/YadioApi.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/global/fiatapis/YadioApi.kt @@ -18,13 +18,13 @@ package fr.acinq.phoenix.managers.global.fiatapis import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.logging.error +import fr.acinq.lightning.utils.currentTimestampMillis import fr.acinq.phoenix.data.ExchangeRate import fr.acinq.phoenix.data.FiatCurrency import fr.acinq.phoenix.data.YadioResponse import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.bodyAsText -import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.minutes /** @@ -57,7 +57,7 @@ class YadioAPI(loggerFactory: LoggerFactory) : ExchangeRateApi { } } - val timestampMillis = Clock.System.now().toEpochMilliseconds() + val timestampMillis = currentTimestampMillis() val fetchedRates: List = parsedResponse?.let { targets.mapNotNull { fiatCurrency -> val name = fiatCurrency.name.take(3) diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/channels/ChannelsImportHelper.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/channels/ChannelsImportHelper.kt deleted file mode 100644 index 800cc9ce2..000000000 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/channels/ChannelsImportHelper.kt +++ /dev/null @@ -1,79 +0,0 @@ -package fr.acinq.phoenix.utils.channels - -import fr.acinq.bitcoin.ByteVector -import fr.acinq.lightning.channel.states.PersistedChannelState -import fr.acinq.lightning.serialization.channel.Encryption.from -import fr.acinq.lightning.serialization.channel.Serialization -import fr.acinq.lightning.wire.EncryptedChannelData -import fr.acinq.phoenix.PhoenixBusiness -import fr.acinq.lightning.logging.error -import fr.acinq.lightning.logging.info -import fr.acinq.phoenix.db.SqliteChannelsDb -import fr.acinq.secp256k1.Hex -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first - - -object ChannelsImportHelper { - - suspend fun doImportChannels( - data: String, - biz: PhoenixBusiness, - ): ChannelsImportResult { - - val loggerFactory = biz.loggerFactory - val log = loggerFactory.newLogger(this::class) - try { - - log.info { "initiating channels-data import" } - - val nodeParams = biz.nodeParamsManager.nodeParams.filterNotNull().first() - val peer = biz.peerManager.getPeer() - - val encryptedChannelData = try { - EncryptedChannelData(ByteVector(Hex.decode(data))) - } catch(e: Exception) { - log.error(e) { "failed to deserialize data blob" } - return ChannelsImportResult.Failure.MalformedData - } - - return PersistedChannelState - .from(nodeParams.nodePrivateKey, encryptedChannelData) - .fold( - onFailure = { - log.error(it) { "failed to decrypt channel state" } - ChannelsImportResult.Failure.DecryptionError - }, - onSuccess = { - when (it) { - is Serialization.DeserializationResult.Success -> { - log.info { "successfully imported channel=${it.state.channelId}" } - peer.db.channels.addOrUpdateChannel(it.state) - val channel = (peer.db.channels as? SqliteChannelsDb)?.getChannel(it.state.channelId) - log.info { "channel added/updated to database, is_closed=${channel?.third}" } - ChannelsImportResult.Success(it.state) - } - is Serialization.DeserializationResult.UnknownVersion -> { - log.error { "cannot use channel state: unknown version=${it.version}" } - ChannelsImportResult.Failure.UnknownVersion(it.version) - } - } - } - ) - - } catch (e: Exception) { - log.error(e) { "error when importing channels" } - return ChannelsImportResult.Failure.Generic(e) - } - } -} - -sealed class ChannelsImportResult { - data class Success(val channel: PersistedChannelState) : ChannelsImportResult() - sealed class Failure : ChannelsImportResult() { - data class Generic(val error: Throwable) : Failure() - data class UnknownVersion(val version: Int) : Failure() - object MalformedData : Failure() - object DecryptionError : Failure() - } -} \ No newline at end of file diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/channels/SpendChannelAddressHelper.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/channels/SpendChannelAddressHelper.kt index 2792ff81f..809d53370 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/channels/SpendChannelAddressHelper.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/channels/SpendChannelAddressHelper.kt @@ -19,6 +19,7 @@ package fr.acinq.phoenix.utils.channels import fr.acinq.bitcoin.ByteVector import fr.acinq.bitcoin.ByteVector64 import fr.acinq.bitcoin.Crypto +import fr.acinq.bitcoin.KeyPath import fr.acinq.bitcoin.PublicKey import fr.acinq.bitcoin.Satoshi import fr.acinq.bitcoin.SigHash @@ -34,7 +35,6 @@ import fr.acinq.lightning.serialization.channel.Encryption.from import fr.acinq.lightning.serialization.channel.Serialization import fr.acinq.lightning.transactions.Scripts import fr.acinq.lightning.transactions.Transactions -import fr.acinq.lightning.wire.EncryptedChannelData import fr.acinq.phoenix.PhoenixBusiness import fr.acinq.secp256k1.Hex import kotlinx.coroutines.flow.filterNotNull @@ -56,21 +56,13 @@ object SpendChannelAddressHelper { business: PhoenixBusiness, amount: Satoshi, fundingTxIndex: Long, - channelData: String, + channelKeyPath: String, remoteFundingPubkey: String, unsignedTx: String ): SpendChannelAddressResult { val loggerFactory = business.loggerFactory val log = loggerFactory.newLogger(this::class) val peer = business.peerManager.getPeer() - val nodeParams = business.nodeParamsManager.nodeParams.filterNotNull().first() - - val deserializedChannelData = try { - EncryptedChannelData(ByteVector(Hex.decode(channelData))) - } catch(e: Exception) { - log.error(e) { "failed to deserialize channels-data blob" } - return SpendChannelAddressResult.Failure.ChannelDataMalformed - } val tx = try { Transaction.read(unsignedTx) @@ -86,45 +78,35 @@ object SpendChannelAddressHelper { return SpendChannelAddressResult.Failure.RemoteFundingPubkeyMalformed(e.message ?: e::class.simpleName.toString()) } - try { - val channelKeyPath = PersistedChannelState.from(nodeParams.nodePrivateKey, deserializedChannelData) - .fold( - onFailure = { - log.error { "failed to decrypt channel" } - return SpendChannelAddressResult.Failure.ChannelDataDecryption - }, - onSuccess = { - when (it) { - is Serialization.DeserializationResult.Success -> { - when (val s = it.state) { - is ChannelStateWithCommitments -> s.commitments.params.localParams.fundingKeyPath - else -> { - log.error { "unhandled channel data state: ${it::class.simpleName}" } - return SpendChannelAddressResult.Failure.ChannelDataUnhandledState(it::class.simpleName) - } - } - } - is Serialization.DeserializationResult.UnknownVersion -> { - log.error { "unhandled channel data version: ${it.version}" } - return SpendChannelAddressResult.Failure.ChannelDataUnhandledVersion(it.version) - } - } - } - ) + val channelKeys = try { + peer.nodeParams.keyManager.channelKeys(KeyPath(channelKeyPath)) + } catch (e: Exception) { + log.error(e) { "failed to read channel keypath=$channelKeyPath" } + return SpendChannelAddressResult.Failure.InvalidChannelKeyPath + } - val channelKeys = peer.nodeParams.keyManager.channelKeys(channelKeyPath) + try { val localFundingKey = channelKeys.fundingKey(fundingTxIndex) val fundingScript = Scripts.multiSig2of2(localFundingKey.publicKey(), pubkey) - val sig = Transactions.sign(tx = tx, inputIndex = 0, Script.write(fundingScript), amount, localFundingKey) + val sig = TODO() //Transactions.sign(tx = tx, inputIndex = 0, Script.write(fundingScript), amount, localFundingKey) val signedData = tx.hashForSigning(0, Script.write(fundingScript), SigHash.SIGHASH_ALL, amount, SigVersion.SIGVERSION_WITNESS_V0) - return if (!Crypto.verifySignature(signedData, sig, localFundingKey.publicKey())) { + + try { + val verifySig = Crypto.verifySignature(signedData, sig, localFundingKey.publicKey()) + if (!verifySig) { + log.error { "signature not verified" } + SpendChannelAddressResult.Failure.InvalidSig(tx.txid, localFundingKey.publicKey(), Script.write(fundingScript).byteVector(), sig) + } else { + SpendChannelAddressResult.Success(tx.txid, localFundingKey.publicKey(), Script.write(fundingScript).byteVector(), sig) + } + } catch (e: Exception) { + log.error(e) { "failed to verify signature" } SpendChannelAddressResult.Failure.InvalidSig(tx.txid, localFundingKey.publicKey(), Script.write(fundingScript).byteVector(), sig) - } else { - SpendChannelAddressResult.Success(tx.txid, localFundingKey.publicKey(), Script.write(fundingScript).byteVector(), sig) } + } catch (e: Exception) { - log.error { "error when spending from channel address: ${e.message}" } + log.error(e) { "failed to sign transaction" } return SpendChannelAddressResult.Failure.Generic(e) } } @@ -134,10 +116,7 @@ sealed class SpendChannelAddressResult { data class Success(val txId: TxId, val publicKey: PublicKey, val fundingScript: ByteVector, val signature: ByteVector64) : SpendChannelAddressResult() sealed class Failure : SpendChannelAddressResult() { data class Generic(val error: Throwable) : Failure() - data object ChannelDataMalformed : Failure() - data object ChannelDataDecryption : Failure() - data class ChannelDataUnhandledState(val stateName: String?) : Failure() - data class ChannelDataUnhandledVersion(val version: Int) : Failure() + data object InvalidChannelKeyPath : Failure() data class TransactionMalformed(val details: String) : Failure() data class RemoteFundingPubkeyMalformed(val details: String) : Failure() data class InvalidSig(val txId: TxId, val publicKey: PublicKey, val fundingScript: ByteVector, val signature: ByteVector64) : Failure() diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ChannelExtensions.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ChannelExtensions.kt index 357f787d9..2843d0137 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ChannelExtensions.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ChannelExtensions.kt @@ -31,18 +31,10 @@ fun ChannelState.isTerminated(): Boolean { } } -fun ChannelState.isLegacyWait(): Boolean { - return this is LegacyWaitForFundingConfirmed - || (this is Offline && this.state is LegacyWaitForFundingConfirmed) - || (this is Syncing && this.state is LegacyWaitForFundingConfirmed) -} - fun ChannelState.isBeingCreated(): Boolean { return when (this) { is Syncing -> state.isBeingCreated() is Offline -> state.isBeingCreated() - is LegacyWaitForFundingLocked, - is LegacyWaitForFundingConfirmed, is WaitForAcceptChannel, is WaitForChannelReady, is WaitForFundingConfirmed, @@ -70,8 +62,6 @@ fun ChannelState.localBalance(): MilliSatoshi? { is Aborted -> null // balance is unknown is Negotiating -> null - is LegacyWaitForFundingLocked -> null - is LegacyWaitForFundingConfirmed -> null is WaitForAcceptChannel -> null is WaitForChannelReady -> null is WaitForFundingConfirmed -> null @@ -82,6 +72,5 @@ fun ChannelState.localBalance(): MilliSatoshi? { is WaitForRemotePublishFutureCommitment -> null // regular case is ChannelStateWithCommitments -> commitments.availableBalanceForSend() - else -> null } } 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 3a238dd51..e93249ee3 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 @@ -72,5 +72,5 @@ fun WalletPayment.errorMessage(): String? = when (this) { is IncomingPayment -> null } -fun WalletPayment.incomingOfferMetadata(): OfferPaymentMetadata.V1? = (this as? Bolt12IncomingPayment)?.metadata as? OfferPaymentMetadata.V1 +fun WalletPayment.incomingOfferMetadata(): OfferPaymentMetadata? = (this as? Bolt12IncomingPayment)?.metadata fun WalletPayment.outgoingInvoiceRequest(): OfferTypes.InvoiceRequest? = ((this as? LightningOutgoingPayment)?.details as? LightningOutgoingPayment.Details.Blinded)?.paymentRequest?.invoiceRequest 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 e03cd038b..e3c94dea5 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 @@ -16,13 +16,12 @@ package fr.acinq.phoenix.utils.extensions -import fr.acinq.lightning.Feature +import fr.acinq.bitcoin.PublicKey import fr.acinq.lightning.payment.Bolt11Invoice import fr.acinq.lightning.payment.Bolt12Invoice import fr.acinq.lightning.payment.OfferPaymentMetadata import fr.acinq.lightning.payment.PaymentRequest -fun Bolt11Invoice.isAmountlessTrampoline() = this.amount == null && this.features.hasFeature(Feature.TrampolinePayment) /** * In Objective-C, the function name `description()` is already in use (part of NSObject). @@ -36,8 +35,8 @@ val PaymentRequest.desc: String? is Bolt12Invoice -> this.description } -val OfferPaymentMetadata.payerNote: String? - get() = when { - this is OfferPaymentMetadata.V1 -> this.payerNote - else -> null - } \ No newline at end of file +val OfferPaymentMetadata.description: String? + get() = when (this) { + is OfferPaymentMetadata.V1 -> null + is OfferPaymentMetadata.V2 -> this.description + } diff --git a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/migrations/IosMigrationHelper.kt b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/migrations/IosMigrationHelper.kt index f2a49484c..34141b068 100644 --- a/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/migrations/IosMigrationHelper.kt +++ b/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/migrations/IosMigrationHelper.kt @@ -59,7 +59,7 @@ object IosMigrationHelper { private fun ChannelState.isLegacy(): Boolean { return this is ChannelStateWithCommitments && this !is ShuttingDown && this !is Negotiating && this !is Closing && this !is Closed - && !this.commitments.params.channelFeatures.hasFeature(Feature.DualFunding) + && !this.commitments.channelParams.channelFeatures.hasFeature(Feature.DualFunding) } /** 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 892f7e59c..65f0ff65c 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 @@ -28,11 +28,9 @@ 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.db.IncomingPayment +import fr.acinq.lightning.crypto.SwapInOnChainKeys import fr.acinq.lightning.db.LightningOutgoingPayment import fr.acinq.lightning.io.NativeSocketException -import fr.acinq.lightning.io.OfferNotPaid import fr.acinq.lightning.io.PaymentNotSent import fr.acinq.lightning.io.PaymentProgress import fr.acinq.lightning.io.PaymentSent @@ -50,11 +48,9 @@ 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.SendManager import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import platform.Foundation.NSData -import kotlin.time.Duration.Companion.seconds /** * Class types from lightning-kmp & bitcoin-kmp are not exported to iOS unless we explicitly @@ -340,7 +336,7 @@ fun ByteArray_toNSDataSlice(buffer: ByteArray, offset: Int, length: Int): NSData fun ByteArray_toNSData(buffer: ByteArray): NSData = buffer.toNSData() fun WalletState.WalletWithConfirmations._spendExpiredSwapIn( - swapInKeys: KeyManager.SwapInOnChainKeys, + swapInKeys: SwapInOnChainKeys, scriptPubKey: ByteVector, feerate: FeeratePerKw ): Pair? { @@ -358,7 +354,7 @@ fun OfferManager.Companion._deterministicOffer( amount: MilliSatoshi?, description: String?, pathId: ByteVector32?, -): Pair { +): OfferTypes.OfferAndKey { return deterministicOffer( chainHash = chainHash, nodePrivateKey = nodePrivateKey,