From cd82d7ffff27b2fcc6277b6b7300a403e49ad8d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 21:34:55 +0000 Subject: [PATCH] feat(wallet): prefer NIP-44 for NWC and notify non-zap payments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NIP-44 encryption preference: NwcSignerState now fetches each wallet's kind 13194 info event (via the relay client, injected by Account), caches whether it advertises `nip44_v2`, and sends pay/RPC requests with NIP-44 when supported — satisfying NIP-47's "client should always prefer nip44 if supported by the wallet service". Falls back to NIP-04 (the legacy default) when unknown or unsupported; response decryption already auto-detects the scheme. Non-zap payment notifications: NwcPaymentNotificationWatcher keeps a standing subscription to each connected wallet's NIP-47 notification stream (kind 23197/23196) on the wallet relay, decrypts payment_received events, and posts a tray notification on a new Payments Received channel. Payments carrying a NIP-57 zap request are skipped, since those already surface through the kind-9735 ZapNotification path — so only plain, non-zap incoming payments are announced. Deep-link pairing: the "Connect wallet via app" button now builds its `nostrnwc://connect` URI through the tested quartz Nip47DeepLink helper instead of a hardcoded percent-encoded string. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RDAAS4ktFbWtRnEVXQsjfs --- .../com/vitorpamplona/amethyst/AppModules.kt | 11 ++ .../vitorpamplona/amethyst/model/Account.kt | 18 ++- .../nip47WalletConnect/NwcSignerState.kt | 64 ++++++++- .../notifications/NotificationCategory.kt | 13 ++ .../NwcPaymentNotificationWatcher.kt | 128 ++++++++++++++++++ .../renderers/NwcPaymentNotifier.kt | 78 +++++++++++ .../loggedIn/wallet/AddNwcWalletScreen.kt | 7 +- amethyst/src/main/res/values/strings.xml | 6 + 8 files changed, 321 insertions(+), 4 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/NwcPaymentNotifier.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index dee33b33c8..b799d3b5ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.service.images.ThumbnailDiskCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.notifications.AlwaysOnNotificationServiceManager import com.vitorpamplona.amethyst.service.notifications.NotificationDispatcher +import com.vitorpamplona.amethyst.service.notifications.NwcPaymentNotificationWatcher import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManagerForRelays @@ -882,6 +883,16 @@ class AppModules( scope = applicationIOScope, ) + // Surfaces non-zap Lightning payments reported by the logged-in account's NWC + // wallet(s) as tray notifications (zaps are already shown via ZapNotification). + val nwcPaymentNotificationWatcher = + NwcPaymentNotificationWatcher( + context = appContext, + client = client, + scope = applicationIOScope, + accountFlow = sessionManager.accountContent.map { (it as? AccountState.LoggedIn)?.account }, + ).also { it.start() } + fun subscribedFlow( address: Address, account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index a5102f3d39..d591c36714 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -291,6 +291,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent @@ -470,7 +471,22 @@ class Account( // account never surfaces under another (the old cache was a process-wide singleton). val relayNotifications = NotifyRequestsCache() - override val nip47SignerState = NwcSignerState(signer, nwcFilterAssembler, cache, scope, settings) + override val nip47SignerState = + NwcSignerState( + signer, + nwcFilterAssembler, + cache, + scope, + settings, + fetchInfoEvent = { uri -> + // Fetch the wallet's kind 13194 info event so NwcSignerState can prefer + // NIP-44 when the wallet advertises it (NIP-47 encryption negotiation). + client.fetchFirst( + uri.relayUri, + Filter(kinds = listOf(NwcInfoEvent.KIND), authors = listOf(uri.pubKeyHex), limit = 1), + ) as? NwcInfoEvent + }, + ) val nip65RelayList = Nip65RelayListState(signer, cache, scope, settings) val localRelayList = LocalRelayListState(signer, cache, scope, settings) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt index 8b06eee55b..e11405c3dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -37,18 +37,24 @@ import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectReque import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectResponseCache import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap /** * Manages NIP-47 (Nostr Wallet Connect) related signing operations and decryption cache for a given account. @@ -60,6 +66,12 @@ class NwcSignerState( val cache: LocalCache, val scope: CoroutineScope, val settings: AccountSettings, + /** + * Fetches a wallet's kind 13194 info event so we can negotiate encryption. + * Injected by [com.vitorpamplona.amethyst.model.Account] (which owns the relay + * client). Null in tests / when unavailable — requests then fall back to NIP-04. + */ + val fetchInfoEvent: (suspend (Nip47WalletConnect.Nip47URINorm) -> NwcInfoEvent?)? = null, ) : INwcSignerState { /** * Flow of the default wallet's NWC URI, derived from multi-wallet settings. @@ -105,6 +117,54 @@ class NwcSignerState( NostrSignerInternal(KeyPair(it)) } + /** + * Per-wallet (keyed by wallet service pubkey) cache of whether the wallet + * advertises NIP-44 (`nip44_v2`) support in its kind 13194 info event. + * NIP-47 says a client "should always prefer nip44 if supported by the wallet + * service"; absent/unknown means we keep the NIP-04 legacy default. + */ + private val nip44SupportByWallet = ConcurrentHashMap() + + init { + // Warm the encryption preference in the background whenever the default + // wallet changes so the payment hot path can read it without blocking. + scope.launch(Dispatchers.IO) { + defaultWalletUri + .filterNotNull() + .distinctUntilChanged { a, b -> a.pubKeyHex == b.pubKeyHex && a.relayUri == b.relayUri } + .collectLatest { warmEncryptionPreference(it) } + } + } + + /** + * Fetches the wallet's info event once and records whether it supports NIP-44. + * Best-effort: any failure leaves the wallet on the NIP-04 fallback. + */ + private suspend fun warmEncryptionPreference(uri: Nip47WalletConnect.Nip47URINorm) { + val fetch = fetchInfoEvent ?: return + if (nip44SupportByWallet.containsKey(uri.pubKeyHex)) return + + val supports = + try { + fetch(uri)?.encryptionSchemes()?.any { it.equals("nip44_v2", ignoreCase = true) } == true + } catch (e: Exception) { + if (e is CancellationException) throw e + false + } + + nip44SupportByWallet[uri.pubKeyHex] = supports + } + + /** + * Non-blocking read of the negotiated encryption preference for a wallet. + * Returns true only once the info event has been fetched and advertised + * `nip44_v2`; otherwise NIP-04 (the legacy default). + */ + private fun prefersNip44(uri: Nip47WalletConnect.Nip47URINorm?): Boolean { + uri ?: return false + return nip44SupportByWallet[uri.pubKeyHex] ?: false + } + fun hasWalletConnectSetup(): Boolean = settings.nwcWallets.value.isNotEmpty() override fun isNIP47Author(pubKey: HexKey?): Boolean = nip47Signer.value.pubKey == pubKey @@ -138,7 +198,7 @@ class NwcSignerState( val walletService = walletUri ?: throw IllegalArgumentException("No NIP47 setup") val walletSigner = buildSigner(walletService) ?: signer - val event = LnZapPaymentRequestEvent.createRequest(request, walletService.pubKeyHex, walletSigner) + val event = LnZapPaymentRequestEvent.createRequest(request, walletService.pubKeyHex, walletSigner, useNip44 = prefersNip44(walletService)) val filter = NWCPaymentQueryState( @@ -184,7 +244,7 @@ class NwcSignerState( ): Pair { val walletService = defaultWalletUri.value ?: throw IllegalArgumentException("No NIP47 setup") - val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, nip47Signer.value) + val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, nip47Signer.value, useNip44 = prefersNip44(walletService)) val filter = NWCPaymentQueryState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationCategory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationCategory.kt index 83c09331d3..e7b476fc49 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationCategory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationCategory.kt @@ -226,6 +226,19 @@ enum class NotificationCategory( group = "com.vitorpamplona.amethyst.CHESS_NOTIFICATION", summaryId = 0x30000, ), + PAYMENT_RECEIVED( + channelIdRes = R.string.app_notification_payments_channel_id, + channelNameRes = R.string.app_notification_payments_channel_name, + channelDescriptionRes = R.string.app_notification_payments_channel_description, + summaryTextRes = R.string.app_notification_payments_summary, + importance = NotificationManager.IMPORTANCE_DEFAULT, + color = 0xFF16B979.toInt(), // lightning green — distinct from the gold zap channel + smallIcon = R.drawable.ic_notif_zap, + settingsIcon = MaterialSymbols.AccountBalanceWallet, + channelGroup = NotifChannelGroup.PAYMENTS, + group = "com.vitorpamplona.amethyst.PAYMENT_RECEIVED_NOTIFICATION", + summaryId = 0xC0000, + ), ; fun channelId(context: Context): String = stringRes(context, channelIdRes) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher.kt new file mode 100644 index 0000000000..4467af7d04 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.notifications + +import android.content.Context +import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntryNorm +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.service.notifications.renderers.NwcPaymentNotifier +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.subscribeAsFlow +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent +import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentReceivedNotification +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +/** + * Always-on watcher that turns NIP-47 wallet notifications (kind 23197/23196) into + * user-facing tray notifications for the logged-in account. + * + * For each configured NWC wallet it opens a standing subscription on that wallet's + * own relay, filtered to notifications `p`-tagged to our client pubkey. Each event + * is decrypted with the per-wallet connection secret and, when it is an incoming + * `payment_received`, posted via [NwcPaymentNotifier] — **unless** the transaction + * metadata carries a NIP-57 zap request, in which case it is dropped: zaps already + * surface through the kind-9735 `ZapNotification` path, so notifying here would + * double up. + * + * Only new notifications (`since` = watch start) are surfaced, so relaunching the + * app never replays old payments as fresh alerts; a per-subscription id set + * de-duplicates re-delivery across relay reconnects. + */ +class NwcPaymentNotificationWatcher( + private val context: Context, + private val client: INostrClient, + private val scope: CoroutineScope, + private val accountFlow: Flow, +) { + fun start() { + scope.launch(Dispatchers.IO) { + accountFlow + .distinctUntilChanged { a, b -> a?.signer?.pubKey == b?.signer?.pubKey } + .collectLatest { account -> + account ?: return@collectLatest + account.settings.nwcWallets.collectLatest { wallets -> + watchWallets(account, wallets) + } + } + } + } + + private suspend fun watchWallets( + account: Account, + wallets: List, + ) = coroutineScope { + wallets.forEach { wallet -> + val signer = account.nip47SignerState.buildSigner(wallet.uri) ?: return@forEach + + val filter = + Filter( + kinds = listOf(NwcNotificationEvent.KIND, NwcNotificationEvent.LEGACY_KIND), + authors = listOf(wallet.uri.pubKeyHex), + tags = mapOf("p" to listOf(signer.pubKey)), + since = TimeUtils.now(), + ) + + launch(Dispatchers.IO) { + val seen = HashSet() + client.subscribeAsFlow(wallet.uri.relayUri, filter).collect { events -> + events.forEach { event -> + val notification = event as? NwcNotificationEvent ?: return@forEach + if (seen.add(notification.id)) { + handle(account, notification, signer) + } + } + } + } + } + } + + private suspend fun handle( + account: Account, + event: NwcNotificationEvent, + signer: NostrSigner, + ) { + val notification = + try { + event.decryptNotification(signer) + } catch (e: Exception) { + if (e is CancellationException) throw e + return + } + + val tx = (notification as? PaymentReceivedNotification)?.notification ?: return + + // A payment carrying a NIP-57 zap request is already shown by ZapNotification. + if (tx.parsedMetadata()?.nostr != null) return + + NwcPaymentNotifier.notify(context, account, tx) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/NwcPaymentNotifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/NwcPaymentNotifier.kt new file mode 100644 index 0000000000..215417f7ac --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/NwcPaymentNotifier.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.notifications.renderers + +import android.content.Context +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.service.notifications.NotificationCategory +import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes +import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard +import com.vitorpamplona.amethyst.service.notifications.notificationManager +import com.vitorpamplona.amethyst.ui.note.showAmount +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * Posts a tray notification for an incoming Lightning payment reported by the + * connected NWC wallet (NIP-47 `payment_received`). Renders on the green Payments + * channel with a wallet icon; the title leads with the amount. + * + * Zaps are intentionally NOT routed here — a `payment_received` whose transaction + * metadata carries a NIP-57 zap request is filtered out upstream by + * [com.vitorpamplona.amethyst.service.notifications.NwcPaymentNotificationWatcher], + * because those already surface through the kind-9735 [ZapNotification] path. + */ +object NwcPaymentNotifier { + suspend fun notify( + context: Context, + account: Account, + tx: NwcTransaction, + ) { + val nm = context.notificationManager() + if (!nm.areNotificationsEnabled()) return + + val msats = tx.amount ?: return + val amount = showAmount((msats / 1000L).toBigDecimal()) + + val id = tx.payment_hash ?: tx.invoice ?: tx.created_at?.toString() ?: return + val time = tx.settled_at ?: tx.created_at ?: TimeUtils.now() + + val title = stringRes(context, R.string.app_notification_payments_channel_message, amount) + val comment = (tx.parsedMetadata()?.comment ?: tx.description)?.ifBlank { null } + val body = comment ?: title + + val accountNpub = NotificationRoutes.accountNpub(account) + val uri = NotificationRoutes.notificationsUri(accountNpub, id) + + nm.postStandard( + category = NotificationCategory.PAYMENT_RECEIVED, + id = id, + messageTitle = title, + messageBody = body, + time = time, + pictureUrl = null, + uri = uri, + applicationContext = context, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt index 73004d6428..57957d568e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt @@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.Size24Modifier +import com.vitorpamplona.quartz.nip47WalletConnect.Nip47DeepLink import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch @@ -137,7 +138,11 @@ fun AddNwcWalletScreen( onClick = { try { uri.openUri( - "nostrnwc://connect?appname=Amethyst&appicon=https%3A%2F%2Fraw.githubusercontent.com%2Fvitorpamplona%2Famethyst%2Frefs%2Fheads%2Fmain%2Ficon.png&callback=amethyst%2Bwalletconnect%3A%2F%2Fdlnwc", + Nip47DeepLink.buildConnectUri( + callback = "amethyst+walletconnect://dlnwc", + appName = "Amethyst", + appIcon = "https://raw.githubusercontent.com/vitorpamplona/amethyst/refs/heads/main/icon.png", + ), ) } catch (_: IllegalArgumentException) { accountViewModel.toastManager.toast( diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 2052e7b286..a6cfcf2618 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1858,6 +1858,12 @@ From %1$s for %1$s + PaymentsReceivedID + Payments Received + Notifies you when your connected wallet receives a payment that is not a Nostr zap + Received %1$s sats + New payments + Reply Mark Read Me