From 14e1d3fa8f5a23662dc15a6cd22fe240c0a2ea86 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 18:21:48 +0000 Subject: [PATCH] feat: move NIP-A3 payment targets out of experimental, follow the merged spec NIP-A3 is merged upstream, so quartz's experimental/nipA3 package becomes nipA3PaymentTargets (imports updated in every module). - PaymentTargetTag lowercases the type on parse and assemble (the spec says the type is always lowercase). - The spec's Cash App type is `cashme`; `cashapp` stays as an alias of it, and a bare cashtag gets its $ in the cash.app link. - URI hand-off for the spec's listed types: nano: for nano, bitcoin:?sp= for bip352 silent payments (BIP-321), revolut.me for revolut, on top of the existing schemes; bip353 and unknown types fall back to payto://type/addr. - The payment targets editor stores the spec spelling of known aliases. - Profile chips get styles for cashme, nano, revolut, bip352 and bip353. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MGR1u8SyzcUuekub39SBsc --- .../nostr-expert/references/nip-catalog.md | 2 +- .../amethyst/LocalPreferences.kt | 2 +- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/AccountSettings.kt | 2 +- .../NipA3PaymentTargetsState.kt | 4 +- .../amethyst/model/zap/RailCapability.kt | 2 +- .../service/payments/PayToAppAvailability.kt | 2 +- .../FilterAccountInfoAndListsFromKey.kt | 2 +- .../paymentTargets/PaymentTargetsScreen.kt | 2 +- .../paymentTargets/PaymentTargetsViewModel.kt | 6 +- .../amethyst/ui/note/ReactionsRow.kt | 4 +- .../loggedIn/backups/BackupConflictCards.kt | 2 +- .../backups/BackupConflictListViews.kt | 6 +- .../backups/BackupConflictPresentation.kt | 2 +- .../profile/header/DisplayPaymentTargets.kt | 18 ++++-- .../profile/header/PaymentTargetsDialog.kt | 2 +- .../profile/header/ProfilePaymentRailChips.kt | 4 +- .../screen/loggedIn/relays/KindDisplayName.kt | 2 +- .../loggedIn/settings/NIP47SetupScreen.kt | 2 +- .../amethyst/commons/model/User.kt | 4 +- .../backups/ReplaceableBackupConflict.kt | 2 +- .../commons/model/cache/EventCache.kt | 2 +- .../model/payments/PayToRailMatcher.kt | 2 +- .../model/payments/PaymentTargetTypes.kt | 34 ++++++++++- .../profile/FilterUserProfileLists.kt | 2 +- .../user/watchers/FilterUserMetadataForKey.kt | 2 +- .../model/payments/PaymentTargetTypesTest.kt | 38 +++++++++++- .../vitorpamplona/quartz/kinds/KindNames.kt | 2 +- .../PaymentTarget.kt | 2 +- .../PaymentTargetTag.kt | 8 ++- .../PaymentTargetsDiff.kt | 2 +- .../PaymentTargetsEvent.kt | 2 +- .../quartz/utils/EventFactory.kt | 2 +- .../PaymentTargetTagTest.kt | 61 +++++++++++++++++++ 34 files changed, 186 insertions(+), 47 deletions(-) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{experimental/nipA3 => nipA3PaymentTargets}/PaymentTarget.kt (95%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{experimental/nipA3 => nipA3PaymentTargets}/PaymentTargetTag.kt (85%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{experimental/nipA3 => nipA3PaymentTargets}/PaymentTargetsDiff.kt (96%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{experimental/nipA3 => nipA3PaymentTargets}/PaymentTargetsEvent.kt (98%) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetTagTest.kt diff --git a/.claude/skills/nostr-expert/references/nip-catalog.md b/.claude/skills/nostr-expert/references/nip-catalog.md index 996addba10..88b1488950 100644 --- a/.claude/skills/nostr-expert/references/nip-catalog.md +++ b/.claude/skills/nostr-expert/references/nip-catalog.md @@ -88,6 +88,7 @@ ent for NIP-04) | | 96 | `nip96FileStorage/` | HTTP file storage | HTTP-based file storage | | 99 | `nip99Classifieds/` | ClassifiedsEvent.kt | Classifieds/marketplace (kind 30402) | | A0 | `nipA0VoiceMessages/` | Voice messages | Voice message events | +| A3 | `nipA3PaymentTargets/` | PaymentTargetsEvent.kt | Payment targets (`payto` tags, kind 10133) | | B7 | `nipB7Blossom/` | Blossom server URLs | Blossom file storage | ### Web/Storage/Other @@ -127,7 +128,6 @@ Located at `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ | `limits/` | Limit enforcement | | `medical/` | Medical data | | `nip95/` | File storage support | -| `nipA3/` | A3 protocol extension | | `nns/` | Nostr Name System | | `profileGallery/` | Profile gallery lists | | `publicMessages/` | Public message lists | diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 256a27ed35..707167a37c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -67,7 +67,6 @@ import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -98,6 +97,7 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils 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 2412c5adf3..5f837dbb83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -199,7 +199,6 @@ import com.vitorpamplona.quartz.experimental.nip95.header.dimension import com.vitorpamplona.quartz.experimental.nip95.header.fileSize import com.vitorpamplona.quartz.experimental.nip95.header.hash import com.vitorpamplona.quartz.experimental.nip95.header.mimeType -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.profileGallery.blurhash import com.vitorpamplona.quartz.experimental.profileGallery.dimension @@ -337,6 +336,7 @@ import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent import com.vitorpamplona.quartz.nipC7Chats.ChatEvent import com.vitorpamplona.quartz.utils.DualCase diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 81932f49de..fbf80b4a67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -50,7 +50,6 @@ import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event @@ -84,6 +83,7 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt index b2012e8794..fc58597571 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.model.nipA3PaymentTargets import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/zap/RailCapability.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/zap/RailCapability.kt index 286ded391f..993c9e1355 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/zap/RailCapability.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/zap/RailCapability.kt @@ -28,12 +28,12 @@ import com.vitorpamplona.amethyst.commons.model.payments.PaymentTargetTypes import com.vitorpamplona.amethyst.model.MIN_ONCHAIN_ZAP_SATS import com.vitorpamplona.amethyst.model.nip60Cashu.CashuWalletState import com.vitorpamplona.amethyst.service.payments.PayToAppAvailability -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip57Zaps.splits.BaseZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget /** * What payment rails would actually reach the recipient(s) of a zap. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/payments/PayToAppAvailability.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/payments/PayToAppAvailability.kt index 4970e9aec4..8657b94992 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/payments/PayToAppAvailability.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/payments/PayToAppAvailability.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.core.graphics.drawable.toBitmap import androidx.core.net.toUri import com.vitorpamplona.amethyst.commons.model.payments.PaymentTargetTypes -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index a4403f5659..d4c69458f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState.Companion.APP_SPECIFIC_DATA_D_TAG import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent @@ -53,6 +52,7 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsScreen.kt index 8aacd82061..3be6faab17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsScreen.kt @@ -74,7 +74,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget @Composable fun PaymentTargetsScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt index 362df0c9cc..4192f2599a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt @@ -23,9 +23,10 @@ package com.vitorpamplona.amethyst.ui.actions.paymentTargets import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.payments.PaymentTargetTypes import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -62,7 +63,8 @@ class PaymentTargetsViewModel : ViewModel() { type: String, authority: String, ) { - val trimmedType = type.trim().lowercase() + // Stores the NIP-A3 spelling of known aliases (btc -> bitcoin, cashapp -> cashme). + val trimmedType = PaymentTargetTypes.canonical(type) val trimmedAuthority = authority.trim() if (trimmedType.isEmpty() || trimmedAuthority.isEmpty()) return val target = PaymentTarget(trimmedType, trimmedAuthority) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index bc90957b86..ea9b705ab1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -238,8 +238,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.PaymentTarge import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.paymentTargetStyleFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.navigateToReloadMint -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -248,6 +246,8 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt index 942c0c7827..4091812db7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt @@ -100,7 +100,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsDiff import com.vitorpamplona.quartz.nip01Core.diff.ContentChange import com.vitorpamplona.quartz.nip01Core.diff.EventDiff import com.vitorpamplona.quartz.nip01Core.metadata.MetadataDiff @@ -118,6 +117,7 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListDiff import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListDiff import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataDiff import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListDiff +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsDiff import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListDiff import org.jetbrains.compose.resources.PluralStringResource import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt index 9be726a715..fc4184018b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt @@ -127,9 +127,6 @@ import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsDiff -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event @@ -161,6 +158,9 @@ import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListDiff import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsDiff +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListDiff import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent import org.jetbrains.compose.resources.PluralStringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt index ab29c78d75..9980be0f5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt @@ -118,7 +118,6 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsDiff import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.diff.ContentChange @@ -149,6 +148,7 @@ import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListDiff import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataDiff import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListDiff +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsDiff import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListDiff import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayPaymentTargets.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayPaymentTargets.kt index 142417dafe..4f65d19674 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayPaymentTargets.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayPaymentTargets.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.ProfilePaymentMethod -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.SegwitAddress fun isLightningPaymentTarget(rawType: String): Boolean = rawType.trim().lowercase() in PaymentTargetTypes.LIGHTNING_TYPES @@ -72,7 +72,7 @@ fun inAppPaymentRouteFor( /** * The URI an external wallet app should receive for [target]: the type's own - * scheme (`bitcoin:`, `lightning:`, `https://cash.app/…`) and RFC 8905 + * scheme (`bitcoin:`, `lightning:`, `https://cash.app/$…`) and RFC 8905 * `payto://` for types Amethyst has no dedicated scheme for. Shared with the * payment-target dialog so the same pill hands off to the same app wherever * it is tapped. @@ -159,6 +159,10 @@ fun paymentTargetStyleFor(rawType: String): PaymentTargetStyle { return when (type) { "bitcoin", "btc", "onchain" -> PaymentTargetStyle(MaterialSymbols.CurrencyBitcoin, BitcoinOrange, "BITCOIN") + "bip352" -> + PaymentTargetStyle(MaterialSymbols.CurrencyBitcoin, BitcoinOrange, "SILENT PAYMENT") + "bip353" -> + PaymentTargetStyle(MaterialSymbols.CurrencyBitcoin, BitcoinOrange, "BIP-353") "lightning", "ln" -> PaymentTargetStyle(MaterialSymbols.Bolt, BitcoinOrange, "LIGHTNING") "lnurl" -> @@ -183,8 +187,12 @@ fun paymentTargetStyleFor(rawType: String): PaymentTargetStyle { PaymentTargetStyle(walletIcon, SOLANA_PURPLE, "SOLANA") "tron", "trx" -> PaymentTargetStyle(walletIcon, TRON_RED, "TRON") - "cashapp" -> - PaymentTargetStyle(walletIcon, CASHAPP_LIME, "CASHAPP") + "cashme", "cashapp" -> + PaymentTargetStyle(walletIcon, CASHAPP_LIME, "CASH APP") + "nano", "xno" -> + PaymentTargetStyle(walletIcon, NANO_BLUE, "NANO") + "revolut" -> + PaymentTargetStyle(walletIcon, REVOLUT_BLUE, "REVOLUT") "venmo" -> PaymentTargetStyle(walletIcon, VENMO_BLUE, "VENMO") "paypal" -> @@ -213,4 +221,6 @@ private val TRON_RED = Color(0xFFEF0027) private val CASHAPP_LIME = Color(0xFF00E64D) private val VENMO_BLUE = Color(0xFF008CFF) private val PAYPAL_DEEP_BLUE = Color(0xFF003087) +private val NANO_BLUE = Color(0xFF209CE9) +private val REVOLUT_BLUE = Color(0xFF0666EB) private val GENERIC_TARGET_COLOR = Color(0xFF7C8DA0) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt index 1941a50d95..d1ed9b394a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt @@ -69,7 +69,7 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget import kotlinx.coroutines.launch @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt index 775012e84e..6ee12c1654 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt @@ -78,8 +78,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.ProfilePaymentMethod import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.rememberProfileClinkOffer import com.vitorpamplona.quartz.experimental.clink.pointers.NOffer -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.TaprootAddress import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt index c8b6e75aaf..1b80cdec86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt @@ -195,7 +195,6 @@ import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent @@ -324,6 +323,7 @@ import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt index 2e091a4cf1..7d155b5587 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt @@ -64,7 +64,7 @@ import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletViewModel -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget @Composable fun NIP47SetupScreen( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt index aaa614f854..9168c65c5e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt @@ -30,8 +30,6 @@ import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.UserCards import com.vitorpamplona.amethyst.commons.util.KmpLock import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.commons.util.withLock -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata @@ -43,6 +41,8 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.tags.NutzapMintTag import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent import com.vitorpamplona.quartz.utils.Hex import kotlin.concurrent.Volatile diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt index b1ffb3334d..b77d36e4d9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.commons.model.backups import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.diff.DiffableEvent @@ -53,6 +52,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.ClientTag +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent /** Which of the account's backed-up events a conflict is about. Drives the dialog's wording. */ diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index 0954ef7a9e..8a7ed5cdd6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -177,7 +177,6 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.Softw import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent @@ -392,6 +391,7 @@ import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PayToRailMatcher.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PayToRailMatcher.kt index fa8bccab2f..b360bbad8d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PayToRailMatcher.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PayToRailMatcher.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.model.payments -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget /** * Picks the NIP-A3 payment targets a sender can hand off to when paying a note's diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PaymentTargetTypes.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PaymentTargetTypes.kt index e82aa23c43..a6d03e4aa3 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PaymentTargetTypes.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PaymentTargetTypes.kt @@ -37,6 +37,9 @@ package com.vitorpamplona.amethyst.commons.model.payments * and the installed-app probe (which needs the URI before it has an authority). */ object PaymentTargetTypes { + private const val CASHME = "cashme" + private const val DOLLAR = "\$" + /** Lightning-family types Amethyst can pay in-app through the Send Payment screen. */ val LIGHTNING_TYPES = setOf("lightning", "ln", "lnurl") @@ -62,10 +65,14 @@ object PaymentTargetTypes { "doge" to "dogecoin", "sol" to "solana", "trx" to "tron", + "xno" to "nano", + // NIP-A3 names the Cash App cashtag type `cashme`; `cashapp` is what + // Amethyst (and others) wrote before the spec settled on it. + "cashapp" to "cashme", ) /** Types whose hand-off is a web page rather than a registered URI scheme. */ - private val WEB_TYPES = setOf("cashapp", "venmo", "paypal") + private val WEB_TYPES = setOf("cashme", "venmo", "paypal", "revolut") /** Types with a dedicated URI scheme, keyed by canonical name. */ private val SCHEMES = @@ -82,16 +89,31 @@ object PaymentTargetTypes { "dogecoin" to "dogecoin", "solana" to "solana", "tron" to "tron", + "nano" to "nano", + ) + + /** + * Types whose scheme carries the address in a query parameter instead of the path: + * a BIP-352 silent payment address goes in a BIP-321 `bitcoin:` URI's `sp` field. + */ + private val QUERY_SCHEMES = + mapOf( + "bip352" to "bitcoin:?sp=", ) private val WEB_HOSTS = mapOf( - "cashapp" to "https://cash.app/", + "cashme" to "https://cash.app/", "venmo" to "https://venmo.com/", "paypal" to "https://paypal.me/", + "revolut" to "https://revolut.me/", ) - /** Trims, lowercases and collapses known aliases onto one family name. */ + /** + * Trims, lowercases and collapses known aliases onto one family name. The result is the + * NIP-A3 spelling where the spec lists one (`cashapp` -> `cashme`), so it is also what a + * new `payto` tag should carry. + */ fun canonical(rawType: String): String { val trimmed = rawType.trim().lowercase() return ALIASES[trimmed] ?: trimmed @@ -130,6 +152,12 @@ object PaymentTargetTypes { val type = canonical(rawType) val value = authority.trim() SCHEMES[type]?.let { return "$it:$value" } + QUERY_SCHEMES[type]?.let { return "$it$value" } + if (type == CASHME) { + // cash.app/$cashtag: NIP-A3 describes the `$`-prefixed tag, but accept a bare one. + val cashtag = if (value.isEmpty() || value.startsWith(DOLLAR)) value else DOLLAR + value + return WEB_HOSTS.getValue(CASHME) + cashtag + } WEB_HOSTS[type]?.let { return "$it$value" } return "payto://$type/$value" } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt index 444f4e1695..f27c2fc3bc 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.commons.relayClient.profile import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.PinListEvent @@ -33,6 +32,7 @@ import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent val UserProfileListKinds = listOf( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt index 0d1290113d..bab6f362a6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.commons.relayClient.event.loaders.forEachChunk import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.EOSEAccountFast -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer @@ -37,6 +36,7 @@ import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent import com.vitorpamplona.quartz.utils.mapOfSet diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PaymentTargetTypesTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PaymentTargetTypesTest.kt index 3c63157b04..07cdef970b 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PaymentTargetTypesTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/payments/PaymentTargetTypesTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.model.payments -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -76,6 +76,42 @@ class PaymentTargetTypesTest { assertFalse(PaymentTargetTypes.isWebTarget("iban")) } + @Test + fun specCashmeIsCashAppAndCashappStaysAnAlias() { + assertEquals("cashme", PaymentTargetTypes.canonical("cashapp")) + assertEquals("cashme", PaymentTargetTypes.canonical("CashMe")) + assertEquals("https://cash.app/\$vitor", PaymentTargetTypes.uriFor("cashme", "\$vitor")) + // NIP-A3 describes a $-prefixed cashtag; accept the bare name too. + assertEquals("https://cash.app/\$vitor", PaymentTargetTypes.uriFor("cashme", "vitor")) + assertTrue(PaymentTargetTypes.isWebTarget("cashme")) + assertEquals(PaymentTargetTypes.probeKeyFor("cashapp"), PaymentTargetTypes.probeKeyFor("cashme")) + } + + @Test + fun specListedTypesUseTheirNativeScheme() { + assertEquals("bitcoin:bc1q", PaymentTargetTypes.uriFor("bitcoin", "bc1q")) + assertEquals("bitcoincash:qq1", PaymentTargetTypes.uriFor("bitcoincash", "qq1")) + assertEquals("ethereum:0xabc", PaymentTargetTypes.uriFor("ethereum", "0xabc")) + assertEquals("lightning:me@ln.tips", PaymentTargetTypes.uriFor("lightning", "me@ln.tips")) + assertEquals("litecoin:ltc1", PaymentTargetTypes.uriFor("litecoin", "ltc1")) + assertEquals("monero:4A", PaymentTargetTypes.uriFor("monero", "4A")) + assertEquals("nano:nano_1dctq", PaymentTargetTypes.uriFor("nano", "nano_1dctq")) + assertEquals("solana:So1", PaymentTargetTypes.uriFor("solana", "So1")) + assertEquals("tron:T9", PaymentTargetTypes.uriFor("tron", "T9")) + assertEquals("zcash:zs1", PaymentTargetTypes.uriFor("zcash", "zs1")) + // BIP-352 silent payment addresses travel in a BIP-321 bitcoin: URI's sp parameter. + assertEquals("bitcoin:?sp=sp1qq", PaymentTargetTypes.uriFor("bip352", "sp1qq")) + assertEquals("https://paypal.me/vitor", PaymentTargetTypes.uriFor("paypal", "vitor")) + assertEquals("https://venmo.com/vitor", PaymentTargetTypes.uriFor("venmo", "vitor")) + assertEquals("https://revolut.me/vitor", PaymentTargetTypes.uriFor("revolut", "vitor")) + } + + @Test + fun specListedTypesWithoutASchemeUsePayto() { + // A BIP-353 name has to be resolved over DNS first; there is no URI scheme for it. + assertEquals("payto://bip353/vitor@example.com", PaymentTargetTypes.uriFor("bip353", "vitor@example.com")) + } + @Test fun unknownTypesFallBackToPayto() { assertEquals("payto://iban/DE75512108001245126199", PaymentTargetTypes.uriFor("IBAN", "DE75512108001245126199")) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt index 0df568b07a..5e75663ce5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt @@ -59,7 +59,6 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.Softw import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent @@ -309,6 +308,7 @@ import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTarget.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTarget.kt similarity index 95% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTarget.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTarget.kt index 66e17243ca..9868f40b19 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTarget.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTarget.kt @@ -18,7 +18,7 @@ * 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.quartz.experimental.nipA3 +package com.vitorpamplona.quartz.nipA3PaymentTargets data class PaymentTarget( val type: String, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTargetTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetTag.kt similarity index 85% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTargetTag.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetTag.kt index 5be6ef4927..442963e6ad 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTargetTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetTag.kt @@ -18,7 +18,7 @@ * 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.quartz.experimental.nipA3 +package com.vitorpamplona.quartz.nipA3PaymentTargets import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.ensure @@ -38,9 +38,11 @@ class PaymentTargetTag { ensure(tag.has(2)) { return null } ensure(tag[2].isNotEmpty()) { return null } - return PaymentTarget(tag[1], tag[2]) + // NIP-A3: the type is always lowercase. Normalize what other clients wrote so + // `Bitcoin` and `bitcoin` are one rail. + return PaymentTarget(tag[1].lowercase(), tag[2]) } - fun assemble(paymentTarget: PaymentTarget) = arrayOf(TAG_NAME, paymentTarget.type, paymentTarget.authority) + fun assemble(paymentTarget: PaymentTarget) = arrayOf(TAG_NAME, paymentTarget.type.lowercase(), paymentTarget.authority) } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTargetsDiff.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetsDiff.kt similarity index 96% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTargetsDiff.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetsDiff.kt index 29ddcc0f85..b55619134a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTargetsDiff.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetsDiff.kt @@ -18,7 +18,7 @@ * 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.quartz.experimental.nipA3 +package com.vitorpamplona.quartz.nipA3PaymentTargets import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.diff.EventDiff diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTargetsEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetsEvent.kt similarity index 98% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTargetsEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetsEvent.kt index c1982a6d02..55e1d53d27 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipA3/PaymentTargetsEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetsEvent.kt @@ -18,7 +18,7 @@ * 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.quartz.experimental.nipA3 +package com.vitorpamplona.quartz.nipA3PaymentTargets import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index a3986c0516..ec0c5a0a4f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -149,7 +149,6 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.Softw import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent @@ -411,6 +410,7 @@ import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent +import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetTagTest.kt new file mode 100644 index 0000000000..18f75b4609 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipA3PaymentTargets/PaymentTargetTagTest.kt @@ -0,0 +1,61 @@ +/* + * 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.quartz.nipA3PaymentTargets + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class PaymentTargetTagTest { + @Test + fun parsesTheSpecExample() { + val tags = + arrayOf( + arrayOf("payto", "bitcoin", "bc1qxq66e0t8d7ugdecwnmv58e90tpry23nc84pg9k"), + arrayOf("payto", "nano", "nano_1dctqbmqxfppo9pswbm6kg9d4s4mbraqn8i4m7ob9gnzz91aurmuho48jx3c"), + arrayOf("payto", "unknowntype", "l7tbta5b9xze6ckkfc99uohzxd009b0r"), + ) + val event = PaymentTargetsEvent("0".repeat(64), "1".repeat(64), 1L, tags, "", "") + assertEquals( + listOf( + PaymentTarget("bitcoin", "bc1qxq66e0t8d7ugdecwnmv58e90tpry23nc84pg9k"), + PaymentTarget("nano", "nano_1dctqbmqxfppo9pswbm6kg9d4s4mbraqn8i4m7ob9gnzz91aurmuho48jx3c"), + PaymentTarget("unknowntype", "l7tbta5b9xze6ckkfc99uohzxd009b0r"), + ), + event.paymentTargets(), + ) + } + + @Test + fun typeIsAlwaysLowercase() { + assertEquals(PaymentTarget("bitcoin", "bc1q"), PaymentTargetTag.parse(arrayOf("payto", "Bitcoin", "bc1q"))) + assertContentEquals(arrayOf("payto", "cashme", "\$Vitor"), PaymentTargetTag.assemble(PaymentTarget("CashMe", "\$Vitor"))) + } + + @Test + fun rejectsIncompleteTags() { + assertNull(PaymentTargetTag.parse(arrayOf("payto", "bitcoin"))) + assertNull(PaymentTargetTag.parse(arrayOf("payto", "", "bc1q"))) + assertNull(PaymentTargetTag.parse(arrayOf("payto", "bitcoin", ""))) + assertNull(PaymentTargetTag.parse(arrayOf("r", "bitcoin", "bc1q"))) + } +}