From 6aff8c693f93c086633eaf7ea55a2dde8ade3ae2 Mon Sep 17 00:00:00 2001 From: tcheeric Date: Tue, 24 Feb 2026 03:04:50 +0000 Subject: [PATCH 1/2] refactor: simplify nostr-java from 9 modules to 4, remove dead code Implements the full design simplification (2.0.0): - Merge 9 modules into 4: core, event, identity, client - Remove 39 concrete event subclasses, 17 tag subclasses, 27 entities - GenericEvent as sole event class, GenericTag with List params - Kinds utility replaces Kind enum, EventFilter builder replaces 14 filters - NostrRelayClient with Virtual Threads, async APIs, Spring Retry - RelayTimeoutException replaces silent empty-list returns - java.util.HexFormat replaces hand-rolled hex encoding - Delete 21 additional dead classes (IContent, GenericEventConverter, etc.) - Merge IKey into BaseKey, BaseAuthMessage into BaseMessage hierarchy - Overhaul all documentation for 2.0 architecture Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 75 +- CLAUDE.md | 337 ++++-- MIGRATION.md | 147 ++- README.md | 142 +-- docs/CODEBASE_OVERVIEW.md | 60 +- docs/GETTING_STARTED.md | 29 +- docs/README.md | 25 +- docs/TROUBLESHOOTING.md | 503 ++------ docs/developer/SECURE_CODING.md | 80 ++ docs/developer/SIMPLIFICATION_PROPOSAL.md | 1014 +++++++++++++++++ docs/explanation/architecture.md | 836 +++----------- docs/explanation/dependency-alignment.md | 96 +- docs/explanation/extending-events.md | 702 ++++-------- docs/explanation/roadmap-1.0.md | 6 +- docs/howto/api-examples.md | 810 ++++--------- docs/howto/custom-events.md | 61 +- docs/howto/diagnostics.md | 103 +- docs/howto/streaming-subscriptions.md | 132 ++- docs/howto/use-nostr-java-api.md | 53 +- docs/howto/version-uplift-workflow.md | 2 +- docs/operations/configuration.md | 31 +- docs/operations/metrics.md | 185 ++- .../problems/GENERIC_TAG_GETCODE_FRAGILITY.md | 266 +++++ docs/reference/nostr-java-api.md | 335 ++++-- nostr-java-api/pom.xml | 127 --- .../src/main/java/nostr/api/EventNostr.java | 126 -- .../src/main/java/nostr/api/NIP01.java | 451 -------- .../src/main/java/nostr/api/NIP02.java | 58 - .../src/main/java/nostr/api/NIP03.java | 37 - .../src/main/java/nostr/api/NIP04.java | 403 ------- .../src/main/java/nostr/api/NIP05.java | 58 - .../src/main/java/nostr/api/NIP09.java | 74 -- .../src/main/java/nostr/api/NIP12.java | 43 - .../src/main/java/nostr/api/NIP14.java | 25 - .../src/main/java/nostr/api/NIP15.java | 98 -- .../src/main/java/nostr/api/NIP20.java | 25 - .../src/main/java/nostr/api/NIP23.java | 139 --- .../src/main/java/nostr/api/NIP25.java | 175 --- .../src/main/java/nostr/api/NIP28.java | 229 ---- .../src/main/java/nostr/api/NIP30.java | 24 - .../src/main/java/nostr/api/NIP31.java | 23 - .../src/main/java/nostr/api/NIP32.java | 34 - .../src/main/java/nostr/api/NIP40.java | 23 - .../src/main/java/nostr/api/NIP42.java | 98 -- .../src/main/java/nostr/api/NIP44.java | 351 ------ .../src/main/java/nostr/api/NIP46.java | 180 --- .../src/main/java/nostr/api/NIP52.java | 233 ---- .../src/main/java/nostr/api/NIP57.java | 423 ------- .../src/main/java/nostr/api/NIP60.java | 484 -------- .../src/main/java/nostr/api/NIP61.java | 158 --- .../src/main/java/nostr/api/NIP65.java | 136 --- .../src/main/java/nostr/api/NIP99.java | 130 --- .../src/main/java/nostr/api/NostrIF.java | 160 --- .../nostr/api/NostrSpringWebSocketClient.java | 276 ----- .../nostr/api/WebSocketClientHandler.java | 283 ----- .../api/client/NostrEventDispatcher.java | 77 -- .../nostr/api/client/NostrRelayRegistry.java | 126 -- .../api/client/NostrRequestDispatcher.java | 82 -- .../api/client/NostrSubscriptionManager.java | 92 -- .../client/WebSocketClientHandlerFactory.java | 24 - .../nostr/api/factory/BaseMessageFactory.java | 16 - .../java/nostr/api/factory/EventFactory.java | 67 -- .../nostr/api/factory/MessageFactory.java | 16 - .../api/factory/impl/BaseTagFactory.java | 72 -- .../api/factory/impl/EventMessageFactory.java | 40 - .../api/factory/impl/GenericEventFactory.java | 78 -- .../nostr/api/nip01/NIP01EventBuilder.java | 85 -- .../nostr/api/nip01/NIP01MessageFactory.java | 40 - .../java/nostr/api/nip01/NIP01TagFactory.java | 104 -- .../main/java/nostr/api/nip57/Bolt11Util.java | 67 -- .../java/nostr/api/nip57/NIP57TagFactory.java | 62 - .../api/nip57/NIP57ZapReceiptBuilder.java | 99 -- .../api/nip57/NIP57ZapRequestBuilder.java | 161 --- .../nostr/api/nip57/ZapRequestParameters.java | 47 - .../java/nostr/api/service/NoteService.java | 12 - .../api/service/impl/DefaultNoteService.java | 153 --- .../src/main/java/nostr/config/Constants.java | 65 -- .../main/java/nostr/config/RelayConfig.java | 21 - .../java/nostr/config/RelaysProperties.java | 11 - .../src/main/resources/app.properties | 1 - .../src/main/resources/relays.properties | 2 - .../test/java/nostr/api/NIP46RequestTest.java | 24 - .../java/nostr/api/TestHandlerFactory.java | 35 - .../api/TestableWebSocketClientHandler.java | 24 - ...strRequestDispatcherEnsureClientsTest.java | 46 - .../client/NostrRequestDispatcherTest.java | 67 -- ...SpringWebSocketClientCloseLoggingTest.java | 90 -- ...WebSocketClientHandlerIntegrationTest.java | 52 - ...NostrSpringWebSocketClientLoggingTest.java | 49 - .../NostrSpringWebSocketClientRelaysTest.java | 30 - ...ngWebSocketClientSubscribeLoggingTest.java | 81 -- .../NostrSubscriptionManagerCloseTest.java | 76 -- .../src/test/java/nostr/api/client/README.md | 20 - .../WebSocketHandlerCloseIdempotentTest.java | 46 - .../WebSocketHandlerCloseSequencingTest.java | 96 -- .../WebSocketHandlerRequestErrorTest.java | 37 - .../WebSocketHandlerSendCloseFrameTest.java | 48 - .../WebSocketHandlerSendRequestTest.java | 47 - .../nostr/api/integration/ApiEventIT.java | 856 -------------- ...EventTestUsingSpringWebSocketClientIT.java | 109 -- .../api/integration/ApiNIP52EventIT.java | 113 -- .../api/integration/ApiNIP52RequestIT.java | 256 ----- .../api/integration/ApiNIP99EventIT.java | 139 --- .../api/integration/ApiNIP99RequestIT.java | 235 ---- .../integration/BaseRelayIntegrationTest.java | 89 -- .../nostr/api/integration/MultiRelayIT.java | 155 --- ...trSpringWebSocketClientSubscriptionIT.java | 161 --- .../integration/SubscriptionLifecycleIT.java | 192 ---- .../integration/ZDoLastApiNIP09EventIT.java | 149 --- .../support/FakeWebSocketClient.java | 137 --- .../support/FakeWebSocketClientFactory.java | 43 - .../java/nostr/api/unit/Bolt11UtilTest.java | 92 -- .../api/unit/CalendarTimeBasedEventTest.java | 195 ---- .../java/nostr/api/unit/ConstantsTest.java | 41 - .../java/nostr/api/unit/JsonParseTest.java | 992 ---------------- .../nostr/api/unit/NIP01EventBuilderTest.java | 37 - .../nostr/api/unit/NIP01MessagesTest.java | 74 -- .../test/java/nostr/api/unit/NIP01Test.java | 311 ----- .../test/java/nostr/api/unit/NIP02Test.java | 71 -- .../test/java/nostr/api/unit/NIP03Test.java | 31 - .../test/java/nostr/api/unit/NIP04Test.java | 194 ---- .../test/java/nostr/api/unit/NIP05Test.java | 37 - .../test/java/nostr/api/unit/NIP09Test.java | 30 - .../test/java/nostr/api/unit/NIP12Test.java | 31 - .../test/java/nostr/api/unit/NIP14Test.java | 18 - .../test/java/nostr/api/unit/NIP15Test.java | 33 - .../test/java/nostr/api/unit/NIP20Test.java | 25 - .../test/java/nostr/api/unit/NIP23Test.java | 29 - .../test/java/nostr/api/unit/NIP25Test.java | 29 - .../test/java/nostr/api/unit/NIP28Test.java | 47 - .../test/java/nostr/api/unit/NIP30Test.java | 19 - .../test/java/nostr/api/unit/NIP31Test.java | 18 - .../test/java/nostr/api/unit/NIP32Test.java | 24 - .../test/java/nostr/api/unit/NIP40Test.java | 18 - .../test/java/nostr/api/unit/NIP42Test.java | 65 -- .../test/java/nostr/api/unit/NIP44Test.java | 181 --- .../test/java/nostr/api/unit/NIP46Test.java | 90 -- .../java/nostr/api/unit/NIP52ImplTest.java | 142 --- .../java/nostr/api/unit/NIP57ImplTest.java | 367 ------ .../test/java/nostr/api/unit/NIP60Test.java | 277 ----- .../test/java/nostr/api/unit/NIP61Test.java | 194 ---- .../test/java/nostr/api/unit/NIP65Test.java | 55 - .../java/nostr/api/unit/NIP99ImplTest.java | 128 --- .../test/java/nostr/api/unit/NIP99Test.java | 89 -- ...gWebSocketClientEventVerificationTest.java | 88 -- .../unit/NostrSpringWebSocketClientTest.java | 90 -- .../api/util/CommonTestObjectsFactory.java | 151 --- .../java/nostr/api/util/JsonComparator.java | 117 -- .../resources/application-test.properties | 4 - .../test/resources/junit-platform.properties | 9 - .../org.mockito.plugins.MockMaker | 1 - .../test/resources/relay-container.properties | 2 - nostr-java-api/src/test/resources/strfry.conf | 75 -- .../java/nostr/base/ElementAttribute.java | 11 - .../main/java/nostr/base/GenericTagQuery.java | 6 - .../java/nostr/base/IBech32Encodable.java | 9 - .../src/main/java/nostr/base/IElement.java | 11 - .../src/main/java/nostr/base/IEvent.java | 8 - .../main/java/nostr/base/IGenericElement.java | 11 - .../src/main/java/nostr/base/IKey.java | 13 - .../src/main/java/nostr/base/ITag.java | 11 - .../src/main/java/nostr/base/Kind.java | 135 --- .../src/main/java/nostr/base/Marker.java | 29 - .../src/main/java/nostr/base/RelayUri.java | 41 - .../java/nostr/base/annotation/Event.java | 18 - .../main/java/nostr/base/annotation/Key.java | 17 - .../main/java/nostr/base/annotation/Tag.java | 20 - .../src/test/java/nostr/base/KindTest.java | 78 -- .../src/test/java/nostr/base/MarkerTest.java | 17 - .../test/java/nostr/base/RelayUriTest.java | 22 - nostr-java-client/pom.xml | 4 +- .../nostr/client/WebSocketClientFactory.java | 15 - .../springwebsocket/ConnectionState.java | 11 + .../springwebsocket/NostrRelayClient.java | 633 ++++++++++ .../RelayTimeoutException.java | 20 + .../SpringWebSocketClient.java | 199 ---- .../SpringWebSocketClientFactory.java | 18 - .../StandardWebSocketClient.java | 447 -------- .../springwebsocket/WebSocketClientIF.java | 107 -- ... => NostrRelayClientMultiMessageTest.java} | 38 +- ... => NostrRelayClientSubscriptionTest.java} | 13 +- ....java => NostrRelayClientTimeoutTest.java} | 13 +- .../SpringWebSocketClientSubscribeTest.java | 155 ++- .../SpringWebSocketClientTest.java | 245 ++-- {nostr-java-base => nostr-java-core}/pom.xml | 30 +- .../src/main/java/nostr/crypto/Pair.java | 0 .../src/main/java/nostr/crypto/Point.java | 0 .../main/java/nostr/crypto/bech32/Bech32.java | 0 .../bech32/Bech32EncodingException.java | 0 .../nostr/crypto/bech32/Bech32Prefix.java | 0 .../crypto/nip04/EncryptedDirectMessage.java | 0 .../nostr/crypto/nip44/EncryptedPayloads.java | 0 .../java/nostr/crypto/schnorr/Schnorr.java | 370 +++--- .../crypto/schnorr/SchnorrException.java | 0 .../main/java/nostr/util/NostrException.java | 0 .../src/main/java/nostr/util/NostrUtil.java | 27 +- .../util/exception/NostrCryptoException.java | 0 .../exception/NostrEncodingException.java | 0 .../util/exception/NostrNetworkException.java | 4 +- .../exception/NostrProtocolException.java | 2 +- .../util/exception/NostrRuntimeException.java | 0 .../util/validator/HexStringValidator.java | 0 .../nostr/util/validator/Nip05Content.java | 0 .../nostr/util/validator/Nip05Validator.java | 97 +- .../test/java/nostr/crypto/CryptoTest.java | 0 .../src/test/java/nostr/crypto/PointTest.java | 0 .../java/nostr/crypto/bech32/Bech32Test.java | 0 .../nostr/crypto/schnorr/SchnorrTest.java | 0 .../nostr/util/NostrUtilExtendedTest.java | 0 .../java/nostr/util/NostrUtilRandomTest.java | 0 .../test/java/nostr/util/NostrUtilTest.java | 0 .../validator/HexStringValidatorTest.java | 0 .../util/validator/Nip05ValidatorTest.java | 81 +- .../resources/application-test.properties | 0 .../test/resources/junit-platform.properties | 0 nostr-java-crypto/pom.xml | 73 -- .../resources/application-test.properties | 4 - nostr-java-encryption/pom.xml | 63 - nostr-java-event/pom.xml | 26 +- .../src/main/java/nostr/base/BaseKey.java | 4 +- .../src/main/java/nostr/base/Command.java | 0 .../src/main/java/nostr/base/Encoder.java | 0 .../src/main/java/nostr/base/IDecoder.java | 2 +- .../src/main/java/nostr/base/ISignable.java | 0 .../java/nostr/base/KeyEncodingException.java | 0 .../src/main/java/nostr/base/KeyType.java | 0 .../src/main/java/nostr/base/Kinds.java | 76 ++ .../main/java/nostr/base/NipConstants.java | 0 .../src/main/java/nostr/base/PrivateKey.java | 0 .../src/main/java/nostr/base/PublicKey.java | 0 .../src/main/java/nostr/base/Relay.java | 0 .../src/main/java/nostr/base/Signature.java | 0 .../main/java/nostr/base/SubscriptionId.java | 0 .../java/nostr/base/json/EventJsonMapper.java | 0 .../src/main/java/nostr/event/BaseEvent.java | 58 - .../main/java/nostr/event/BaseMessage.java | 3 +- .../src/main/java/nostr/event/BaseTag.java | 269 +---- .../src/main/java/nostr/event/Deleteable.java | 6 - .../src/main/java/nostr/event/IContent.java | 9 - .../main/java/nostr/event/JsonContent.java | 19 - .../src/main/java/nostr/event/NIP01Event.java | 27 - .../src/main/java/nostr/event/NIP04Event.java | 28 - .../src/main/java/nostr/event/NIP05Event.java | 17 - .../src/main/java/nostr/event/NIP09Event.java | 19 - .../src/main/java/nostr/event/NIP25Event.java | 31 - .../src/main/java/nostr/event/NIP52Event.java | 23 - .../src/main/java/nostr/event/NIP99Event.java | 21 - .../main/java/nostr/event/Nip05Content.java | 23 - .../src/main/java/nostr/event/Reaction.java | 18 - .../src/main/java/nostr/event/Response.java | 17 - .../java/nostr/event/entities/Amount.java | 15 - .../nostr/event/entities/CalendarContent.java | 189 --- .../event/entities/CalendarRsvpContent.java | 65 -- .../java/nostr/event/entities/CashuMint.java | 27 - .../java/nostr/event/entities/CashuProof.java | 43 - .../java/nostr/event/entities/CashuQuote.java | 17 - .../java/nostr/event/entities/CashuToken.java | 64 -- .../nostr/event/entities/CashuWallet.java | 117 -- .../nostr/event/entities/ChannelProfile.java | 28 - .../event/entities/ClassifiedListing.java | 31 - .../nostr/event/entities/CustomerOrder.java | 66 -- .../nostr/event/entities/NIP15Content.java | 24 - .../nostr/event/entities/NIP42Content.java | 5 - .../java/nostr/event/entities/NutZap.java | 35 - .../event/entities/NutZapInformation.java | 17 - .../nostr/event/entities/PaymentRequest.java | 54 - .../event/entities/PaymentShipmentStatus.java | 28 - .../java/nostr/event/entities/Product.java | 51 - .../java/nostr/event/entities/Profile.java | 27 - .../java/nostr/event/entities/Reaction.java | 18 - .../java/nostr/event/entities/Response.java | 18 - .../nostr/event/entities/SpendingHistory.java | 50 - .../main/java/nostr/event/entities/Stall.java | 48 - .../nostr/event/entities/UserProfile.java | 60 - .../java/nostr/event/entities/ZapReceipt.java | 28 - .../java/nostr/event/entities/ZapRequest.java | 26 - .../event/filter/AbstractFilterable.java | 19 - .../nostr/event/filter/AddressTagFilter.java | 73 -- .../java/nostr/event/filter/AuthorFilter.java | 35 - .../java/nostr/event/filter/EventFilter.java | 212 +++- .../java/nostr/event/filter/Filterable.java | 93 -- .../main/java/nostr/event/filter/Filters.java | 68 +- .../event/filter/GenericTagQueryFilter.java | 55 - .../nostr/event/filter/GeohashTagFilter.java | 37 - .../nostr/event/filter/HashtagTagFilter.java | 37 - .../event/filter/IdentifierTagFilter.java | 39 - .../java/nostr/event/filter/KindFilter.java | 43 - .../event/filter/ReferencedEventFilter.java | 37 - .../filter/ReferencedPublicKeyFilter.java | 39 - .../java/nostr/event/filter/SinceFilter.java | 43 - .../java/nostr/event/filter/UntilFilter.java | 43 - .../java/nostr/event/filter/UrlTagFilter.java | 36 - .../nostr/event/filter/VoteTagFilter.java | 37 - .../event/impl/AbstractBaseCalendarEvent.java | 25 - .../impl/AbstractBaseNostrConnectEvent.java | 33 - .../nostr/event/impl/AddressableEvent.java | 47 - .../event/impl/CalendarDateBasedEvent.java | 129 --- .../java/nostr/event/impl/CalendarEvent.java | 91 -- .../nostr/event/impl/CalendarRsvpEvent.java | 126 -- .../event/impl/CalendarTimeBasedEvent.java | 99 -- .../impl/CanonicalAuthenticationEvent.java | 65 -- .../nostr/event/impl/ChannelCreateEvent.java | 63 - .../nostr/event/impl/ChannelMessageEvent.java | 138 --- .../event/impl/ChannelMetadataEvent.java | 94 -- .../java/nostr/event/impl/CheckoutEvent.java | 71 -- .../event/impl/ClassifiedListingEvent.java | 173 --- .../nostr/event/impl/ContactListEvent.java | 43 - .../impl/CreateOrUpdateProductEvent.java | 68 -- .../event/impl/CreateOrUpdateStallEvent.java | 69 -- .../nostr/event/impl/CustomerOrderEvent.java | 51 - .../java/nostr/event/impl/DeletionEvent.java | 68 -- .../nostr/event/impl/DirectMessageEvent.java | 52 - .../java/nostr/event/impl/EphemeralEvent.java | 38 - .../java/nostr/event/impl/GenericEvent.java | 336 +----- .../nostr/event/impl/HideMessageEvent.java | 47 - .../impl/InternetIdentifierMetadataEvent.java | 72 -- .../java/nostr/event/impl/MentionsEvent.java | 53 - .../java/nostr/event/impl/MerchantEvent.java | 60 - .../impl/MerchantRequestPaymentEvent.java | 45 - .../java/nostr/event/impl/MuteUserEvent.java | 40 - .../nostr/event/impl/NostrConnectEvent.java | 20 - .../event/impl/NostrConnectRequestEvent.java | 23 - .../event/impl/NostrConnectResponseEvent.java | 21 - .../event/impl/NostrMarketplaceEvent.java | 47 - .../java/nostr/event/impl/NutZapEvent.java | 106 -- .../event/impl/NutZapInformationalEvent.java | 96 -- .../main/java/nostr/event/impl/OtsEvent.java | 29 - .../java/nostr/event/impl/ReactionEvent.java | 42 - .../nostr/event/impl/ReplaceableEvent.java | 46 - .../java/nostr/event/impl/TextNoteEvent.java | 42 - .../impl/VerifyPaymentOrShippedEvent.java | 45 - .../nostr/event/impl/ZapReceiptEvent.java | 105 -- .../nostr/event/impl/ZapRequestEvent.java | 118 -- .../event/json/codec/BaseEventEncoder.java | 4 +- .../event/json/codec/BaseTagDecoder.java | 39 - .../event/json/codec/BaseTagEncoder.java | 2 +- .../json/codec/EventEncodingException.java | 0 .../event/json/codec/FilterableProvider.java | 49 - .../event/json/codec/FiltersDecoder.java | 49 - .../event/json/codec/FiltersEncoder.java | 20 +- .../event/json/codec/GenericEventDecoder.java | 41 - .../event/json/codec/GenericTagDecoder.java | 20 +- .../event/json/codec/Nip05ContentDecoder.java | 38 - .../CalendarDateBasedEventDeserializer.java | 32 - .../CalendarEventDeserializer.java | 32 - .../CalendarRsvpEventDeserializer.java | 32 - .../CalendarTimeBasedEventDeserializer.java | 32 - .../ClassifiedListingEventDeserializer.java | 56 - .../json/deserializer/TagDeserializer.java | 46 +- .../serializer/AbstractTagSerializer.java | 41 - .../json/serializer/AddressTagSerializer.java | 38 - .../json/serializer/BaseTagSerializer.java | 27 +- .../json/serializer/CashuTokenSerializer.java | 42 - .../serializer/ExpirationTagSerializer.java | 24 - .../json/serializer/GenericTagSerializer.java | 25 +- .../serializer/IdentifierTagSerializer.java | 21 - .../serializer/ReferenceTagSerializer.java | 31 - .../json/serializer/RelaysTagSerializer.java | 30 - .../event/json/serializer/TagSerializer.java | 24 - .../nostr/event/message/BaseAuthMessage.java | 18 - .../CanonicalAuthenticationMessage.java | 30 +- .../nostr/event/message/EventMessage.java | 12 +- .../nostr/event/message/GenericMessage.java | 73 -- .../message/RelayAuthenticationMessage.java | 2 +- .../java/nostr/event/message/ReqMessage.java | 11 +- .../event/support/GenericEventConverter.java | 34 - .../event/support/GenericEventSerializer.java | 32 - .../support/GenericEventTypeClassifier.java | 29 - .../event/support/GenericEventUpdater.java | 35 - .../event/support/GenericEventValidator.java | 61 - .../main/java/nostr/event/tag/AddressTag.java | 85 -- .../java/nostr/event/tag/DelegationTag.java | 64 -- .../main/java/nostr/event/tag/EmojiTag.java | 49 - .../main/java/nostr/event/tag/EventTag.java | 80 -- .../java/nostr/event/tag/ExpirationTag.java | 42 - .../main/java/nostr/event/tag/GenericTag.java | 40 +- .../main/java/nostr/event/tag/GeohashTag.java | 49 - .../main/java/nostr/event/tag/HashtagTag.java | 46 - .../java/nostr/event/tag/IdentifierTag.java | 38 - .../nostr/event/tag/LabelNamespaceTag.java | 39 - .../main/java/nostr/event/tag/LabelTag.java | 51 - .../main/java/nostr/event/tag/NonceTag.java | 55 - .../main/java/nostr/event/tag/PriceTag.java | 86 -- .../main/java/nostr/event/tag/PubKeyTag.java | 89 -- .../java/nostr/event/tag/ReferenceTag.java | 74 -- .../main/java/nostr/event/tag/RelaysTag.java | 56 - .../main/java/nostr/event/tag/SubjectTag.java | 53 - .../java/nostr/event/tag/TagRegistry.java | 59 - .../src/main/java/nostr/event/tag/UrlTag.java | 44 - .../main/java/nostr/event/tag/VoteTag.java | 38 - .../src/test/java/nostr/base/BaseKeyTest.java | 0 .../src/test/java/nostr/base/CommandTest.java | 0 .../src/test/java/nostr/base/RelayTest.java | 0 .../event/impl/AddressableEventTest.java | 67 -- .../impl/AddressableEventValidateTest.java | 40 - .../impl/ChannelMessageEventValidateTest.java | 63 - .../impl/ClassifiedListingEventTest.java | 37 - .../impl/ContactListEventValidateTest.java | 60 - .../event/impl/DeletionEventValidateTest.java | 67 -- .../impl/DirectMessageEventValidateTest.java | 58 - .../nostr/event/impl/EphemeralEventTest.java | 36 - .../impl/EphemeralEventValidateTest.java | 40 - .../event/impl/GenericEventValidateTest.java | 49 - .../impl/HideMessageEventValidateTest.java | 53 - .../event/impl/MuteUserEventValidateTest.java | 62 - .../event/impl/ReactionEventValidateTest.java | 62 - .../impl/ReplaceableEventValidateTest.java | 56 - .../event/impl/TextNoteEventValidateTest.java | 71 -- .../impl/ZapRequestEventValidateTest.java | 86 -- .../json/codec/BaseEventEncoderTest.java | 15 +- .../event/serializer/EventSerializerTest.java | 12 +- .../support/GenericEventSupportTest.java | 73 -- .../java/nostr/event/unit/BaseTagTest.java | 248 ---- .../event/unit/CalendarContentAddTagTest.java | 67 -- .../event/unit/CalendarContentDecodeTest.java | 103 -- .../event/unit/CalendarDeserializerTest.java | 159 --- .../unit/ClassifiedListingDecodeTest.java | 39 - .../java/nostr/event/unit/DecodeTest.java | 91 -- .../java/nostr/event/unit/EventTagTest.java | 99 -- .../event/unit/EventWithAddressTagTest.java | 147 --- .../nostr/event/unit/FiltersDecoderTest.java | 394 ------- .../nostr/event/unit/FiltersEncoderTest.java | 389 ------- .../java/nostr/event/unit/FiltersTest.java | 51 +- .../event/unit/GenericEventBuilderTest.java | 13 +- .../java/nostr/event/unit/GenericTagTest.java | 2 +- .../event/unit/JsonContentValidationTest.java | 62 - .../nostr/event/unit/KindMappingTest.java | 24 - .../nostr/event/unit/Nip60FilterJsonTest.java | 53 +- .../java/nostr/event/unit/PriceTagTest.java | 80 -- .../event/unit/ProductSerializationTest.java | 51 - .../java/nostr/event/unit/PubkeyTagTest.java | 26 - .../java/nostr/event/unit/RelaysTagTest.java | 44 - .../nostr/event/unit/TagDeserializerTest.java | 91 -- .../nostr/event/unit/TagRegistryTest.java | 34 - .../nostr/event/unit/ValidateKindTest.java | 30 - nostr-java-examples/pom.xml | 34 - .../examples/ExpirationEventExample.java | 60 - .../java/nostr/examples/FilterExample.java | 44 - .../java/nostr/examples/NostrApiExamples.java | 326 ------ .../SpringClientTextEventExample.java | 22 - .../examples/SpringSubscriptionExample.java | 48 - .../nostr/examples/TextNoteEventExample.java | 30 - .../src/main/resources/logging.properties | 86 -- .../nostr/id/ClassifiedListingEventTest.java | 89 -- .../src/test/java/nostr/id/EntityFactory.java | 174 --- .../test/java/nostr/id/ReactionEventTest.java | 32 - .../java/nostr/id/ZapReceiptEventTest.java | 42 - .../java/nostr/id/ZapRequestEventTest.java | 94 -- .../test/resources/junit-platform.properties | 7 - .../pom.xml | 16 +- .../java/nostr/encryption/MessageCipher.java | 0 .../nostr/encryption/MessageCipher04.java | 0 .../nostr/encryption/MessageCipher44.java | 0 .../src/main/java/nostr/id/Identity.java | 0 .../main/java/nostr/id/SigningException.java | 0 .../nostr/encryption/MessageCipherTest.java | 0 .../src/test/java/nostr/id/EntityFactory.java | 102 ++ .../src/test/java/nostr/id/EventTest.java | 13 - .../src/test/java/nostr/id/IdentityTest.java | 307 +++-- .../resources/application-test.properties | 0 .../test/resources/junit-platform.properties | 0 nostr-java-util/pom.xml | 62 - .../util/http/DefaultHttpClientProvider.java | 13 - .../nostr/util/http/HttpClientProvider.java | 16 - .../test/resources/junit-platform.properties | 7 - pom.xml | 42 +- 466 files changed, 5209 insertions(+), 31642 deletions(-) create mode 100644 docs/developer/SECURE_CODING.md create mode 100644 docs/developer/SIMPLIFICATION_PROPOSAL.md create mode 100644 docs/problems/GENERIC_TAG_GETCODE_FRAGILITY.md delete mode 100644 nostr-java-api/pom.xml delete mode 100644 nostr-java-api/src/main/java/nostr/api/EventNostr.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP01.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP02.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP03.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP04.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP05.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP09.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP12.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP14.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP15.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP20.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP23.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP25.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP28.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP30.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP31.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP32.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP40.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP42.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP44.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP46.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP52.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP57.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP60.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP61.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP65.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NIP99.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NostrIF.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/WebSocketClientHandler.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/client/NostrEventDispatcher.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/client/NostrRelayRegistry.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/client/NostrRequestDispatcher.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/client/NostrSubscriptionManager.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/client/WebSocketClientHandlerFactory.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/factory/BaseMessageFactory.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/factory/EventFactory.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/factory/MessageFactory.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/factory/impl/BaseTagFactory.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/factory/impl/EventMessageFactory.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/factory/impl/GenericEventFactory.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/nip01/NIP01EventBuilder.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/nip01/NIP01MessageFactory.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/nip01/NIP01TagFactory.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/nip57/Bolt11Util.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/nip57/NIP57TagFactory.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/nip57/NIP57ZapReceiptBuilder.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/nip57/NIP57ZapRequestBuilder.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/nip57/ZapRequestParameters.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/service/NoteService.java delete mode 100644 nostr-java-api/src/main/java/nostr/api/service/impl/DefaultNoteService.java delete mode 100644 nostr-java-api/src/main/java/nostr/config/Constants.java delete mode 100644 nostr-java-api/src/main/java/nostr/config/RelayConfig.java delete mode 100644 nostr-java-api/src/main/java/nostr/config/RelaysProperties.java delete mode 100644 nostr-java-api/src/main/resources/app.properties delete mode 100644 nostr-java-api/src/main/resources/relays.properties delete mode 100644 nostr-java-api/src/test/java/nostr/api/NIP46RequestTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/TestHandlerFactory.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/TestableWebSocketClientHandler.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/NostrRequestDispatcherEnsureClientsTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/NostrRequestDispatcherTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientCloseLoggingTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientHandlerIntegrationTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientLoggingTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientRelaysTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientSubscribeLoggingTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/NostrSubscriptionManagerCloseTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/README.md delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerCloseIdempotentTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerCloseSequencingTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerRequestErrorTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerSendCloseFrameTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerSendRequestTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/ApiEventIT.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/ApiEventTestUsingSpringWebSocketClientIT.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/ApiNIP52EventIT.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/ApiNIP52RequestIT.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/ApiNIP99EventIT.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/ApiNIP99RequestIT.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/BaseRelayIntegrationTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/MultiRelayIT.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/NostrSpringWebSocketClientSubscriptionIT.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/SubscriptionLifecycleIT.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/ZDoLastApiNIP09EventIT.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/support/FakeWebSocketClient.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/integration/support/FakeWebSocketClientFactory.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/Bolt11UtilTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/CalendarTimeBasedEventTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/ConstantsTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/JsonParseTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP01EventBuilderTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP01MessagesTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP01Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP02Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP03Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP04Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP05Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP09Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP12Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP14Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP15Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP20Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP23Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP25Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP28Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP30Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP31Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP32Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP40Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP42Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP44Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP46Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP52ImplTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP57ImplTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP60Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP61Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP65Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP99ImplTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NIP99Test.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientEventVerificationTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientTest.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/util/CommonTestObjectsFactory.java delete mode 100644 nostr-java-api/src/test/java/nostr/api/util/JsonComparator.java delete mode 100644 nostr-java-api/src/test/resources/application-test.properties delete mode 100644 nostr-java-api/src/test/resources/junit-platform.properties delete mode 100644 nostr-java-api/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker delete mode 100644 nostr-java-api/src/test/resources/relay-container.properties delete mode 100644 nostr-java-api/src/test/resources/strfry.conf delete mode 100644 nostr-java-base/src/main/java/nostr/base/ElementAttribute.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/GenericTagQuery.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/IBech32Encodable.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/IElement.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/IEvent.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/IGenericElement.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/IKey.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/ITag.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/Kind.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/Marker.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/RelayUri.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/annotation/Event.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/annotation/Key.java delete mode 100644 nostr-java-base/src/main/java/nostr/base/annotation/Tag.java delete mode 100644 nostr-java-base/src/test/java/nostr/base/KindTest.java delete mode 100644 nostr-java-base/src/test/java/nostr/base/MarkerTest.java delete mode 100644 nostr-java-base/src/test/java/nostr/base/RelayUriTest.java delete mode 100644 nostr-java-client/src/main/java/nostr/client/WebSocketClientFactory.java create mode 100644 nostr-java-client/src/main/java/nostr/client/springwebsocket/ConnectionState.java create mode 100644 nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java create mode 100644 nostr-java-client/src/main/java/nostr/client/springwebsocket/RelayTimeoutException.java delete mode 100644 nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClient.java delete mode 100644 nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClientFactory.java delete mode 100644 nostr-java-client/src/main/java/nostr/client/springwebsocket/StandardWebSocketClient.java delete mode 100644 nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClientIF.java rename nostr-java-client/src/test/java/nostr/client/springwebsocket/{StandardWebSocketClientMultiMessageTest.java => NostrRelayClientMultiMessageTest.java} (73%) rename nostr-java-client/src/test/java/nostr/client/springwebsocket/{StandardWebSocketClientSubscriptionTest.java => NostrRelayClientSubscriptionTest.java} (79%) rename nostr-java-client/src/test/java/nostr/client/springwebsocket/{StandardWebSocketClientTimeoutTest.java => NostrRelayClientTimeoutTest.java} (51%) rename {nostr-java-base => nostr-java-core}/pom.xml (80%) rename {nostr-java-crypto => nostr-java-core}/src/main/java/nostr/crypto/Pair.java (100%) rename {nostr-java-crypto => nostr-java-core}/src/main/java/nostr/crypto/Point.java (100%) rename {nostr-java-crypto => nostr-java-core}/src/main/java/nostr/crypto/bech32/Bech32.java (100%) rename {nostr-java-crypto => nostr-java-core}/src/main/java/nostr/crypto/bech32/Bech32EncodingException.java (100%) rename {nostr-java-crypto => nostr-java-core}/src/main/java/nostr/crypto/bech32/Bech32Prefix.java (100%) rename {nostr-java-crypto => nostr-java-core}/src/main/java/nostr/crypto/nip04/EncryptedDirectMessage.java (100%) rename {nostr-java-crypto => nostr-java-core}/src/main/java/nostr/crypto/nip44/EncryptedPayloads.java (100%) rename {nostr-java-crypto => nostr-java-core}/src/main/java/nostr/crypto/schnorr/Schnorr.java (97%) rename {nostr-java-crypto => nostr-java-core}/src/main/java/nostr/crypto/schnorr/SchnorrException.java (100%) rename {nostr-java-util => nostr-java-core}/src/main/java/nostr/util/NostrException.java (100%) rename {nostr-java-util => nostr-java-core}/src/main/java/nostr/util/NostrUtil.java (77%) rename {nostr-java-util => nostr-java-core}/src/main/java/nostr/util/exception/NostrCryptoException.java (100%) rename {nostr-java-util => nostr-java-core}/src/main/java/nostr/util/exception/NostrEncodingException.java (100%) rename {nostr-java-util => nostr-java-core}/src/main/java/nostr/util/exception/NostrNetworkException.java (97%) rename {nostr-java-util => nostr-java-core}/src/main/java/nostr/util/exception/NostrProtocolException.java (97%) rename {nostr-java-util => nostr-java-core}/src/main/java/nostr/util/exception/NostrRuntimeException.java (100%) rename {nostr-java-util => nostr-java-core}/src/main/java/nostr/util/validator/HexStringValidator.java (100%) rename {nostr-java-util => nostr-java-core}/src/main/java/nostr/util/validator/Nip05Content.java (100%) rename {nostr-java-util => nostr-java-core}/src/main/java/nostr/util/validator/Nip05Validator.java (58%) rename {nostr-java-crypto => nostr-java-core}/src/test/java/nostr/crypto/CryptoTest.java (100%) rename {nostr-java-crypto => nostr-java-core}/src/test/java/nostr/crypto/PointTest.java (100%) rename {nostr-java-crypto => nostr-java-core}/src/test/java/nostr/crypto/bech32/Bech32Test.java (100%) rename {nostr-java-crypto => nostr-java-core}/src/test/java/nostr/crypto/schnorr/SchnorrTest.java (100%) rename {nostr-java-util => nostr-java-core}/src/test/java/nostr/util/NostrUtilExtendedTest.java (100%) rename {nostr-java-util => nostr-java-core}/src/test/java/nostr/util/NostrUtilRandomTest.java (100%) rename {nostr-java-util => nostr-java-core}/src/test/java/nostr/util/NostrUtilTest.java (100%) rename {nostr-java-util => nostr-java-core}/src/test/java/nostr/util/validator/HexStringValidatorTest.java (100%) rename {nostr-java-util => nostr-java-core}/src/test/java/nostr/util/validator/Nip05ValidatorTest.java (71%) rename {nostr-java-util => nostr-java-core}/src/test/resources/application-test.properties (100%) rename {nostr-java-base => nostr-java-core}/src/test/resources/junit-platform.properties (100%) delete mode 100644 nostr-java-crypto/pom.xml delete mode 100644 nostr-java-crypto/src/test/resources/application-test.properties delete mode 100644 nostr-java-encryption/pom.xml rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/BaseKey.java (95%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/Command.java (100%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/Encoder.java (100%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/IDecoder.java (94%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/ISignable.java (100%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/KeyEncodingException.java (100%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/KeyType.java (100%) create mode 100644 nostr-java-event/src/main/java/nostr/base/Kinds.java rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/NipConstants.java (100%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/PrivateKey.java (100%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/PublicKey.java (100%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/Relay.java (100%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/Signature.java (100%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/SubscriptionId.java (100%) rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/base/json/EventJsonMapper.java (100%) delete mode 100644 nostr-java-event/src/main/java/nostr/event/BaseEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/Deleteable.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/IContent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/JsonContent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/NIP01Event.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/NIP04Event.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/NIP05Event.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/NIP09Event.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/NIP25Event.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/NIP52Event.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/NIP99Event.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/Nip05Content.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/Reaction.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/Response.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/Amount.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/CalendarContent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/CalendarRsvpContent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/CashuMint.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/CashuProof.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/CashuQuote.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/CashuToken.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/CashuWallet.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/ChannelProfile.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/ClassifiedListing.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/CustomerOrder.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/NIP15Content.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/NIP42Content.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/NutZap.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/NutZapInformation.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/PaymentRequest.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/PaymentShipmentStatus.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/Product.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/Profile.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/Reaction.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/Response.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/SpendingHistory.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/Stall.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/UserProfile.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/ZapReceipt.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/entities/ZapRequest.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/AbstractFilterable.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/AddressTagFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/AuthorFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/Filterable.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/GenericTagQueryFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/GeohashTagFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/HashtagTagFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/IdentifierTagFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/KindFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/ReferencedEventFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/ReferencedPublicKeyFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/SinceFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/UntilFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/UrlTagFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/filter/VoteTagFilter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/AbstractBaseCalendarEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/AbstractBaseNostrConnectEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/AddressableEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/CalendarDateBasedEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/CalendarEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/CalendarTimeBasedEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/CanonicalAuthenticationEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/ChannelCreateEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/ChannelMessageEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/ChannelMetadataEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/CheckoutEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/ClassifiedListingEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/ContactListEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateProductEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateStallEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/CustomerOrderEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/DeletionEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/DirectMessageEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/EphemeralEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/HideMessageEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/InternetIdentifierMetadataEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/MentionsEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/MerchantEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/MerchantRequestPaymentEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/MuteUserEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/NostrConnectEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/NostrConnectRequestEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/NostrConnectResponseEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/NostrMarketplaceEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/NutZapEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/NutZapInformationalEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/OtsEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/ReactionEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/ReplaceableEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/TextNoteEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/VerifyPaymentOrShippedEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/ZapReceiptEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/impl/ZapRequestEvent.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java rename {nostr-java-base => nostr-java-event}/src/main/java/nostr/event/json/codec/EventEncodingException.java (100%) delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/codec/FilterableProvider.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarDateBasedEventDeserializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarEventDeserializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarRsvpEventDeserializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarTimeBasedEventDeserializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/deserializer/ClassifiedListingEventDeserializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/serializer/AbstractTagSerializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/serializer/AddressTagSerializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/serializer/CashuTokenSerializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/serializer/ExpirationTagSerializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/serializer/IdentifierTagSerializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/serializer/ReferenceTagSerializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/serializer/RelaysTagSerializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/serializer/TagSerializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/message/BaseAuthMessage.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/message/GenericMessage.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/support/GenericEventConverter.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/support/GenericEventSerializer.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/support/GenericEventTypeClassifier.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/support/GenericEventUpdater.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/support/GenericEventValidator.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/AddressTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/DelegationTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/EmojiTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/EventTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/ExpirationTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/GeohashTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/HashtagTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/IdentifierTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/LabelNamespaceTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/LabelTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/NonceTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/PriceTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/PubKeyTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/ReferenceTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/RelaysTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/SubjectTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/TagRegistry.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/UrlTag.java delete mode 100644 nostr-java-event/src/main/java/nostr/event/tag/VoteTag.java rename {nostr-java-base => nostr-java-event}/src/test/java/nostr/base/BaseKeyTest.java (100%) rename {nostr-java-base => nostr-java-event}/src/test/java/nostr/base/CommandTest.java (100%) rename {nostr-java-base => nostr-java-event}/src/test/java/nostr/base/RelayTest.java (100%) delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/AddressableEventTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/AddressableEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/ChannelMessageEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/ClassifiedListingEventTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/ContactListEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/DeletionEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/DirectMessageEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/GenericEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/HideMessageEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/MuteUserEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/ReactionEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/ReplaceableEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/TextNoteEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/impl/ZapRequestEventValidateTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/support/GenericEventSupportTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/BaseTagTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/CalendarContentAddTagTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/CalendarContentDecodeTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/CalendarDeserializerTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/ClassifiedListingDecodeTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/DecodeTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/EventTagTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/EventWithAddressTagTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/FiltersDecoderTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/FiltersEncoderTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/JsonContentValidationTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/KindMappingTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/PriceTagTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/ProductSerializationTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/PubkeyTagTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/RelaysTagTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/TagDeserializerTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/TagRegistryTest.java delete mode 100644 nostr-java-event/src/test/java/nostr/event/unit/ValidateKindTest.java delete mode 100644 nostr-java-examples/pom.xml delete mode 100644 nostr-java-examples/src/main/java/nostr/examples/ExpirationEventExample.java delete mode 100644 nostr-java-examples/src/main/java/nostr/examples/FilterExample.java delete mode 100644 nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java delete mode 100644 nostr-java-examples/src/main/java/nostr/examples/SpringClientTextEventExample.java delete mode 100644 nostr-java-examples/src/main/java/nostr/examples/SpringSubscriptionExample.java delete mode 100644 nostr-java-examples/src/main/java/nostr/examples/TextNoteEventExample.java delete mode 100644 nostr-java-examples/src/main/resources/logging.properties delete mode 100644 nostr-java-id/src/test/java/nostr/id/ClassifiedListingEventTest.java delete mode 100644 nostr-java-id/src/test/java/nostr/id/EntityFactory.java delete mode 100644 nostr-java-id/src/test/java/nostr/id/ReactionEventTest.java delete mode 100644 nostr-java-id/src/test/java/nostr/id/ZapReceiptEventTest.java delete mode 100644 nostr-java-id/src/test/java/nostr/id/ZapRequestEventTest.java delete mode 100644 nostr-java-id/src/test/resources/junit-platform.properties rename {nostr-java-id => nostr-java-identity}/pom.xml (86%) rename {nostr-java-encryption => nostr-java-identity}/src/main/java/nostr/encryption/MessageCipher.java (100%) rename {nostr-java-encryption => nostr-java-identity}/src/main/java/nostr/encryption/MessageCipher04.java (100%) rename {nostr-java-encryption => nostr-java-identity}/src/main/java/nostr/encryption/MessageCipher44.java (100%) rename {nostr-java-id => nostr-java-identity}/src/main/java/nostr/id/Identity.java (100%) rename {nostr-java-id => nostr-java-identity}/src/main/java/nostr/id/SigningException.java (100%) rename {nostr-java-encryption => nostr-java-identity}/src/test/java/nostr/encryption/MessageCipherTest.java (100%) create mode 100644 nostr-java-identity/src/test/java/nostr/id/EntityFactory.java rename {nostr-java-id => nostr-java-identity}/src/test/java/nostr/id/EventTest.java (90%) rename {nostr-java-id => nostr-java-identity}/src/test/java/nostr/id/IdentityTest.java (88%) rename {nostr-java-id => nostr-java-identity}/src/test/resources/application-test.properties (100%) rename {nostr-java-crypto => nostr-java-identity}/src/test/resources/junit-platform.properties (100%) delete mode 100644 nostr-java-util/pom.xml delete mode 100644 nostr-java-util/src/main/java/nostr/util/http/DefaultHttpClientProvider.java delete mode 100644 nostr-java-util/src/main/java/nostr/util/http/HttpClientProvider.java delete mode 100644 nostr-java-util/src/test/resources/junit-platform.properties diff --git a/CHANGELOG.md b/CHANGELOG.md index 09abeaa09..5a91b9103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,80 @@ The format is inspired by Keep a Changelog, and this project adheres to semantic ## [Unreleased] -No unreleased changes yet. +### Removed +- Dead code cleanup — deleted unused classes: `IContent`, `JsonContent`, `Reaction` enum, `Response`, `Nip05Content`, `Nip05ContentDecoder`, `BaseAuthMessage`, `GenericMessage`, `IKey`, `GenericEventConverter`, `GenericEventTypeClassifier`, `GenericEventDecoder`, `FiltersDecoder`, `BaseTagDecoder`, `GenericEventValidator`, `GenericEventSerializer`, `GenericEventUpdater`, `GenericTagQuery`, `HttpClientProvider`, `DefaultHttpClientProvider`. +- `testAuthMessage` test and `GenericEventSupportTest` removed (tested deleted classes). +- `createGenericTagQuery()` removed from `EntityFactory` (only consumer of deleted `GenericTagQuery`). + +### Changed +- `RelayAuthenticationMessage` and `CanonicalAuthenticationMessage` now extend `BaseMessage` directly (previously extended the now-deleted `BaseAuthMessage`). +- `BaseKey` now directly implements `Serializable` (previously implemented the now-deleted `IKey` interface). +- `Nip05Validator` now creates `HttpClient` instances directly via a `Function` factory (previously used deleted `HttpClientProvider`/`DefaultHttpClientProvider` interface). + +## [2.0.0] - 2026-02-24 + +This is a major release that implements the full design simplification described in `docs/developer/SIMPLIFICATION_PROPOSAL.md`, reducing the library from 9 modules with ~180 classes to 4 modules with ~40 classes. + +### Added +- `Kinds` utility class with static `int` constants for common Nostr event kinds (`TEXT_NOTE`, `SET_METADATA`, `CONTACT_LIST`, etc.) and range-check methods (`isReplaceable()`, `isEphemeral()`, `isAddressable()`, `isValid()`). +- `GenericTag.of(String code, String... params)` factory method for concise tag creation. +- `GenericTag.toArray()` returning the NIP-01 wire format `["code", "param0", "param1", ...]`. +- `GenericTag` now stores tag values as `List` (replacing `List`), providing direct access via `getParams()`. +- `EventFilter` builder API for composable relay filters: `.kinds()`, `.authors()`, `.since()`, `.until()`, `.addTagFilter()`, `.limit()`, `.ids()`. +- `RelayTimeoutException` — typed exception replacing silent empty-list returns on relay timeout. +- `ConnectionState` enum (`CONNECTING`, `CONNECTED`, `RECONNECTING`, `CLOSED`) for WebSocket connection state tracking. +- `NostrRelayClient` async Virtual Thread APIs: `connectAsync(...)`, `sendAsync(...)`, and `subscribeAsync(...)`. +- `Nip05Validator.validateAsync()` and `Nip05Validator.validateBatch(...)` for parallel NIP-05 validation workloads. +- Spring Retry support (`@NostrRetryable`, `@Recover`) consolidated directly into `NostrRelayClient`. + +### Changed +- **Module consolidation** — merged 9 modules into 4: + - `nostr-java-util` + `nostr-java-crypto` → `nostr-java-core` + - `nostr-java-base` + `nostr-java-event` → `nostr-java-event` + - `nostr-java-id` + `nostr-java-encryption` → `nostr-java-identity` + - `nostr-java-client` (unchanged) +- **`GenericEvent`** is now the sole event class. All 39 concrete event subclasses removed. Events are differentiated by `int kind` instead of Java type. +- **`GenericTag`** is now the sole tag class. All 17 concrete tag subclasses removed. Tags are a simple `code` + `List params`. +- `GenericEvent.kind` changed from `Kind` enum to plain `int`. Builder simplified to `.kind(int)` only. +- `GenericEvent.tags` changed from `List` to `List`. +- `GenericEvent` implements `ISignable` directly (no longer extends `BaseEvent`). +- `EventMessage` now references `GenericEvent` directly instead of `IEvent`. +- `IDecoder` type bound changed from `IDecoder` to unbounded `IDecoder`. +- `TagDeserializer` now always produces `GenericTag` with `List` params — no registry dispatch. +- `GenericTagSerializer` simplified to output `[code, param0, param1, ...]` directly from `List`. +- `GenericEventDeserializer` simplified — no subclass dispatch to concrete event types. +- `NostrUtil.bytesToHex()` now uses `java.util.HexFormat` instead of hand-rolled hex encoding. +- `NostrUtil.hexToBytes()` family of methods now uses `java.util.HexFormat.parseHex()` — fails fast on invalid hex instead of silently producing corrupt bytes. +- WebSocket client termination detection now uses proper JSON parsing instead of brittle string-prefix matching. +- `StandardWebSocketClient` renamed to `NostrRelayClient`. +- `SpringWebSocketClient` absorbed into `NostrRelayClient` (single client class with retry support). +- Relay subscription callbacks are now dispatched on Virtual Threads to avoid blocking inbound WebSocket processing. +- `DefaultHttpClientProvider` now uses a shared Virtual Thread executor instead of creating a new executor per `HttpClient`. +- All `synchronized` blocks in `NostrRelayClient` replaced with `ReentrantLock` to avoid Virtual Thread pinning. +- Configurable max events per request limit (default 10,000) to prevent unbounded memory accumulation. + +### Fixed +- **`GenericTag.getCode()` NPE** — structurally eliminated by removing the dual-path tag architecture. `getCode()` is now a trivial field accessor with zero NPE risk. +- Removed the remaining `synchronized` cleanup block from `NostrRelayClient.send(...)`, using `ReentrantLock` consistently to avoid VT pinning risk. +- Relay timeout now throws `RelayTimeoutException` instead of silently returning an empty list, allowing callers to distinguish "no results" from "timed out". + +### Removed +- **`nostr-java-api` module** — all 26 NIP classes (NIP01–NIP99), `EventNostr`, factory classes, client managers, service layer, and configuration classes. +- **`nostr-java-examples` module** — all 6 example classes. +- **39 concrete event subclasses** — `TextNoteEvent`, `DirectMessageEvent`, `ContactListEvent`, `ReactionEvent`, `DeletionEvent`, `EphemeralEvent`, `ReplaceableEvent`, `AddressableEvent`, all Calendar/Marketplace/Channel/NostrConnect events, and more. Use `GenericEvent` with the appropriate `int kind`. +- **17 concrete tag subclasses** — `EventTag`, `PubKeyTag`, `AddressTag`, `IdentifierTag`, `ReferenceTag`, `HashtagTag`, `ExpirationTag`, `UrlTag`, `SubjectTag`, `DelegationTag`, `RelaysTag`, `NonceTag`, `PriceTag`, `EmojiTag`, `GeohashTag`, `LabelTag`, `LabelNamespaceTag`, `VoteTag`. Use `GenericTag.of(code, params...)`. +- **27 entity classes** — `UserProfile`, `Profile`, `ChannelProfile`, `ZapRequest`, `ZapReceipt`, `Reaction`, all Cashu entities, all marketplace entities, and more. +- **`Kind` enum** — replaced by `Kinds` utility class with static `int` constants. +- **`ElementAttribute`** — replaced by `List` in `GenericTag`. +- **`TagRegistry`** — no longer needed with a single tag class. +- **Interfaces and abstract classes**: `IElement`, `ITag`, `IEvent`, `IGenericElement`, `IBech32Encodable`, `Deleteable`, `BaseEvent`, `BaseTag`. +- **Annotations**: `@Tag`, `@Event`, `@Key`. +- **14 filter classes** — `AbstractFilterable`, `KindFilter`, `AuthorFilter`, `SinceFilter`, `UntilFilter`, `HashtagTagFilter`, `AddressTagFilter`, `GeohashTagFilter`, `IdentifierTagFilter`, `ReferencedEventFilter`, `ReferencedPublicKeyFilter`, `UrlTagFilter`, `VoteTagFilter`, `GenericTagQueryFilter`. Use `EventFilter.builder()`. +- **Concrete serializers/deserializers** — `AddressTagSerializer`, `ReferenceTagSerializer`, `ExpirationTagSerializer`, `IdentifierTagSerializer`, `RelaysTagSerializer`, `BaseTagSerializer`, `AbstractTagSerializer`, `CalendarEventDeserializer`, `ClassifiedListingEventDeserializer`, `CashuTokenSerializer`. +- **Client classes** — `WebSocketClientIF`, `WebSocketClientFactory`, `SpringWebSocketClientFactory`, `SpringWebSocketClient`. Use `NostrRelayClient` directly. +- **Dead code** — `Marker` enum, `RelayUri` value object. +- **5 old modules** — `nostr-java-util`, `nostr-java-crypto`, `nostr-java-base`, `nostr-java-id`, `nostr-java-encryption` (merged into the 4 remaining modules). +- Dead `pollIntervalMs` parameter from WebSocket client constructors. ## [1.3.0] - 2026-01-25 diff --git a/CLAUDE.md b/CLAUDE.md index e32d52855..1cec768c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,25 +4,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -`nostr-java` is a Java SDK for the Nostr protocol. It provides utilities for creating, signing, and publishing Nostr events to relays. The project implements 20+ Nostr Implementation Possibilities (NIPs). +`nostr-java` is a Java SDK for the Nostr protocol. It provides utilities for creating, signing, and publishing Nostr events to relays. - **Language**: Java 21+ - **Build Tool**: Maven -- **Architecture**: Multi-module Maven project with 9 modules +- **Architecture**: Multi-module Maven project with 4 modules ## Module Architecture -The codebase follows a layered dependency structure. Understanding this hierarchy is essential for making changes: +The codebase follows a layered dependency structure: -1. **nostr-java-util** – Foundation utilities (no dependencies on other modules) -2. **nostr-java-crypto** – BIP340 Schnorr signatures (depends on util) -3. **nostr-java-base** – Common model classes (depends on crypto, util) -4. **nostr-java-event** – Event and tag definitions (depends on base, crypto, util) -5. **nostr-java-id** – Identity and key handling (depends on base, crypto) -6. **nostr-java-encryption** – Message encryption (depends on base, crypto, id) -7. **nostr-java-client** – WebSocket relay client (depends on event, base) -8. **nostr-java-api** – High-level API (depends on all above) -9. **nostr-java-examples** – Sample applications (depends on api) +1. **nostr-java-core** – Foundation utilities and BIP340 Schnorr cryptography (packages: `nostr.util`, `nostr.crypto`) +2. **nostr-java-event** – Event model, tags, filters, serialization, and base types (packages: `nostr.event`, `nostr.base`) +3. **nostr-java-identity** – Identity/key management and encryption (packages: `nostr.id`, `nostr.encryption`) +4. **nostr-java-client** – WebSocket relay client with Spring support (packages: `nostr.client`) + +**Dependency chain**: `core → event → identity → client` **Key principle**: Lower-level modules cannot depend on higher-level ones. When adding features, place code at the lowest appropriate level. @@ -37,17 +34,11 @@ mvn clean test # Run integration tests (requires Docker for Testcontainers) mvn clean verify -# Run integration tests with verbose output -mvn -q verify - # Install artifacts without tests mvn install -Dmaven.test.skip=true # Run a specific test class mvn -q test -Dtest=GenericEventBuilderTest - -# Run a specific test method -mvn -q test -Dtest=GenericEventBuilderTest#testSpecificMethod ``` ### Code Quality @@ -65,29 +56,35 @@ mvn verify ### Event System -- **GenericEvent** (`nostr-java-event/src/main/java/nostr/event/impl/GenericEvent.java`) is the core event class +- **GenericEvent** is the single event class for all Nostr event kinds +- Events use `int kind` values; common kinds defined as constants in `Kinds` utility class - Events can be built using: - - Direct constructors with `PublicKey` and `Kind`/`Integer` + - Direct constructors with `PublicKey` and `int kind` - Static `GenericEvent.builder()` for flexible construction - All events must be signed before sending to relays -- Events support both NIP-defined kinds (via `Kind` enum) and custom kinds (via `Integer`) -### Client Architecture +### Tag System -Two WebSocket client implementations: +- **GenericTag** is the single tag class, holding `code` + `List params` +- Factory: `GenericTag.of("p", pubkeyHex, relayUrl)` or `BaseTag.create("e", eventId)` +- Serialized as JSON arrays: `["code", "param0", "param1", ...]` -1. **StandardWebSocketClient** – Blocking, waits for relay responses with configurable timeout -2. **NostrSpringWebSocketClient** – Non-blocking with Spring WebSocket and retry support (3 retries, exponential backoff from 500ms) +### Filter System -Configuration properties: -- `nostr.websocket.await-timeout-ms=60000` -- `nostr.websocket.poll-interval-ms=500` +- **EventFilter** with builder pattern for composable query filters +- Supports `ids`, `authors`, `kinds`, `since`, `until`, `limit`, and tag filters via `.addTagFilter()` +- **Filters** holds a `List` for REQ messages -### Tag System +### Client Architecture -- Tags are represented by `BaseTag` and subclasses -- Custom tags can be registered via `TagRegistry` -- Serialization/deserialization handled by Jackson with custom serializers in `nostr.event.json.serializer` +- **NostrRelayClient** – Blocking send with configurable timeout, streaming subscribe, Spring Retry (3 attempts, exponential backoff) +- Throws `RelayTimeoutException` on timeout (instead of returning empty list) +- Tracks `ConnectionState` (CONNECTING, CONNECTED, RECONNECTING, CLOSED) + +Configuration properties: +- `nostr.websocket.await-timeout-ms=60000` +- `nostr.websocket.max-idle-timeout-ms=3600000` +- `nostr.websocket.max-events-per-request=10000` ### Identity and Signing @@ -96,25 +93,10 @@ Configuration properties: - Signing uses Schnorr signatures (BIP340) - Public keys use Bech32 encoding (npub prefix) -## NIPs Implementation - -The codebase implements NIPs through dedicated classes in `nostr-java-api`: -- NIP classes (e.g., `NIP01`, `NIP04`, `NIP25`) provide builder methods and utilities -- Event implementations in `nostr-java-event/src/main/java/nostr/event/impl/` -- Refer to `.github/copilot-instructions.md` for the full NIP specification links - -When implementing new NIP support: -1. Add event class in `nostr-java-event` if needed -2. Create NIP helper class in `nostr-java-api` -3. Add tests in both modules -4. Update README.md with NIP reference -5. Add example in `nostr-java-examples` - ## Testing Strategy - **Unit tests** (`*Test.java`): No external dependencies, use mocks - **Integration tests** (`*IT.java`): Use Testcontainers to start `nostr-rs-relay` -- Relay container image can be overridden in `src/test/resources/relay-container.properties` - Integration tests may be retried once on failure (configured in failsafe plugin) ## Code Standards @@ -129,46 +111,261 @@ When implementing new NIP support: ## Dependency Management -- **BOM**: `nostr-java-bom` (version 1.1.1) manages all dependency versions +- **BOM**: `nostr-java-bom` manages all dependency versions - Root `pom.xml` includes temporary module version overrides until next BOM release - Never add version numbers to dependencies in child modules – let the BOM manage versions -## Documentation - -Comprehensive documentation in `docs/`: -- `docs/GETTING_STARTED.md` – Installation and setup -- `docs/howto/use-nostr-java-api.md` – API usage guide -- `docs/howto/streaming-subscriptions.md` – Subscription management -- `docs/howto/custom-events.md` – Creating custom event types -- `docs/reference/nostr-java-api.md` – API reference -- `docs/CODEBASE_OVERVIEW.md` – Module layout and build instructions - ## Common Patterns and Gotchas ### Event Building ```java -// Using builder for custom kinds +// Using builder GenericEvent event = GenericEvent.builder() - .kind(customKindInteger) + .kind(Kinds.TEXT_NOTE) .content("content") .pubKey(publicKey) .build(); -// Using constructor for standard kinds -GenericEvent event = new GenericEvent(pubKey, Kind.TEXT_NOTE); +// Using constructor +GenericEvent event = new GenericEvent(pubKey, Kinds.TEXT_NOTE); ``` -### Signing and Sending +### Tags ```java -// Sign and send pattern -EventNostr nostr = new NIP01(identity); -nostr.createTextNote("Hello Nostr!") - .sign() - .send(relays); +// Create tags +GenericTag tag = GenericTag.of("p", pubkeyHex, "wss://relay.example.com"); +GenericTag hashtag = GenericTag.of("t", "nostr"); ``` -### Custom Tags -Register custom tags in `TagRegistry` before deserializing events that contain them. +### Filters +```java +// Build a filter +EventFilter filter = EventFilter.builder() + .kind(Kinds.TEXT_NOTE) + .author(pubkeyHex) + .since(timestamp) + .limit(100) + .build(); +``` ### WebSocket Sessions Spring WebSocket client maintains persistent connections. Always close subscriptions properly to avoid resource leaks. + +### Use Virtual Threads for Concurrency + +This project uses Java 21 Virtual Threads (Project Loom) for efficient concurrency. Virtual Threads are enabled by default via `spring.threads.virtual.enabled=true` in the gateway. **Always prefer Virtual Threads over platform threads for I/O-bound work.** + +#### When to Use Virtual Threads + +| Scenario | Use Virtual Threads? | Pattern | +|----------|---------------------|---------| +| Mint API calls (mint, melt, swap) | Yes | `CompletableFuture` with VT executor | +| Database queries (gateway) | Yes | Parallel queries with VT executor | +| Nostr relay operations | Yes | Parallel publish/fetch across relays | +| Nostrdb queries | Yes | VT handles LMDB blocking efficiently | +| SSE event delivery | Yes | Spring WebFlux handles this automatically | +| File I/O (wallet storage) | Yes | VT handles blocking efficiently | +| Cryptographic operations (signing) | No | CPU-bound, use parallel streams | +| Quick in-memory operations | No | Overhead not justified | + +#### Patterns and Examples + +**1. Parallel I/O with CompletableFuture and VT Executor (Preferred)** + +Use when you need results from multiple independent I/O operations: + +```java +import java.util.concurrent.*; + +// Parallel mint API queries with Virtual Threads +private List fetchQuoteStatuses(List quoteIds, MintClient mintClient) { + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + List> futures = quoteIds.stream() + .map(id -> CompletableFuture.supplyAsync(() -> { + return mintClient.getMintQuoteStatus(id); + }, executor)) + .toList(); + + // Wait for all futures to complete + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + + return futures.stream() + .map(f -> f.getNow(null)) + .filter(Objects::nonNull) + .toList(); + } +} +``` + +**2. Parallel Nostr Relay Operations** + +Use when publishing or fetching from multiple relays: + +```java +// Publish event to multiple relays in parallel +private Map publishToRelays(NostrEvent event, List relayUrls) { + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + Map> futures = new ConcurrentHashMap<>(); + + for (String relayUrl : relayUrls) { + futures.put(relayUrl, CompletableFuture.supplyAsync(() -> { + try { + return nostrClient.publish(relayUrl, event); + } catch (Exception e) { + log.warn("Failed to publish to {}: {}", relayUrl, e.getMessage()); + return false; + } + }, executor)); + } + + CompletableFuture.allOf(futures.values().toArray(new CompletableFuture[0])).join(); + + return futures.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getNow(false))); + } +} +``` + +**3. Fire-and-Forget with @Async** + +Use for event handlers that shouldn't block the caller: + +```java +@Async // Runs on VT via AsyncConfig +@EventListener +public void onWalletUpdate(WalletUpdateEvent event) { + // Sync to Nostr in background + nostrSyncService.syncWalletState(event.getWalletId()); +} +``` + +**4. Parallel Database Queries in Gateway** + +Use for fetching related entities: + +```java +// Parallel fetch of user data from multiple tables +private UserProfile loadFullProfile(String pubkey) { + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + var profileFuture = CompletableFuture.supplyAsync( + () -> profileRepository.findByPubkey(pubkey), executor); + var walletsFuture = CompletableFuture.supplyAsync( + () -> walletRepository.findByOwnerPubkey(pubkey), executor); + var settingsFuture = CompletableFuture.supplyAsync( + () -> settingsRepository.findByPubkey(pubkey), executor); + + CompletableFuture.allOf(profileFuture, walletsFuture, settingsFuture).join(); + + return UserProfile.builder() + .profile(profileFuture.getNow(null)) + .wallets(walletsFuture.getNow(List.of())) + .settings(settingsFuture.getNow(null)) + .build(); + } +} +``` + +**5. Parallel Mint Swaps** + +Use when swapping tokens across multiple mints: + +```java +// Parallel swaps when consolidating tokens from multiple mints +private List parallelSwap(List requests) { + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + List> futures = requests.stream() + .map(req -> CompletableFuture.supplyAsync(() -> { + try { + return mintClient.swap(req.getMintUrl(), req.getProofs(), req.getOutputs()); + } catch (Exception e) { + return SwapResult.failed(req.getMintUrl(), e.getMessage()); + } + }, executor)) + .toList(); + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + + return futures.stream() + .map(f -> f.getNow(SwapResult.failed("unknown", "timeout"))) + .toList(); + } +} +``` + +#### Anti-Patterns to Avoid + +**❌ Sequential I/O in loops when items are independent:** +```java +// BAD: Sequential blocking calls +for (String mintUrl : mintUrls) { + var keysets = mintClient.getKeysets(mintUrl); // Blocks + // ... +} +``` + +**❌ Using synchronized for I/O operations (causes VT pinning):** +```java +// BAD: Pins virtual thread to carrier thread +synchronized (lock) { + database.query(...); // Pinned during entire I/O! +} + +// GOOD: Use ReentrantLock instead +private final ReentrantLock lock = new ReentrantLock(); +lock.lock(); +try { + database.query(...); // VT can unmount during I/O +} finally { + lock.unlock(); +} +``` + +**❌ Creating platform thread pools for I/O work:** +```java +// BAD: Wastes platform threads on I/O +ExecutorService pool = Executors.newFixedThreadPool(10); + +// GOOD: Use virtual thread executor +ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor(); +``` + +**❌ Blocking the SSE thread:** +```java +// BAD: Blocks SSE connection during mint call +@GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE) +public Flux events() { + var status = mintClient.checkStatus(...); // Blocks! + return Flux.just(Event.of(status)); +} + +// GOOD: Use reactive operators +@GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE) +public Flux events() { + return Mono.fromCallable(() -> mintClient.checkStatus(...)) + .subscribeOn(Schedulers.boundedElastic()) + .map(Event::of) + .flux(); +} +``` + +#### VT Configuration Reference + +| Component | Configuration | Purpose | +|-----------|--------------|---------| +| Spring Boot | `spring.threads.virtual.enabled=true` | Use VT for request handling | +| Tomcat | `server.tomcat.threads.max=50` | Reduced (VTs handle concurrency) | +| `@Async` | `AsyncConfig` bean | VT executor for async methods | +| HTTP Client | `JdkClientHttpRequestFactory` | VT-friendly HTTP client | + +#### Debugging Virtual Threads + +```bash +# Enable VT debugging output +-Djdk.tracePinnedThreads=full + +# Check for pinning in logs +grep -i "pinned" logs/application.log + +# Monitor virtual thread count +jcmd Thread.dump_to_file -format=json threads.json +``` diff --git a/MIGRATION.md b/MIGRATION.md index ef969b6ce..2d86e391f 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,24 +1,151 @@ # Migration Guide -This guide helps you migrate between major versions of nostr-java, detailing breaking changes and deprecated API replacements. +This guide helps you migrate between major versions of nostr-java, detailing breaking changes and API replacements. --- ## Table of Contents -- [Migrating to 1.0.0](#migrating-to-100) - - [Deprecated APIs Removed](#deprecated-apis-removed) - - [Breaking Changes](#breaking-changes) -- [Migrating from 0.6.x](#migrating-from-06x) - - [Event Kind Constants](#event-kind-constants) - - [ObjectMapper Usage](#objectmapper-usage) - - [NIP01 API Changes](#nip01-api-changes) +- [Migrating to 2.0.0](#migrating-to-200) +- [Migrating to 1.0.0](#migrating-to-100) (historical) --- -## Migrating to 1.0.0 +## Migrating to 2.0.0 -**Status:** Planned for future release +**Status:** Released 2026-02-24 + +Version 2.0.0 is a major simplification that reduces the library from 9 modules (~180 classes) to 4 modules (~40 classes). See [CHANGELOG.md](CHANGELOG.md) for the full list of changes and [docs/developer/SIMPLIFICATION_PROPOSAL.md](docs/developer/SIMPLIFICATION_PROPOSAL.md) for the design rationale. + +### Module changes + +| Old Module | New Module | Notes | +|---|---|---| +| `nostr-java-util` + `nostr-java-crypto` | `nostr-java-core` | Merged | +| `nostr-java-base` + `nostr-java-event` | `nostr-java-event` | Merged | +| `nostr-java-id` + `nostr-java-encryption` | `nostr-java-identity` | Merged | +| `nostr-java-client` | `nostr-java-client` | Unchanged | +| `nostr-java-api` | (removed) | Use `GenericEvent` directly | +| `nostr-java-examples` | (removed) | See docs/howto guides | + +**Dependency:** Replace `nostr-java-api` with `nostr-java-client` (transitively includes all modules): + +```xml + +nostr-java-api + + +nostr-java-client +``` + +### Event creation + +```java +// Before (1.x) +NIP01 nip01 = new NIP01(identity); +nip01.createTextNoteEvent("Hello Nostr!") + .sign() + .send(relays); + +// After (2.0) +GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Hello Nostr!") + .build(); +identity.sign(event); +try (NostrRelayClient client = new NostrRelayClient("wss://relay.398ja.xyz")) { + client.send(new EventMessage(event)); +} +``` + +### Tags + +```java +// Before (1.x) +EventTag eTag = new EventTag("abc123", "wss://relay.example.com", Marker.REPLY); +PubKeyTag pTag = new PubKeyTag(recipientPublicKey); +HashtagTag hTag = new HashtagTag("nostr"); + +// After (2.0) +GenericTag.of("e", "abc123", "wss://relay.example.com", "reply") +GenericTag.of("p", recipientPublicKey.toString()) +GenericTag.of("t", "nostr") +``` + +### Kind values + +```java +// Before (1.x) +Kind.TEXT_NOTE // Kind enum +Kind.TEXT_NOTE.getValue() // to get int + +// After (2.0) +Kinds.TEXT_NOTE // static int constant (value: 1) +``` + +### Filters + +```java +// Before (1.x) +new Filters( + new KindFilter<>(Kind.TEXT_NOTE), + new AuthorFilter<>(pubKey), + new SinceFilter(timestamp) +); + +// After (2.0) +EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE)) + .authors(List.of(pubKeyHex)) + .since(timestamp) + .build(); +``` + +### WebSocket client + +```java +// Before (1.x) — StandardWebSocketClient or SpringWebSocketClient +StandardWebSocketClient client = new StandardWebSocketClient("wss://relay.example.com"); +// or: SpringWebSocketClient client = new SpringWebSocketClient(wsClient); + +// After (2.0) — NostrRelayClient (with retry, VT dispatch, async APIs) +NostrRelayClient client = new NostrRelayClient("wss://relay.example.com"); +// Async: +NostrRelayClient.connectAsync("wss://relay.example.com"); +``` + +### Timeout handling + +```java +// Before (1.x) — silent empty list on timeout +List events = client.send(message); // returns empty list on timeout + +// After (2.0) — throws RelayTimeoutException +try { + List events = client.send(message); +} catch (RelayTimeoutException e) { + // explicit timeout handling +} +``` + +### Removed classes (summary) + +- All 26 NIP classes (`NIP01`–`NIP99`) and `EventNostr` +- All 39 concrete event subclasses (`TextNoteEvent`, `ReactionEvent`, etc.) +- All 17 concrete tag subclasses (`EventTag`, `PubKeyTag`, etc.) +- All 27 entity classes (`UserProfile`, `ZapRequest`, etc.) +- `Kind` enum, `ElementAttribute`, `TagRegistry`, `BaseTag`, `BaseEvent` +- `IElement`, `ITag`, `IEvent`, `IGenericElement`, `IBech32Encodable`, `Deleteable` +- `@Tag`, `@Event`, `@Key` annotations +- 14 filter classes (`KindFilter`, `AuthorFilter`, etc.) +- `WebSocketClientIF`, `WebSocketClientFactory`, `SpringWebSocketClientFactory`, `SpringWebSocketClient` + +--- + +## Migrating to 1.0.0 (Historical) + +**Status:** Released 2025-10-13 **Deprecation Warnings Since:** 0.6.2 Version 1.0.0 will remove all APIs deprecated in the 0.6.x series. This guide helps you prepare your codebase for a smooth upgrade. diff --git a/README.md b/README.md index e6c5e9bc6..e069f1bdc 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,38 @@ See [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) for installation and usage instructions. +## Quick Start + +```java +Identity identity = Identity.generateRandomIdentity(); + +GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Hello Nostr!") + .tags(List.of(GenericTag.of("t", "nostr-java"))) + .build(); + +identity.sign(event); + +try (NostrRelayClient client = new NostrRelayClient("wss://relay.398ja.xyz")) { + client.send(new EventMessage(event)); +} +``` + +## Module Architecture + +4 modules with a strict dependency chain: + +``` +nostr-java-core → nostr-java-event → nostr-java-identity → nostr-java-client +``` + +- **nostr-java-core** — Foundation utilities, BIP-340 Schnorr cryptography, Bech32 encoding, hex conversion +- **nostr-java-event** — `GenericEvent`, `GenericTag`, `Kinds` constants, `EventFilter` builder, messages, JSON serialization +- **nostr-java-identity** — `Identity` key management, event signing, NIP-04/NIP-44 encryption +- **nostr-java-client** — `NostrRelayClient` WebSocket client with retry, Virtual Threads, and async APIs + ## Running Tests - Full test suite (requires Docker for Testcontainers ITs): @@ -24,105 +56,55 @@ See [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) for installation and usag `mvn -q -Pno-docker verify` -The `no-docker` profile excludes tests under `**/nostr/api/integration/**` and sets `noDocker=true` for conditional test disabling. - ## Troubleshooting -For diagnosing relay send issues and capturing failure details, see the how‑to guide: [docs/howto/diagnostics.md](docs/howto/diagnostics.md). +For diagnosing relay send issues and capturing failure details, see the how-to guide: [docs/howto/diagnostics.md](docs/howto/diagnostics.md). ## Documentation - Docs index: [docs/README.md](docs/README.md) — quick entry point to all guides and references. -- Operations: [docs/operations/README.md](docs/operations/README.md) — logging, metrics, configuration, diagnostics. - Getting started: [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) — install via Maven/Gradle and build from source. -- API how‑to: [docs/howto/use-nostr-java-api.md](docs/howto/use-nostr-java-api.md) — create, sign, and publish basic events. -- Streaming subscriptions: [docs/howto/streaming-subscriptions.md](docs/howto/streaming-subscriptions.md) — open and manage long‑lived, non‑blocking subscriptions. -- Custom events how‑to: [docs/howto/custom-events.md](docs/howto/custom-events.md) — define, sign, and send custom event types. +- API how-to: [docs/howto/use-nostr-java-api.md](docs/howto/use-nostr-java-api.md) — create, sign, and publish events. +- Streaming subscriptions: [docs/howto/streaming-subscriptions.md](docs/howto/streaming-subscriptions.md) — open and manage long-lived, non-blocking subscriptions. +- Custom events: [docs/howto/custom-events.md](docs/howto/custom-events.md) — working with custom event kinds. - API reference: [docs/reference/nostr-java-api.md](docs/reference/nostr-java-api.md) — classes, key methods, and short examples. -- Extending events: [docs/explanation/extending-events.md](docs/explanation/extending-events.md) — guidance for extending the event model. -- Codebase overview and contributing: [docs/CODEBASE_OVERVIEW.md](docs/CODEBASE_OVERVIEW.md) — layout, testing, and contribution workflow. +- Events and tags: [docs/explanation/extending-events.md](docs/explanation/extending-events.md) — in-depth guide to GenericEvent and GenericTag. +- Architecture: [docs/explanation/architecture.md](docs/explanation/architecture.md) — module design and data flow. +- Codebase overview: [docs/CODEBASE_OVERVIEW.md](docs/CODEBASE_OVERVIEW.md) — layout, testing, and contribution workflow. +- Operations: [docs/operations/README.md](docs/operations/README.md) — logging, metrics, configuration, diagnostics. -## Examples +## Features -Examples are located in the [`nostr-java-examples`](./nostr-java-examples) module. See the [API Examples Guide](docs/howto/api-examples.md) for detailed walkthroughs. +- **Minimal API surface** — one event class (`GenericEvent`), one tag class (`GenericTag`), ~40 total classes +- **Protocol-aligned** — kinds are integers, tags are string arrays, no library-imposed type hierarchy +- **Virtual Thread concurrency** — relay I/O and listener dispatch on Java 21 Virtual Threads +- **Async APIs** — `connectAsync()`, `sendAsync()`, `subscribeAsync()` via `CompletableFuture` +- **Reliable connectivity** — Spring Retry, typed `RelayTimeoutException`, connection state tracking +- **NIP-04/NIP-44 encryption** — legacy and modern message encryption +- **BIP-340 Schnorr signatures** — event signing and verification +- **Well-documented** — architecture guides, how-to guides, and API reference -### Key Examples +## v2.0.0 Highlights -- [`NostrApiExamples`](nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java) – Comprehensive examples covering 13+ use cases including text notes, encrypted DMs, reactions, channels, and more. See the [guide](docs/howto/api-examples.md) for details. +- Simplified from 9 modules (~180 classes) to 4 modules (~40 classes) +- `GenericEvent` is the sole event class for all kinds — no subclasses +- `GenericTag` stores tags as `code` + `List` — no `ElementAttribute`, no `TagRegistry` +- `Kinds` utility replaces the `Kind` enum — any integer is valid +- `EventFilter` builder replaces 14 thin filter wrapper classes +- `NostrRelayClient` with Virtual Thread dispatch and async APIs +- `RelayTimeoutException` replaces silent empty-list timeout returns +- `java.util.HexFormat` replaces hand-rolled hex encoding -- [`SpringSubscriptionExample`](nostr-java-examples/src/main/java/nostr/examples/SpringSubscriptionExample.java) – Shows how to open a non-blocking `NostrSpringWebSocketClient` subscription and close it after a fixed duration. - -## Features +See [CHANGELOG.md](CHANGELOG.md) for the full list of changes. + +## NIP Support -- ✅ **Clean Architecture** - Modular design following SOLID principles -- ✅ **Comprehensive NIP Support** - 25 NIPs implemented covering core protocol, encryption, payments, and more -- ✅ **Type-Safe API** - Strongly-typed events, tags, and messages with builder patterns -- ✅ **Non-Blocking Subscriptions** - Spring WebSocket client with reactive streaming support -- ✅ **Well-Documented** - Extensive JavaDoc, architecture guides, and code examples -- ✅ **Production-Ready** - High test coverage, CI/CD pipeline, code quality checks - -## Recent Improvements (v1.0.0) - -🎯 **API Cleanup & Removals (breaking)** -- Deprecated APIs removed: `Constants.Kind`, `Encoder.ENCODER_MAPPER_BLACKBIRD`, and NIP01 Identity-based overloads -- NIP01 now exclusively uses the instance-configured sender; builder simplified accordingly - -🚀 **Performance & Serialization** -- Centralized JSON mapper via `nostr.event.json.EventJsonMapper` (Blackbird module); unified across event encoders - -📚 **Documentation & Structure** -- Migration guide updated for 1.0.0 removals and replacements -- Troubleshooting moved to dedicated how‑to: `docs/howto/diagnostics.md` -- README streamlined to focus on users; maintainer topics moved under docs - -🛠️ **Build & Release Tooling** -- CI workflow split for Docker vs no‑Docker runs -- Release automation (`scripts/release.sh`) with bump/tag/verify/publish steps - -See [docs/explanation/architecture.md](docs/explanation/architecture.md) for detailed architecture overview. - -## Supported NIPs - -**25 NIPs implemented** - comprehensive coverage of core protocol, security, and advanced features. - -### NIP Compliance Matrix - -| Category | NIP | Description | Status | -|----------|-----|-------------|--------| -| **Core Protocol** | [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) | Basic protocol flow | ✅ Complete | -| | [NIP-02](https://github.com/nostr-protocol/nips/blob/master/02.md) | Follow List | ✅ Complete | -| | [NIP-12](https://github.com/nostr-protocol/nips/blob/master/12.md) | Generic Tag Queries | ✅ Complete | -| | [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) | Bech32 encoding | ✅ Complete | -| | [NIP-20](https://github.com/nostr-protocol/nips/blob/master/20.md) | Command Results | ✅ Complete | -| **Security & Identity** | [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md) | DNS-based identifiers | ✅ Complete | -| | [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md) | Client authentication | ✅ Complete | -| | [NIP-46](https://github.com/nostr-protocol/nips/blob/master/46.md) | Remote signing | ✅ Complete | -| **Encryption** | [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) | Encrypted DMs | ✅ Complete | -| | [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md) | Versioned encryption | ✅ Complete | -| **Content Types** | [NIP-08](https://github.com/nostr-protocol/nips/blob/master/08.md) | Handling Mentions | ✅ Complete | -| | [NIP-09](https://github.com/nostr-protocol/nips/blob/master/09.md) | Event Deletion | ✅ Complete | -| | [NIP-14](https://github.com/nostr-protocol/nips/blob/master/14.md) | Subject tags | ✅ Complete | -| | [NIP-23](https://github.com/nostr-protocol/nips/blob/master/23.md) | Long-form content | ✅ Complete | -| | [NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md) | Reactions | ✅ Complete | -| | [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md) | Public Chat | ✅ Complete | -| | [NIP-30](https://github.com/nostr-protocol/nips/blob/master/30.md) | Custom Emoji | ✅ Complete | -| | [NIP-32](https://github.com/nostr-protocol/nips/blob/master/32.md) | Labeling | ✅ Complete | -| | [NIP-52](https://github.com/nostr-protocol/nips/blob/master/52.md) | Calendar Events | ✅ Complete | -| **Commerce & Payments** | [NIP-15](https://github.com/nostr-protocol/nips/blob/master/15.md) | Marketplace | ✅ Complete | -| | [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md) | Lightning Zaps | ✅ Complete | -| | [NIP-60](https://github.com/nostr-protocol/nips/blob/master/60.md) | Cashu Wallets | ✅ Complete | -| | [NIP-61](https://github.com/nostr-protocol/nips/blob/master/61.md) | Nutzaps | ✅ Complete | -| | [NIP-99](https://github.com/nostr-protocol/nips/blob/master/99.md) | Classified Listings | ✅ Complete | -| **Utilities** | [NIP-03](https://github.com/nostr-protocol/nips/blob/master/03.md) | OpenTimestamps | ✅ Complete | -| | [NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md) | Expiration Timestamp | ✅ Complete | - -**Coverage:** 25/100+ NIPs (core protocol + most commonly used extensions) +The library is NIP-agnostic by design. Any current or future NIP can be implemented using `GenericEvent.builder().kind(kindNumber)` with appropriate tags via `GenericTag.of(code, params...)` — no library updates required. The `Kinds` utility class provides named constants for commonly used kind values. ## Contributing Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for: - Coding standards and conventions -- How to add new NIPs - Pull request guidelines - Testing requirements diff --git a/docs/CODEBASE_OVERVIEW.md b/docs/CODEBASE_OVERVIEW.md index 50378a66e..2cefd3e3c 100644 --- a/docs/CODEBASE_OVERVIEW.md +++ b/docs/CODEBASE_OVERVIEW.md @@ -1,61 +1,71 @@ # Codebase Overview -Navigation: [Docs index](README.md) · [Getting started](GETTING_STARTED.md) · [API how‑to](howto/use-nostr-java-api.md) · [API reference](reference/nostr-java-api.md) +Navigation: [Docs index](README.md) · [Getting started](GETTING_STARTED.md) · [API how-to](howto/use-nostr-java-api.md) · [API reference](reference/nostr-java-api.md) This document provides an overview of the project structure and instructions for building and testing the modules. ## Module layout -- **nostr-java-base** – common model classes and utilities used across the project. -- **nostr-java-crypto** – pure Java implementation of BIP340 Schnorr signature test vectors. -- **nostr-java-event** – definitions of Nostr events and tags. -- **nostr-java-id** – identity generation and handling of keys. -- **nostr-java-util** – helper utilities used by other modules. -- **nostr-java-client** – WebSocket client used to communicate with relays. -- **nostr-java-api** – high level API wrapping event creation and relay communication. -- **nostr-java-encryption** – optional encryption support for messages. -- **nostr-java-examples** – sample applications demonstrating how to use the API. + +nostr-java 2.0 has 4 modules with a clear dependency chain: + +``` +nostr-java-core → nostr-java-event → nostr-java-identity → nostr-java-client +``` + +- **nostr-java-core** — Foundation utilities, BIP-340 Schnorr cryptography, Bech32 encoding, hex conversion (`java.util.HexFormat`), validators, and exception hierarchy. No dependencies on other project modules. +- **nostr-java-event** — `GenericEvent` (sole event class), `GenericTag` (sole tag class with `List` params), `Kinds` constants, `EventFilter` builder, relay messages, JSON serialization, `PublicKey`/`PrivateKey`/`Signature` value objects, and `ISignable` contract. +- **nostr-java-identity** — `Identity` key management, event signing, and NIP-04/NIP-44 message encryption (`MessageCipher04`, `MessageCipher44`). +- **nostr-java-client** — `NostrRelayClient` WebSocket client with Spring Retry, Virtual Thread dispatch, async APIs (`connectAsync`, `sendAsync`, `subscribeAsync`), and connection state tracking. ## Building and testing + The project is built with Maven. Unit tests do not require a running relay, while integration tests use Testcontainers to start a relay in Docker. ### Unit-tested build ```bash -./mvnw clean test -./mvnw install -Dmaven.test.skip=true +mvn clean test +mvn install -Dmaven.test.skip=true ``` ### Integration-tested build (requires Docker) ```bash -./mvnw clean install +mvn clean verify ``` -Integration tests start a `nostr-rs-relay` container automatically. The image used can be overridden in `src/test/resources/relay-container.properties` by setting `relay.container.image=`. +Integration tests start a relay container automatically. The image used can be overridden in `src/test/resources/relay-container.properties` by setting `relay.container.image=`. ## WebSocket configuration -`StandardWebSocketClient` waits for relay responses when sending messages. The timeout and polling interval are configured with the following properties (values in milliseconds): + +`NostrRelayClient` waits for relay responses when sending messages. Configuration properties (values in milliseconds): + ``` nostr.websocket.await-timeout-ms=60000 -nostr.websocket.poll-interval-ms=500 +nostr.websocket.max-idle-timeout-ms=3600000 +nostr.websocket.max-text-message-buffer-size=1048576 +nostr.websocket.max-binary-message-buffer-size=1048576 ``` -If a relay response is not received before the timeout elapses, the client logs the failure, closes the WebSocket session, and returns an empty list of events. + +If a relay response is not received before the timeout elapses, the client throws a `RelayTimeoutException`. The maximum number of events accumulated per request defaults to 10,000 to prevent unbounded memory growth. ## Retry behavior -`SpringWebSocketClient` leverages Spring Retry so that failed send operations are retried up to three times with an exponential backoff starting at 500 ms. + +`NostrRelayClient` uses Spring Retry (`@NostrRetryable`) so that failed send and subscribe operations are retried up to three times with exponential backoff starting at 500 ms. + +## Virtual Threads + +The client dispatches relay subscription callbacks on Virtual Threads, so expensive listener logic does not block inbound WebSocket I/O. Async APIs (`connectAsync`, `sendAsync`, `subscribeAsync`) also run on Virtual Threads via named thread factories. ## Examples For practical usage examples, see: -- [API Examples Guide](howto/api-examples.md) – Comprehensive examples covering 13+ use cases -- [Custom Events How-To](howto/custom-events.md) – Creating custom event types -- [Streaming Subscriptions](howto/streaming-subscriptions.md) – Long-lived subscriptions -- [Extending Events](explanation/extending-events.md) – Extending the event model with custom tags - -Example code is also available in the [`nostr-java-examples`](../nostr-java-examples) module. +- [API how-to](howto/use-nostr-java-api.md) — Create, sign, and send events +- [Custom events](howto/custom-events.md) — Working with custom event kinds +- [Streaming subscriptions](howto/streaming-subscriptions.md) — Long-lived subscriptions ## Contributing Before submitting changes: -1. **Run verification**: `./mvnw -q verify` – ensure all tests pass +1. **Run verification**: `mvn -q verify` — ensure all tests pass 2. **Follow code style**: Use clear, descriptive names and remove unused imports 3. **Write tests**: Include unit tests and update relevant documentation 4. **Follow commit conventions**: Use conventional commits (see [CONTRIBUTING.md](../CONTRIBUTING.md)) diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index c57f30a8b..3c2e913db 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -1,6 +1,6 @@ # Getting Started -Navigation: [Docs index](README.md) · [API how‑to](howto/use-nostr-java-api.md) · [Streaming subscriptions](howto/streaming-subscriptions.md) · [API reference](reference/nostr-java-api.md) · [Codebase overview](CODEBASE_OVERVIEW.md) +Navigation: [Docs index](README.md) · [API how-to](howto/use-nostr-java-api.md) · [Streaming subscriptions](howto/streaming-subscriptions.md) · [API reference](reference/nostr-java-api.md) · [Codebase overview](CODEBASE_OVERVIEW.md) ## Prerequisites - Maven @@ -11,7 +11,7 @@ Navigation: [Docs index](README.md) · [API how‑to](howto/use-nostr-java-api.m ```bash git clone https://github.com/tcheeric/nostr-java.git cd nostr-java -./mvnw clean install +mvn clean install ``` ## Using Maven @@ -41,9 +41,28 @@ Use the BOM to align versions and omit per-module versions: + xyz.tcheeric - nostr-java-api + nostr-java-client + + +``` + +Or pick only the modules you need: + +```xml + + + + xyz.tcheeric + nostr-java-identity + + + + + xyz.tcheeric + nostr-java-event ``` @@ -59,10 +78,8 @@ repositories { dependencies { implementation platform('xyz.tcheeric:nostr-java-bom:X.Y.Z') - implementation 'xyz.tcheeric:nostr-java-api' + implementation 'xyz.tcheeric:nostr-java-client' } ``` Replace X.Y.Z with the latest version from the releases page. - -Examples are available in the [`nostr-java-examples`](../nostr-java-examples) module. diff --git a/docs/README.md b/docs/README.md index 31e407edf..71d98618a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,15 +6,14 @@ Quick links to the most relevant guides and references. - [GETTING_STARTED.md](GETTING_STARTED.md) — Installation and setup via Maven/Gradle - [TROUBLESHOOTING.md](TROUBLESHOOTING.md) — Common issues and solutions -- [MIGRATION.md](MIGRATION.md) — Upgrading between versions -## How‑to Guides +## How-to Guides -- [howto/use-nostr-java-api.md](howto/use-nostr-java-api.md) — Basic API usage -- [howto/api-examples.md](howto/api-examples.md) — Comprehensive examples with 13+ use cases -- [howto/streaming-subscriptions.md](howto/streaming-subscriptions.md) — Long-lived subscriptions -- [howto/custom-events.md](howto/custom-events.md) — Creating custom event types -- [howto/manage-roadmap-project.md](howto/manage-roadmap-project.md) — Sync the GitHub Project with the 1.0 backlog +- [howto/use-nostr-java-api.md](howto/use-nostr-java-api.md) — Quick start: create, sign, and send events +- [howto/api-examples.md](howto/api-examples.md) — Comprehensive examples for common use cases +- [howto/streaming-subscriptions.md](howto/streaming-subscriptions.md) — Long-lived subscriptions with NostrRelayClient +- [howto/custom-events.md](howto/custom-events.md) — Working with custom event kinds +- [howto/diagnostics.md](howto/diagnostics.md) — Inspecting relay failures and troubleshooting - [howto/version-uplift-workflow.md](howto/version-uplift-workflow.md) — Tagging, publishing, and BOM alignment for releases - [howto/configure-release-secrets.md](howto/configure-release-secrets.md) — Configure Maven Central and GPG secrets for releases - [howto/ci-it-stability.md](howto/ci-it-stability.md) — Keep CI green and stabilize Docker-based ITs @@ -22,7 +21,6 @@ Quick links to the most relevant guides and references. ## Operations - [operations/README.md](operations/README.md) — Ops index (logging, metrics, config) -- [howto/diagnostics.md](howto/diagnostics.md) — Inspecting relay failures and troubleshooting ## Reference @@ -30,9 +28,13 @@ Quick links to the most relevant guides and references. ## Explanation -- [explanation/extending-events.md](explanation/extending-events.md) — Extending the event model -- [explanation/roadmap-1.0.md](explanation/roadmap-1.0.md) — Outstanding work before the 1.0 release -- [explanation/dependency-alignment.md](explanation/dependency-alignment.md) — How versions are aligned and the 1.0 cleanup plan +- [explanation/extending-events.md](explanation/extending-events.md) — Working with events and tags (GenericEvent, GenericTag, Kinds) +- [explanation/architecture.md](explanation/architecture.md) — Module architecture and data flow +- [explanation/dependency-alignment.md](explanation/dependency-alignment.md) — How versions are aligned via BOM + +## Developer + +- [developer/SIMPLIFICATION_PROPOSAL.md](developer/SIMPLIFICATION_PROPOSAL.md) — 2.0 design simplification proposal ## Project @@ -40,5 +42,4 @@ Quick links to the most relevant guides and references. ## Tests Overview -- API Client/Handler tests: `nostr-java-api/src/test/java/nostr/api/client/README.md` — logging, relays, handler send/close/request, dispatcher & subscription manager - Client module (Spring WebSocket): `nostr-java-client/src/test/java/nostr/client/springwebsocket/README.md` — send/subscribe retries and timeout behavior diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 250730a18..0ffe34208 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -21,7 +21,7 @@ This guide helps you diagnose and resolve common issues when using nostr-java. ### Problem: Dependency Not Found -**Symptom**: Maven or Gradle cannot resolve `xyz.tcheeric:nostr-java-api:` +**Symptom**: Maven or Gradle cannot resolve `xyz.tcheeric:nostr-java-client:` **Solution**: Ensure you've added the custom repository to your build configuration: @@ -89,7 +89,7 @@ Exclude conflicting transitive dependencies if needed (version managed by the BO ```xml xyz.tcheeric - nostr-java-api + nostr-java-client conflicting-group @@ -117,12 +117,12 @@ Ensure the relay URL uses the correct WebSocket protocol: **Bad:** ```java -Map relays = Map.of("relay", "https://relay.398ja.xyz"); // Wrong protocol +new NostrRelayClient("https://relay.398ja.xyz"); // Wrong protocol ``` **Good:** ```java -Map relays = Map.of("relay", "wss://relay.398ja.xyz"); +new NostrRelayClient("wss://relay.398ja.xyz"); ``` #### 2. Relay is Down or Unreachable @@ -133,7 +133,6 @@ Test the relay URL independently: websocat wss://relay.398ja.xyz # Or use an online WebSocket tester -# https://www.websocket.org/echo.html ``` Try alternative public relays: @@ -150,36 +149,24 @@ System.setProperty("https.proxyHost", "proxy.example.com"); System.setProperty("https.proxyPort", "8080"); ``` -#### 4. SSL/TLS Certificate Issues - -**Symptom**: `SSLHandshakeException` - -For self-signed certificates in development only: -```java -// WARNING: Only use in development, never in production -System.setProperty("jdk.internal.httpclient.disableHostnameVerification", "true"); -``` - ### Problem: Connection Drops Unexpectedly **Symptom**: Subscription stops receiving events after a period -**Solution**: Implement retry logic and connection monitoring: +**Solution**: Use retry and error handling with `NostrRelayClient`: ```java -NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(); -client.setRelays(relays); - -AutoCloseable subscription = client.subscribe( - filters, - "my-subscription", - message -> System.out.println(message), - error -> { - System.err.println("Connection error: " + error.getMessage()); - // Implement reconnection logic here - // Consider exponential backoff - } -); +try (NostrRelayClient client = new NostrRelayClient("wss://relay.398ja.xyz")) { + AutoCloseable subscription = client.subscribe( + req, + message -> System.out.println(message), + error -> { + System.err.println("Connection error: " + error.getMessage()); + // NostrRelayClient uses Spring Retry with exponential backoff + }, + () -> System.out.println("Connection closed") + ); +} ``` --- @@ -198,13 +185,21 @@ Ensure you sign the event before sending: **Bad:** ```java -GenericEvent event = new GenericEvent(pubKey, Kind.TEXT_NOTE, List.of(), "Hello"); +GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Hello") + .build(); client.send(new EventMessage(event)); // Missing signature! ``` **Good:** ```java -GenericEvent event = new GenericEvent(pubKey, Kind.TEXT_NOTE, List.of(), "Hello"); +GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Hello") + .build(); identity.sign(event); // Sign first client.send(new EventMessage(event)); ``` @@ -213,43 +208,13 @@ client.send(new EventMessage(event)); Never modify an event after signing it. Any change invalidates the signature. -**Bad:** -```java -identity.sign(event); -event.setContent("Different content"); // Signature now invalid! -client.send(new EventMessage(event)); -``` - #### 3. Incorrect Key Format -Ensure private keys are in the correct format (32-byte hex string): +Ensure private keys are in the correct format (32-byte hex string, 64 hex characters): ```java -// Valid 32-byte hex key (64 hex characters) String validKey = "a".repeat(64); Identity id = Identity.create(validKey); - -// Invalid - too short -String invalidKey = "abc123"; // Will throw exception -``` - -### Problem: Identity Generation Fails - -**Symptom**: `NostrException` or invalid key errors - -**Solution**: Use the provided identity generation methods: - -```java -// Recommended: Generate random identity -Identity identity = Identity.generateRandomIdentity(); - -// From existing private key (hex string) -String privateKeyHex = "..."; -Identity identity = Identity.create(privateKeyHex); - -// Verify the identity -PublicKey pubKey = identity.getPublicKey(); -System.out.println("Public key: " + pubKey.toString()); ``` --- @@ -258,29 +223,25 @@ System.out.println("Public key: " + pubKey.toString()); ### Problem: Events Not Appearing on Relay -**Symptom**: Event sent successfully but doesn't appear in queries - **Debugging Steps:** #### 1. Verify Event Structure -Check the event JSON before sending: - ```java -GenericEvent event = new GenericEvent(pubKey, kind, tags, content); +GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Hello") + .build(); identity.sign(event); // Log the event JSON String json = new EventMessage(event).encode(); System.out.println("Sending event: " + json); - -client.send(new EventMessage(event)); ``` #### 2. Check Relay Response -Many relays send OK messages. Listen for them: - ```java List responses = client.send(new EventMessage(event)); responses.forEach(response -> @@ -288,36 +249,20 @@ responses.forEach(response -> ); ``` -#### 3. Verify Event ID Calculation +### Problem: Relay Timeout + +**Symptom**: `RelayTimeoutException` when sending events -The event ID must be calculated correctly: +**Solution**: Increase the timeout or check relay connectivity: ```java -// Event ID is automatically calculated during signing -identity.sign(event); -System.out.println("Event ID: " + event.getId()); +// Increase timeout to 2 minutes +NostrRelayClient client = new NostrRelayClient("wss://relay.398ja.xyz", 120_000); ``` -### Problem: "Invalid Event Kind" Error - -**Symptom**: Relay rejects event with kind error - -**Solution**: Ensure you're using a valid kind number per [NIP-16](https://github.com/nostr-protocol/nips/blob/master/16.md): - -- **Regular (1-9999)**: Standard events that can be deleted -- **Replaceable (10000-19999)**: Newer event replaces older ones -- **Ephemeral (20000-29999)**: Not stored by relays -- **Parameterized Replaceable (30000-39999)**: Replaceable with parameters - -```java -// Valid kinds -int TEXT_NOTE = 1; // Regular -int METADATA = 0; // Regular -int CONTACTS = 3; // Replaceable (10000-19999 range in older spec, but 3 is special) -int CUSTOM_KIND = 30000; // Parameterized replaceable - -// Use appropriate kind for your use case -GenericEvent event = new GenericEvent(pubKey, CUSTOM_KIND, tags, content); +Or configure via Spring properties: +```properties +nostr.websocket.await-timeout-ms=120000 ``` --- @@ -326,110 +271,58 @@ GenericEvent event = new GenericEvent(pubKey, CUSTOM_KIND, tags, content); ### Problem: Subscription Receives No Events -**Symptom**: Subscription opens successfully but callback never fires - **Debugging Steps:** #### 1. Verify Filter Configuration -Check that your filters match existing events: - ```java -// Too restrictive - might match nothing -Filters tooRestrictive = new Filters( - new AuthorFilter(specificPubKey), - new KindFilter<>(Kind.TEXT_NOTE), - new SinceFilter(Instant.now().getEpochSecond()) // Only future events -); +// Too restrictive — might match nothing +EventFilter tooRestrictive = EventFilter.builder() + .authors(List.of(specificPubKey)) + .kinds(List.of(Kinds.TEXT_NOTE)) + .since(Instant.now().getEpochSecond()) // Only future events + .build(); -// More permissive - should match events -Filters permissive = new Filters( - new KindFilter<>(Kind.TEXT_NOTE) -); -permissive.setLimit(10); // Limit results +// More permissive — should match events +EventFilter permissive = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE)) + .limit(10) + .build(); ``` -#### 2. Test with Known Events - -Query for a specific event you know exists: - -```java -Filters filters = new Filters(new IdsFilter(knownEventId)); -client.subscribe(filters, "test-sub", - message -> System.out.println("Found: " + message), - error -> System.err.println("Error: " + error) -); -``` - -#### 3. Check Relay Supports Filter Type - -Not all relays support all filter types. Test with basic filters first: +#### 2. Test with Basic Filters ```java // Most widely supported -Filters basic = new Filters(new KindFilter<>(Kind.TEXT_NOTE)); -basic.setLimit(5); +EventFilter basic = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE)) + .limit(5) + .build(); ``` ### Problem: Subscription Callback Blocks **Symptom**: Application becomes unresponsive or slow -**Solution**: Offload heavy processing from the WebSocket thread: - -```java -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -ExecutorService executor = Executors.newFixedThreadPool(4); - -AutoCloseable subscription = client.subscribe( - filters, - "my-sub", - message -> { - // Hand off to executor immediately - executor.submit(() -> { - // Heavy processing here - processMessage(message); - }); - }, - error -> System.err.println(error) -); - -// Don't forget to shut down executor -executor.shutdown(); -``` - -### Problem: Too Many Events Causing Backpressure - -**Symptom**: Memory usage grows, events arrive faster than processing - -**Solution**: Implement flow control: +**Note**: In nostr-java 2.0, subscription callbacks are dispatched on Virtual Threads, so blocking in callbacks does not block WebSocket I/O. However, for high-throughput feeds, consider using a bounded queue: ```java -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; - -BlockingQueue eventQueue = new LinkedBlockingQueue<>(1000); // Max 1000 events +BlockingQueue eventQueue = new LinkedBlockingQueue<>(1000); -client.subscribe( - filters, - "my-sub", +client.subscribe(req, message -> { if (!eventQueue.offer(message)) { System.err.println("Queue full, dropping event"); } }, - error -> System.err.println(error) + error -> System.err.println(error), + () -> {} ); // Process from queue at controlled rate while (running) { String message = eventQueue.poll(1, TimeUnit.SECONDS); - if (message != null) { - processMessage(message); - } + if (message != null) processMessage(message); } ``` @@ -437,7 +330,7 @@ while (running) { ## Encryption & Decryption Issues -### Problem: Decryption Fails for NIP-04 Messages +### Problem: Decryption Fails **Symptom**: `NostrException` or garbled plaintext @@ -448,50 +341,30 @@ while (running) { Ensure you're using the recipient's private key to decrypt: ```java -// Alice sends to Bob Identity alice = Identity.generateRandomIdentity(); Identity bob = Identity.generateRandomIdentity(); -NIP04 dm = new NIP04(alice, bob.getPublicKey()) - .createDirectMessageEvent("Secret message"); - -// Bob must use his identity to decrypt -String plaintext = NIP04.decrypt(bob, dm.getEvent()); // Correct - -// This would fail: -// String plaintext = NIP04.decrypt(alice, dm.getEvent()); // Wrong! -``` +// Alice encrypts for Bob +MessageCipher04 cipher = new MessageCipher04(alice.getPrivateKey(), bob.getPublicKey()); +String encrypted = cipher.encrypt("Secret message"); -#### 2. Corrupted Ciphertext - -Verify the event content wasn't modified: - -```java -try { - String decrypted = NIP04.decrypt(identity, event); - System.out.println(decrypted); -} catch (NostrException e) { - System.err.println("Decryption failed - content may be corrupted"); - e.printStackTrace(); -} +// Bob decrypts (using his private key + Alice's public key) +MessageCipher04 bobCipher = new MessageCipher04(bob.getPrivateKey(), alice.getPublicKey()); +String decrypted = bobCipher.decrypt(encrypted); ``` ### Problem: NIP-44 vs NIP-04 Confusion -**Symptom**: Decryption fails with wrong cipher version - **Solution**: Match encryption and decryption versions: ```java // NIP-04 (legacy) MessageCipher04 cipher04 = new MessageCipher04(senderPriv, recipientPub); String encrypted04 = cipher04.encrypt("Hello"); -String decrypted04 = cipher04.decrypt(encrypted04); // NIP-44 (recommended) MessageCipher44 cipher44 = new MessageCipher44(senderPriv, recipientPub); String encrypted44 = cipher44.encrypt("Hello"); -String decrypted44 = cipher44.decrypt(encrypted44); // Can't mix: cipher04.decrypt(encrypted44) will fail! ``` @@ -502,54 +375,33 @@ String decrypted44 = cipher44.decrypt(encrypted44); ### Problem: Slow Event Publishing -**Symptom**: High latency when sending events - **Solutions:** -#### 1. Batch Events When Possible +#### Use Async APIs ```java -List events = List.of( - new EventMessage(event1), - new EventMessage(event2), - new EventMessage(event3) -); - -// Send in parallel to multiple relays -events.forEach(event -> client.send(event)); -``` - -#### 2. Use Async Publishing - -```java -import java.util.concurrent.CompletableFuture; - -CompletableFuture future = CompletableFuture.runAsync(() -> { - client.send(new EventMessage(event)); -}); - -// Continue other work -future.join(); // Wait when needed +NostrRelayClient.connectAsync("wss://relay.398ja.xyz") + .thenCompose(client -> client.sendAsync(new EventMessage(event))) + .thenAccept(responses -> System.out.println("Done: " + responses)); ``` ### Problem: High Memory Usage -**Symptom**: Application memory grows continuously - **Solutions:** #### 1. Limit Subscription Results ```java -Filters filters = new Filters(new KindFilter<>(Kind.TEXT_NOTE)); -filters.setLimit(100); // Limit to 100 most recent events +EventFilter filter = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE)) + .limit(100) + .build(); ``` #### 2. Close Subscriptions When Done ```java AutoCloseable subscription = client.subscribe(/* ... */); - try { // Use subscription } finally { @@ -557,74 +409,13 @@ try { } ``` -#### 3. Clear References to Large Objects - -```java -// Don't hold references to all events -client.subscribe(filters, "sub", message -> { - processMessage(message); - // Don't: allMessages.add(message); // Memory leak! -}); -``` - ---- - -## Getting More Help - -If your issue isn't covered here: - -1. **Check the API reference**: [reference/nostr-java-api.md](reference/nostr-java-api.md) -2. **Review examples**: Browse the [`nostr-java-examples`](../nostr-java-examples) module -3. **Search existing issues**: [GitHub Issues](https://github.com/tcheeric/nostr-java/issues) -4. **Open a new issue**: Provide: - - nostr-java version (e.g., `X.Y.Z`) - - Java version (`java -version`) - - Minimal code to reproduce - - Full error stack trace - - Expected vs actual behavior - -## Debug Logging - -Enable debug logging to diagnose issues: - -```java -import java.util.logging.Logger; -import java.util.logging.Level; - -Logger logger = Logger.getLogger("nostr"); -logger.setLevel(Level.FINE); - -// Or configure via logging.properties -// nostr.level = FINE -``` - -For Spring Boot applications, add to `application.properties`: - -```properties -logging.level.nostr=DEBUG -logging.level.nostr.client=TRACE -``` - --- ## Integration Testing Issues ### Problem: Tests Timeout After 60 Seconds -**Symptom**: Integration tests hang and fail with `NoSuchElementException: No value present` or `No message received` - -**Possible Causes & Solutions:** - -#### 1. nostr-rs-relay Quanta Bug - -The `scsibug/nostr-rs-relay` Docker image contains a known bug in the `quanta` crate that causes panics in Docker environments: - -``` -thread 'tokio-ws-10' panicked at quanta-0.9.3/src/lib.rs:274:13: -po2_denom was zero! -``` - -**Solution**: Use strfry relay instead: +**Solution**: Use strfry relay instead of nostr-rs-relay: ```properties # src/test/resources/relay-container.properties @@ -632,142 +423,36 @@ relay.container.image=dockurr/strfry:latest relay.container.port=7777 ``` -#### 2. Relay Container Not Starting - -Check Docker is available and the container starts properly: - -```bash -# Verify Docker is running -docker info - -# Test the relay image manually -docker run --rm -p 7777:7777 dockurr/strfry:latest -``` - -### Problem: strfry Rejects All Events (Whitelist) - -**Symptom**: Events return `success=false` with message `blocked: pubkey not in whitelist` - -**Cause**: The default strfry Docker image has a write policy that whitelists specific pubkeys. - -**Solution**: Create a custom strfry.conf that disables the whitelist: - -```conf -# src/test/resources/strfry.conf -relay { - writePolicy { - plugin = "" # Disable write policy plugin - } -} -``` - -Mount this config in your test container: - -```java -RELAY = new GenericContainer<>(image) - .withExposedPorts(relayPort) - .withClasspathResourceMapping( - "strfry.conf", "/etc/strfry.conf", BindMode.READ_ONLY) - .withTmpFs(Map.of("/app/strfry-db", "rw")) - .waitingFor(Wait.forLogMessage(".*Started websocket server on.*", 1)); -``` - -### Problem: Filter Queries Return Empty Results - -**Symptom**: Tests send events successfully but filter queries return only EOSE (no events) - -**Cause**: Race condition - the relay needs time to index events before they can be queried. - -**Solution**: Add a small delay between publishing and querying: - -```java -// Send event -nip01.createTextNoteEvent(List.of(tag), "content").signAndSend(relays); - -// Wait for relay to index the event -Thread.sleep(100); - -// Now query -List result = nip01.sendRequest(filters, subscriptionId); -``` - -### Problem: strfry Requires High File Descriptor Limits - -**Symptom**: Container fails with `Unable to set NOFILES limit` - -**Solution**: Configure ulimits in Testcontainers: - -```java -import com.github.dockerjava.api.model.Ulimit; - -RELAY = new GenericContainer<>(image) - .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig() - .withUlimits(new Ulimit[] {new Ulimit("nofile", 1000000L, 1000000L)})) - // ... other configuration -``` - -### Problem: Tests Use Wrong Relay URL - -**Symptom**: Tests connect to hardcoded URL instead of Testcontainers dynamic port - -**Cause**: Tests may use `@Autowired` relays from properties file instead of dynamic container port. - -**Solution**: Use a base test class that provides the dynamic relay URL: - -```java -public abstract class BaseRelayIntegrationTest { - @Container - private static final GenericContainer RELAY = /* ... */; - - static Map getTestRelays() { - String host = RELAY.getHost(); - int port = RELAY.getMappedPort(7777); - return Map.of("relay", String.format("ws://%s:%d", host, port)); - } -} - -// In your test -@BeforeEach -void setUp() { - relays = getTestRelays(); // Use dynamic URL, not autowired -} -``` - ### Problem: Tests Fail in CI but Pass Locally -**Symptom**: Docker-based tests fail in CI environments - **Possible Causes:** 1. **Docker not available**: Skip tests when Docker is unavailable: - ```java @DisabledIfSystemProperty(named = "noDocker", matches = "true") -public class MyIntegrationTest extends BaseRelayIntegrationTest { - // ... -} ``` -Run with: `mvn test -DnoDocker=true` - -2. **Different Docker environments**: Some CI environments don't support certain CPU features (TSC) that cause the quanta bug. - -3. **Resource constraints**: CI containers may have limited resources. Use tmpfs for relay storage: - +2. **Resource constraints**: Use tmpfs for relay storage: ```java .withTmpFs(Map.of("/app/strfry-db", "rw")) ``` -### Relay Configuration Reference +--- + +## Getting More Help + +If your issue isn't covered here: -| Relay | Image | Port | Wait Strategy | -|-------|-------|------|---------------| -| strfry | `dockurr/strfry:latest` | 7777 | `Started websocket server on` | -| nostr-rs-relay | `scsibug/nostr-rs-relay:latest` | 8080 | `listening on:` (has quanta bug) | +1. **Check the API reference**: [reference/nostr-java-api.md](reference/nostr-java-api.md) +2. **Review examples**: [howto/api-examples.md](howto/api-examples.md) +3. **Search existing issues**: [GitHub Issues](https://github.com/tcheeric/nostr-java/issues) +4. **Open a new issue**: Provide nostr-java version, Java version, minimal reproducing code, and full stack trace. -Configure via `src/test/resources/relay-container.properties`: +## Debug Logging + +For Spring Boot applications, add to `application.properties`: ```properties -relay.container.image=dockurr/strfry:latest -relay.container.port=7777 +logging.level.nostr=DEBUG +logging.level.nostr.client=TRACE ``` diff --git a/docs/developer/SECURE_CODING.md b/docs/developer/SECURE_CODING.md new file mode 100644 index 000000000..a905d497b --- /dev/null +++ b/docs/developer/SECURE_CODING.md @@ -0,0 +1,80 @@ +# Secure Coding Guidelines for imani-bridge + +This document outlines the mandatory secure coding practices for the `imani-bridge` project. These guidelines are derived from industry best practices (OWASP, Oracle, etc.) and must be followed for all contributions. + +## 1. Input Validation and Output Encoding + +### Input Validation +* **Validate All Inputs:** Adopt a "deny by default" approach. Define strictly what is allowed (allow-listing) rather than what is forbidden (block-listing). +* **Context-Aware:** Validation must be appropriate for the data type (e.g., email, UUID, payment amount). +* **Boundary Checks:** Check for length, range, and format constraints. +* **Sanitization:** Sanitize input *before* processing, but prefer validation over sanitization where possible. + +### Output Encoding +* **Context-Aware Encoding:** Encode data based on where it will be displayed (HTML body, HTML attribute, JavaScript, CSS, URL). +* **Prevention:** This is the primary defense against Cross-Site Scripting (XSS). +* **Libraries:** Use established libraries like OWASP Java Encoder. + +## 2. Injection Prevention + +### SQL/Database Injection +* **Parameterized Queries:** ALWAYS use `PreparedStatement` in JDBC or parameterized queries in JPA/Hibernate. +* **No Concatenation:** NEVER concatenate user input directly into query strings. +* **ORM Usage:** Use criteria APIs or named queries where possible. + +### Command Injection +* **Avoid OS Commands:** Do not use `Runtime.exec()` or `ProcessBuilder` with user-supplied arguments. +* **APIs:** Use Java API equivalents (e.g., `java.nio.file`) instead of shell commands (e.g., `ls`, `rm`). + +### Log Injection +* **Sanitize Logs:** Ensure user input written to logs does not contain newline characters (` +`, ` `) to prevent log forging. +* **Structured Logging:** Prefer structured logging (JSON) to mitigate format string attacks. + +## 3. Cryptography & Secrets Management + +### Cryptography +* **Standard Libraries:** Use `BouncyCastle` (already in dependencies) or `Google Tink`. **NEVER** implement custom cryptographic algorithms. +* **Algorithms:** + * **Hashing:** SHA-256 or higher. + * **Password Hashing:** Argon2id (preferred) or BCrypt. + * **Encryption:** AES-GCM (256-bit). +* **Randomness:** Use `SecureRandom` for security-critical random numbers (keys, nonces, tokens), not `java.util.Random`. + +### Secrets Management +* **No Hardcoded Secrets:** NEVER commit API keys, passwords, or tokens to the repository. +* **Environment Variables:** Load secrets from environment variables or a secure vault. +* **Git Hooks:** Use `git-secrets` or similar tools to prevent accidental commits of sensitive data. + +## 4. Authentication & Authorization + +### Authentication +* **Strong Defaults:** Enforce strong password policies (length, complexity). +* **Session Management:** Use secure, HTTP-only, SameSite cookies for session identifiers. +* **MFA:** Support Multi-Factor Authentication where applicable. + +### Authorization +* **Least Privilege:** Grant the minimum necessary permissions for a user or service to function. +* **Vertical & Horizontal:** Check permissions at every access point (controller, service method). Ensure users can only access their *own* data (horizontal privilege escalation prevention). + +## 5. Dependency Management + +* **Vulnerability Scanning:** Regularly scan dependencies for known vulnerabilities (CVEs) using `OWASP Dependency Check` or similar plugins. +* **Updates:** Keep libraries (Spring Boot, BouncyCastle, etc.) up-to-date. +* **Transitive Dependencies:** Be aware of and manage transitive dependencies. + +## 6. Error Handling & Logging + +* **Generic Errors:** specific error details (stack traces, internal paths) should NEVER be exposed to the client/API response. Return generic error codes/messages. +* **Audit Logging:** Log security-critical events (login attempts, failed authorization, sensitive data access). +* **Exception Blocks:** Do not suppress exceptions silently. Log them with sufficient context (internally). + +## 7. XML & Serialization + +* **XXE Prevention:** Disable DTDs and external entity processing in XML parsers (`DocumentBuilderFactory`, `SAXParserFactory`, `XMLInputFactory`). +* **Deserialization:** Avoid Java native serialization if possible. If necessary, use strict allow-lists for classes that can be deserialized. + +## References +* [OWASP Java Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Java_Security_Cheat_Sheet.html) +* [Oracle Secure Coding Guidelines](https://www.oracle.com/java/technologies/javase/seccodeguide.html) +* [TechOral Java Security Best Practices](https://techoral.com/java/java-security-best-practices.html) diff --git a/docs/developer/SIMPLIFICATION_PROPOSAL.md b/docs/developer/SIMPLIFICATION_PROPOSAL.md new file mode 100644 index 000000000..c879dbb11 --- /dev/null +++ b/docs/developer/SIMPLIFICATION_PROPOSAL.md @@ -0,0 +1,1014 @@ +# nostr-java Design Simplification Proposal + +## Executive Summary + +This proposal reduces nostr-java from **9 modules with ~170 classes** to **4 modules with ~40 classes** by: + +- Removing the `api` and `examples` modules entirely +- Eliminating all 40 concrete event subclasses — `GenericEvent` becomes the sole event class +- Eliminating all 17 concrete tag subclasses — `GenericTag` becomes the sole tag class, backed by `List` instead of `ElementAttribute` +- Removing 27 entity classes, the `Kind` enum, `ElementAttribute`, `TagRegistry`, and all NIP-specific code +- Dropping most interfaces and abstract classes (`ITag`, `IEvent`, `IElement`, `IGenericElement`, `IBech32Encodable`, `Deleteable`, `BaseEvent`, `BaseTag`) +- Simplifying filters to 3 classes, serialization to ~5 classes +- Merging 7 modules into 4 (`core`, `event`, `identity`, `client`) + +**This simplification also resolves the `GenericTag.getCode()` NPE bug** documented in `docs/problems/GENERIC_TAG_GETCODE_FRAGILITY.md`. The root cause — a dual-path tag architecture where annotation-based registered tags and field-based generic tags have incompatible interfaces — is eliminated entirely. There is only one tag class, one code accessor, one value accessor. No reflection, no annotations, no NPE. + +--- + +## Current State + +``` +Modules: 9 +Concrete event classes: 40 (TextNoteEvent, DirectMessageEvent, CalendarEvent, etc.) +Concrete tag classes: 17 (EventTag, PubKeyTag, AddressTag, etc.) +Entity classes: 27 (UserProfile, ZapRequest, CashuToken, etc.) +NIP API classes: 26 (NIP01..NIP99) +Interfaces/abstract classes: ~15 (ITag, IEvent, IElement, ISignable, BaseTag, BaseEvent, etc.) +Filter classes: 17 +Serializer/Deserializer: ~16 +Message classes: 10 +Kind enum: 1 (35 constants, 130 lines) +``` + +## Proposed State + +``` +Modules: 4 (core, event, identity, client) +Concrete event classes: 1 (GenericEvent) +Concrete tag classes: 1 (GenericTag — backed by List) +Entity classes: 0 +NIP API classes: 0 +Interfaces/abstract classes: 3 (ISignable, IKey, BaseKey — only those earning their existence) +Filter classes: 3 (EventFilter, Filters, Filterable) +Serializer/Deserializer: ~5 +Message classes: 7 (keep all — they map 1:1 to the Nostr relay protocol) +Kind constants: 1 (optional Kinds utility class with static int fields) +``` + +--- + +## Phase 1: Remove `nostr-java-api` and `nostr-java-examples` + +### What gets deleted +- **26 NIP classes** (NIP01.java through NIP99.java) — convenience wrappers around GenericEvent +- **EventNostr** and **NostrSpringWebSocketClient** — high-level orchestration +- **All factory classes** (GenericEventFactory, NIP01EventBuilder, NIP57ZapRequestBuilder, etc.) +- **Client management** (WebSocketClientHandler, NostrRelayRegistry, NostrSubscriptionManager, etc.) +- **Service layer** (NoteService, DefaultNoteService) +- **All 6 example classes** +- **Configuration classes** (RelayConfig, RelaysProperties, Constants) + +### What migrates elsewhere +Nothing. Users build `GenericEvent` directly and use `nostr-java-client` to send. The NIP classes were syntactic sugar — they created a `GenericEvent` with a specific kind and specific tags. With a good builder and clear documentation, users can do this themselves. + +### Impact +Users lose convenience methods like `NIP01.createTextNote("Hello")`. Instead: +```java +GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(1) + .content("Hello Nostr!") + .build(); +identity.sign(event); +``` +The tradeoff: more explicit code, but far less library surface area to maintain. + +### Risk: Low +The api module has no dependents except examples. No downstream code breaks. + +--- + +## Phase 2: Remove All Concrete Event Subclasses + +### What gets deleted (40 classes) +All classes in `nostr-java-event/src/main/java/nostr/event/impl/` except `GenericEvent.java`: + +- TextNoteEvent, DirectMessageEvent, ContactListEvent, ReactionEvent, DeletionEvent +- EphemeralEvent, ReplaceableEvent, AddressableEvent +- All Calendar events (4 classes + abstract base) +- All Marketplace events (7 classes) +- All Nostr Connect events (4 classes + abstract base) +- All Channel events (5 classes) +- Zap events, NutZap events, OTS events, etc. +- InternetIdentifierMetadataEvent, MentionsEvent, ClassifiedListingEvent + +### What `GenericEvent` already provides +`GenericEvent` is already **fully functional** as the sole event class: +- Supports any kind via `Integer` — no need for subclass-per-kind +- Has `isReplaceable()`, `isEphemeral()`, `isAddressable()` — range checks based on kind value +- Has builder pattern, validation, serialization, signing support +- Tags are a generic list + +### What the subclasses provided (and why it's not needed) +1. **Kind validation** (`validateKind()` overrides) — Replace with: validation at event creation time using integer range checks +2. **Tag validation** (`validateTags()` overrides) — Replace with: optional validation utilities users can call, or builder-time validation +3. **Convenience constructors** — Replace with: `GenericEvent.builder()` already handles this +4. **Type-safe casting** — Replace with: query by kind integer. Runtime `instanceof` checks were fragile anyway since deserialized events come back as `GenericEvent` + +### What needs updating +- **`GenericEvent.convert()`** static method — remove it (no subclasses to convert to) +- **Deserialization** — `GenericEventDeserializer` already returns `GenericEvent`, so the specialized deserializers (CalendarEventDeserializer, ClassifiedListingEventDeserializer) are deleted + +### Risk: Medium +Any external code using concrete event types like `TextNoteEvent` breaks. This is a major version change. + +--- + +## Phase 3: Remove All Concrete Tag Subclasses and `TagRegistry` + +### What gets deleted (18 classes) +All classes in `nostr-java-event/src/main/java/nostr/event/tag/` except `GenericTag.java`: + +- EventTag, PubKeyTag, AddressTag, IdentifierTag, ReferenceTag +- HashtagTag, ExpirationTag, UrlTag, SubjectTag, DelegationTag +- RelaysTag, NonceTag, PriceTag, EmojiTag, GeohashTag +- LabelTag, LabelNamespaceTag, VoteTag +- **TagRegistry** — no registrations needed when there's only GenericTag + +### Risk: Medium +Same as Phase 2 — breaking change for typed tag users. + +--- + +## Phase 4: Remove Entity Classes + +### What gets deleted (27 classes in `entities/`) +- UserProfile, Profile, ChannelProfile +- ZapRequest, ZapReceipt, Reaction +- CalendarContent, CalendarRsvpContent +- All Cashu-related entities (CashuToken, CashuProof, CashuQuote, CashuMint, CashuWallet) +- NutZap, NutZapInformation +- All marketplace entities (Product, Stall, CustomerOrder, etc.) +- ClassifiedListing, NIP15Content, NIP42Content +- Amount, PaymentRequest, PaymentShipmentStatus, SpendingHistory, Response + +### Rationale +These are content DTOs for specific NIPs. With GenericEvent as the sole event class, the content is just a `String` (often JSON). Users parse it themselves into whatever model they need. The library shouldn't prescribe content schemas. + +### Risk: Low +These entities were only used by the concrete event subclasses and the api module NIP classes — both already removed. + +--- + +## Phase 5: Drop `Kind` Enum — Replace with Integer + Optional Constants + +### What gets deleted +- `Kind.java` — the 130-line enum with 35 constants, `@JsonCreator`, `valueOf()` / `valueOfStrict()` / `findByValue()`, and the null-vs-exception handling that was itself a recent bug fix + +### Why + +1. **The Nostr protocol uses integers.** Kind is just an integer on the wire. The enum adds an indirection layer that must be maintained as new NIPs appear. +2. **The enum is already incomplete.** There are hundreds of defined kinds in the wild. The current enum has ~35. Users constantly hit `null` from `Kind.valueOf(int)` for kinds not in the enum. +3. **Custom kinds require bypassing the enum anyway.** `GenericEvent.builder()` already has `.customKind(Integer)` alongside `.kind(Kind)` — two paths for the same thing. +4. **Maintenance burden.** Every new NIP means someone has to add an enum constant and release a new library version. That's the exact coupling this simplification eliminates. + +### Replacement: Optional constants class + +```java +/** + * Common Nostr event kind values for discoverability. + * Users can use any integer — these are convenience constants, not an exhaustive list. + */ +public final class Kinds { + public static final int SET_METADATA = 0; + public static final int TEXT_NOTE = 1; + public static final int RECOMMEND_SERVER = 2; + public static final int CONTACT_LIST = 3; + public static final int ENCRYPTED_DIRECT_MESSAGE = 4; + public static final int DELETION = 5; + public static final int REPOST = 6; + public static final int REACTION = 7; + public static final int ZAP_REQUEST = 9734; + public static final int ZAP_RECEIPT = 9735; + // ... other commonly used kinds + + /** Valid kind range per NIP-01: 0 to 65535. */ + public static boolean isValid(int kind) { return kind >= 0 && kind <= 65_535; } + public static boolean isReplaceable(int kind) { return kind >= 10_000 && kind < 20_000; } + public static boolean isEphemeral(int kind) { return kind >= 20_000 && kind < 30_000; } + public static boolean isAddressable(int kind) { return kind >= 30_000 && kind < 40_000; } + + private Kinds() {} +} +``` + +This gives IDE autocompletion without any enum baggage — no `valueOf`, no `@JsonCreator`, no null-vs-exception debates, no forced library updates for new kinds. + +### Implications +- **`GenericEvent.kind`** — already `Integer`. Remove the `Kind`-typed constructors and builder methods. One `.kind(int)` method. +- **`GenericEvent.builder()`** — simplify: remove `.kind(Kind)` and `.customKind(Integer)`, keep only `.kind(int)`. +- **`isReplaceable()` / `isEphemeral()` / `isAddressable()`** — migrate to `Kinds` utility or keep on `GenericEvent` (they already check integer ranges). +- **Deserialization** — simpler. `kind` deserializes as a plain `int`. No `@JsonCreator` dance, no null handling for unknown kinds. +- **`KindFilter`** — already works with integers. +- **`EventTypeChecker`** — can be merged into `Kinds` utility class. + +### Risk: Low +Zero functional loss. Strictly simpler. + +--- + +## Phase 6: Drop `ElementAttribute` — Tags Become `List` + +### What gets deleted +- **`ElementAttribute`** record — `record ElementAttribute(String name, Object value)` in `nostr-java-base` +- **`IGenericElement`** interface — its only purpose was to expose `getAttributes()`/`addAttribute()` for `ElementAttribute` + +### Why +The `name` field in `ElementAttribute` (e.g., "param0", "param1") is **entirely synthetic** — generated during deserialization, never present in the Nostr protocol. Tags are just **positional arrays**: `["e", "abc123", "wss://relay.example.com", "reply"]`. The wrapper object adds indirection for no protocol benefit. + +### Replacement +`GenericTag` stores a `List` directly: + +```java +// Before +GenericTag { + code = "e", + attributes = [ElementAttribute("param0", "abc123"), ElementAttribute("param1", "wss://...")] +} +tag.getAttributes().get(0).value().toString() // to read a value + +// After +GenericTag { + code = "e", + params = ["abc123", "wss://..."] +} +tag.getParams().get(0) // to read a value — direct, no wrapper +``` + +### Implications +- **`GenericTag`** — replace `List attributes` with `List params` +- **`GenericTagSerializer`** — simplify: iterate `params` list directly instead of mapping `ElementAttribute.value().toString()` +- **`GenericTagDecoder`** — simplify: build `List` directly from JSON array elements instead of wrapping in `ElementAttribute` +- **`BaseTag.create()`** (migrating to `GenericTag.of()`) — no longer needs to generate synthetic `ElementAttribute` names +- **`GenericMessage`** — also implements `IGenericElement`; update to use `List` or a similar simple approach +- **`GenericTagQueryFilter`** — update to work with `List` params + +### Risk: Low +`ElementAttribute` was internal plumbing. Downstream consumers were already working around it. + +--- + +## Phase 7: Drop Interfaces and Abstract Classes — Resolves GenericTag.getCode() NPE + +**This phase directly resolves the bug documented in `docs/problems/GENERIC_TAG_GETCODE_FRAGILITY.md`.** + +### The Problem + +The current tag system has a **dual-path architecture** that causes a design contradiction in `GenericTag`: + +1. **Annotation path** (registered tags): `BaseTag.getCode()` reads `@Tag` annotation via reflection +2. **Field path** (generic tags): `GenericTag.getCode()` reads instance `code` field + +`GenericTag.getCode()` delegates to `super.getCode()` when `code` is empty, but `GenericTag` has no `@Tag` annotation — so the delegation **always throws NPE**. This dead branch has caused production bugs in downstream consumers (see: imani-bridge Cashu gateway losing ecash tokens due to silent tag filtering failures). + +The deeper problem: consumers must handle both paths to access tag values uniformly, leading to reflection hacks that break under Java 21 JPMS. + +### How this simplification fixes it + +By eliminating the entire interface/abstract hierarchy, `GenericTag` becomes a standalone concrete class. There is no `super.getCode()` to delegate to. `getCode()` is a trivial field accessor: `return this.code`. Zero NPE risk. + +### What gets dropped + +| Interface / Abstract Class | Current Purpose | Why Droppable | +|---|---|---| +| **`IElement`** | `default getNip() { return "1"; }` — a default method returning a constant | Dead weight. Nothing meaningful depends on the NIP string. | +| **`IGenericElement`** | Exposes `getAttributes()`/`addAttribute()` for `ElementAttribute` | Goes away with `ElementAttribute` (Phase 6). | +| **`ITag`** | `setParent(IEvent)`, `getCode()` — 2 methods | With one concrete tag class, the interface adds no polymorphism. `GenericEvent` references `GenericTag` directly. | +| **`IEvent`** | `getId()` — extends `IElement`, `IBech32Encodable` | With one concrete event class, same reasoning. `GenericEvent` has these methods directly. | +| **`IBech32Encodable`** | `toBech32()` — 1 method | `toBech32()` stays as a method on `GenericEvent`. Doesn't need an interface. | +| **`Deleteable`** | `getKind()` — 1 method | `GenericEvent` already has `getKind()`. The interface adds nothing. | +| **`BaseEvent`** | Empty abstract class: `abstract class BaseEvent implements IEvent {}` | Inline into `GenericEvent`. | +| **`BaseTag`** | Factory methods + reflection-based `getSupportedFields()` / `getFieldValue()` | Factory methods migrate to `GenericTag.of()`. Reflection methods deleted — they only served annotation-driven serialization of concrete tags. | + +### What gets kept + +| Interface / Class | Why It Earns Its Existence | +|---|---| +| **`ISignable`** | Real polymorphic contract. `Identity.sign(ISignable)` decouples signing from the event model. Tiny (4 methods). In practice `GenericEvent` is the only implementor after simplification, but the interface keeps the `id` module independent of the `event` module — which matters for the module merge (Phase 10). | +| **`IKey`** + **`BaseKey`** | Real shared behavior for `PublicKey`/`PrivateKey` — Bech32 encoding, hex conversion, equality semantics. Worth keeping. | +| **`IDecoder`** | Shared by 7 decoder classes. Provides a shared `ObjectMapper` instance and `decode(String)` contract. Could be simplified to a static utility + functional interface later. | +| **`BaseMessage`** | Genuinely polymorphic — 7+ distinct message types share the `command` field and `encode()` contract. | + +### Remove annotations +- **`@Tag` annotation** — no longer needed. GenericTag stores its code in a field. +- **`@Event` annotation** — no longer needed. No concrete event subclasses. +- **`@Key` annotation on tag fields** — no longer needed for tag serialization. + +Note: `@Key` is still used on `GenericEvent` fields for event serialization. Evaluate whether it can be replaced by explicit serialization logic in `EventSerializer` (which already knows the field order). If so, `@Key` can be dropped entirely. + +### What `GenericTag` looks like after this phase + +```java +@Data +@JsonSerialize(using = GenericTagSerializer.class) +public class GenericTag { + + private String code; + private final List params; + + public GenericTag(String code, String... params) { + this.code = code; + this.params = new ArrayList<>(List.of(params)); + } + + public GenericTag(String code, List params) { + this.code = code; + this.params = new ArrayList<>(params); + } + + public String getCode() { return this.code; } + + public List getParams() { return Collections.unmodifiableList(this.params); } + + /** NIP-01 wire format: ["code", "param0", "param1", ...] */ + public List toArray() { + var result = new ArrayList(); + result.add(code); + result.addAll(params); + return result; + } + + /** Factory method — the primary way to create tags. */ + public static GenericTag of(String code, String... params) { + return new GenericTag(code, params); + } + + public static GenericTag of(String code, List params) { + return new GenericTag(code, params); + } +} +``` + +No interfaces, no abstract classes, no `ElementAttribute`, no annotations, no reflection. Just a code and a list of strings — exactly what a Nostr tag is. + +### What `GenericEvent` looks like after this phase + +```java +@Data +public class GenericEvent implements ISignable { + + private String id; + private PublicKey pubKey; + private Long createdAt; + private int kind; + private List tags; // was List + private String content; + private Signature signature; + + // builder, update(), validate(), toBech32(), isReplaceable(), etc. + // No BaseEvent parent, no IEvent, no Deleteable +} +``` + +### Ripple effects +1. **`GenericEvent.tags` type** — changes from `List` to `List` +2. **`BaseMessage implements IElement`** — remove `implements IElement`; the `getNip()` default was never used meaningfully +3. **`IDecoder`** — change bound to `IDecoder` (unbounded) +4. **`GenericTagQuery implements IElement`** — remove `implements IElement`; it's a standalone record +5. **`EventMessage`** — references `IEvent` currently; change to reference `GenericEvent` directly +6. **`BaseTag.setParent(IEvent)` no-op** — disappears entirely since `GenericTag` doesn't implement `ITag` +7. **`GenericEvent.updateTagsParents()`** — can be removed (it called `setParent` which was a no-op) + +### Risk: Medium +Breaking change for any code referencing these interfaces. Justified by a major version bump. + +--- + +## Phase 8: Simplify the Filter System + +### Current state: 17 filter classes +Most are thin wrappers: `KindFilter`, `AuthorFilter`, `SinceFilter`, `UntilFilter`, `HashtagTagFilter`, `AddressTagFilter`, etc. + +### Proposed state: 3 classes +Keep only: +1. **`EventFilter`** — The composable filter builder. Enhance it to be the single entry point: + ```java + EventFilter.builder() + .kinds(List.of(1, 7)) + .authors(List.of("pubkey_hex")) + .since(timestamp) + .until(timestamp) + .addTagFilter("e", List.of("event_id")) + .addTagFilter("p", List.of("pubkey")) + .addTagFilter("#t", List.of("nostr")) + .limit(100) + .build(); + ``` +2. **`Filters`** — Container for multiple EventFilter (OR logic), needed for REQ messages +3. **`Filterable`** — Interface (if still needed) + +### Delete +- AbstractFilterable, KindFilter, AuthorFilter, SinceFilter, UntilFilter +- HashtagTagFilter, AddressTagFilter, GeohashTagFilter, IdentifierTagFilter +- ReferencedEventFilter, ReferencedPublicKeyFilter, UrlTagFilter, VoteTagFilter +- GenericTagQueryFilter (merge into EventFilter) + +### Risk: Low +Filters are internal plumbing. The new EventFilter API is cleaner. + +--- + +## Phase 9: Simplify Serialization Infrastructure + +### Delete +- All concrete tag serializers (AddressTagSerializer, ReferenceTagSerializer, etc.) +- All concrete event deserializers (CalendarEventDeserializer, ClassifiedListingEventDeserializer, etc.) +- `BaseTagSerializer` — merge into GenericTagSerializer +- Codec classes tied to concrete types + +### Keep +- `GenericEventSerializer` / `GenericEventDeserializer` — core event JSON +- `GenericTagSerializer` — simplified to output `[code, param0, param1, ...]` from `List` +- `EventSerializer` — canonical NIP-01 serialization for ID/signature computation +- `PublicKeyDeserializer`, `SignatureDeserializer` — needed for key/sig parsing +- `BaseEventEncoder`, `BaseMessageDecoder` — needed for wire protocol +- `EventJsonMapper` — central mapper + +### Simplify +- `TagDeserializer` — simplify to always produce `GenericTag(code, List)` directly from the JSON array. No more dispatch to concrete types via TagRegistry. + +--- + +## Phase 10: Replace Custom Hex with `java.util.HexFormat` + +### What gets deleted +- `NostrUtil.HEX_ARRAY` constant — hand-rolled lookup table +- `NostrUtil.bytesToHex(byte[])` — manual char-array loop with `toLowerCase()` +- `NostrUtil.hexToBytesConvert(String)` — manual `Character.digit()` loop +- `NostrUtil.hex128ToBytes(String)` — duplicate of `hexToBytes` with different length +- `NostrUtil.nip04PubKeyHexToBytes(String)` — duplicate of `hexToBytes` with different length + +### Why +Java 17+ provides `java.util.HexFormat` — a standard, well-tested, performant hex codec. The project requires Java 21+, so it's available. The current implementation is ~30 lines of hand-rolled byte manipulation that `HexFormat` replaces with one-liners. + +The three length-specific methods (`hexToBytes` for 64-char, `hex128ToBytes` for 128-char, `nip04PubKeyHexToBytes` for 66-char) differ only in the length parameter passed to `HexStringValidator`. They can be collapsed into one method. + +### Replacement + +```java +import java.util.HexFormat; + +public class NostrUtil { + + private static final HexFormat HEX = HexFormat.of(); + + /** Encode bytes to lowercase hex string. */ + public static String bytesToHex(byte[] b) { + return HEX.formatHex(b); + } + + /** Decode hex string to bytes with length validation. */ + public static byte[] hexToBytes(String hex, int expectedHexLength) { + HexStringValidator.validateHex(hex, expectedHexLength); + return HEX.parseHex(hex); + } + + // Convenience overloads for common lengths: + + /** Decode 64-char hex (32-byte keys). */ + public static byte[] hexToBytes(String hex) { + return hexToBytes(hex, 64); + } + + /** Decode 128-char hex (64-byte Schnorr signatures). */ + public static byte[] hex128ToBytes(String hex) { + return hexToBytes(hex, 128); + } +} +``` + +`HexFormat.of()` produces lowercase output by default — matching the current `bytesToHex` behavior (which uppercases then calls `toLowerCase()`). The `HexFormat` instance is thread-safe and reusable. + +### Call sites affected (21 files) + +No call-site changes needed for `bytesToHex` — signature is identical. + +For `hexToBytes` / `hex128ToBytes` — signatures are identical, so existing callers work unchanged. + +The only caller of `nip04PubKeyHexToBytes` is `EncryptedDirectMessage`: +```java +// Before +ECPoint pubKeyPt = curve.decodePoint(NostrUtil.nip04PubKeyHexToBytes("02" + publicKeyHex)); + +// After — use the general method with explicit length +ECPoint pubKeyPt = curve.decodePoint(NostrUtil.hexToBytes("02" + publicKeyHex, 66)); +``` + +### Behavioral difference +`HexFormat.parseHex()` throws `IllegalArgumentException` on invalid hex characters. The current `hexToBytesConvert()` silently returns `-1` bytes from `Character.digit()` on invalid input (which then corrupt downstream data). The `HexFormat` behavior is strictly better — fail fast instead of silent corruption. + +### Risk: Very Low +Drop-in replacement. Same signatures, same output, stricter input validation. One call site changes (`nip04PubKeyHexToBytes` → `hexToBytes` with length). + +--- + +## Phase 11: Harden WebSocket Client + +The current WebSocket client has several reliability and robustness issues that become more critical once the api-layer orchestration (NostrRelayRegistry, WebSocketClientHandler, NostrSubscriptionManager) is removed and users interact with the client directly. + +### Current Weaknesses + +#### 1. Brittle termination detection via string prefix matching + +```java +// Current — StandardWebSocketClient line 194 +return payload.startsWith("[\"EOSE\"") + || payload.startsWith("[\"OK\"") + || payload.startsWith("[\"NOTICE\"") + || payload.startsWith("[\"CLOSED\""); +``` + +**Problems:** +- Breaks on JSON formatting variations: `[ "EOSE"` (space after bracket) is valid JSON but won't match +- Could false-positive on content that starts with these strings (unlikely but possible) +- Hardcoded — new Nostr relay message types require code changes +- No validation that the message is well-formed JSON + +**Fix:** Parse the first element of the JSON array properly: +```java +private boolean isTerminationMessage(String payload) { + if (payload == null || payload.length() < 2) return false; + try { + JsonNode node = objectMapper.readTree(payload); + if (!node.isArray() || node.isEmpty()) return false; + String command = node.get(0).asText(); + return TERMINATION_COMMANDS.contains(command); + } catch (Exception e) { + return false; + } +} + +private static final Set TERMINATION_COMMANDS = + Set.of("EOSE", "OK", "NOTICE", "CLOSED", "AUTH"); +``` + +Jackson ObjectMapper is reusable and thread-safe. The cost of parsing is negligible compared to the network round-trip that produced the message. + +#### 2. No automatic reconnection + +When a connection drops mid-subscription, all messages are silently lost. The caller's error listener fires, but there's no recovery mechanism. The caller must detect the failure, create a new client, and re-establish all subscriptions. + +**Fix:** Add a `ReconnectingWebSocketClient` wrapper: + +```java +public class ReconnectingWebSocketClient implements WebSocketClientIF { + private final String relayUri; + private final ReconnectPolicy policy; + private volatile StandardWebSocketClient delegate; + private final Map activeSubscriptions = new ConcurrentHashMap<>(); + + // On disconnect: + // 1. Exponential backoff reconnect (configurable max retries, max delay) + // 2. Re-register all active subscriptions after reconnect + // 3. Notify listeners of reconnection via onReconnect callback + // 4. Give up after max retries and notify error listeners +} +``` + +Key behaviors: +- Reconnect with exponential backoff (default: 1s → 2s → 4s → ... → 60s cap, infinite retries) +- Re-send active subscription REQ messages after reconnect +- Fire a `reconnectListener` callback so callers know subscriptions were re-established +- Configurable via `ReconnectPolicy` (max retries, base delay, max delay, jitter) +- Thread-safe — reconnection happens on a background thread, sends are queued or rejected during reconnect + +#### 3. No ping/pong heartbeat for stale connection detection + +The idle timeout (default 1 hour) is passive — it only fires if the *container* detects inactivity. If the network silently drops (e.g., NAT timeout, mobile network switch), the connection appears alive but no messages flow. The caller won't know until the next send attempt fails. + +**Fix:** Add periodic WebSocket ping frames: + +```java +private final ScheduledExecutorService heartbeatExecutor = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "nostr-heartbeat"); + t.setDaemon(true); + return t; + }); + +// Schedule after connection established: +heartbeatExecutor.scheduleAtFixedRate(() -> { + try { + if (clientSession.isOpen()) { + clientSession.sendMessage(new PingMessage()); + } + } catch (Exception e) { + log.warn("Heartbeat ping failed, connection may be dead", e); + handleConnectionLoss(e); + } +}, pingIntervalMs, pingIntervalMs, TimeUnit.MILLISECONDS); +``` + +Default interval: 30 seconds (configurable via `nostr.websocket.ping-interval-ms`). Set to 0 to disable. + +#### 4. Unbounded message accumulation in `PendingRequest` + +When a REQ produces a large result set, `PendingRequest.events` accumulates all messages in memory until EOSE arrives. A relay returning 100k events could OOM the client. + +**Fix:** Add a configurable event count limit: +```java +private static final int DEFAULT_MAX_EVENTS_PER_REQUEST = 10_000; + +void addEvent(String event) { + if (events.size() >= maxEventsPerRequest) { + log.warn("Event limit reached ({}), completing request early", maxEventsPerRequest); + complete(); + return; + } + events.add(event); +} +``` + +Configurable via `nostr.websocket.max-events-per-request`. Default: 10,000. Users expecting larger result sets should use subscriptions instead of blocking sends. + +#### 5. No connection state query + +Users cannot ask "is this client connected?" without attempting a send and catching an exception. + +**Fix:** Add state tracking: +```java +public enum ConnectionState { CONNECTING, CONNECTED, RECONNECTING, CLOSED } + +private final AtomicReference state = + new AtomicReference<>(ConnectionState.CONNECTING); + +public ConnectionState getConnectionState() { return state.get(); } +public boolean isConnected() { return state.get() == ConnectionState.CONNECTED; } +``` + +Update state in `afterConnectionEstablished()`, `afterConnectionClosed()`, and reconnection logic. + +#### 6. Configuration scattered across System properties and Spring `@Value` + +Currently: +- `@Value` annotations for `awaitTimeoutMs`, `pollIntervalMs`, `maxIdleTimeoutMs` (Spring injection) +- `System.getProperty()` calls in `createSpringClient()` for container-level config +- Constructor parameters for programmatic config + +These three config paths can conflict. A user setting `nostr.websocket.max-idle-timeout-ms` as a Spring property won't affect `createSpringClient()` which reads system properties. + +**Fix:** Consolidate into a `WebSocketClientConfig` record: +```java +public record WebSocketClientConfig( + long awaitTimeoutMs, // default 60_000 + long maxIdleTimeoutMs, // default 3_600_000 + long pingIntervalMs, // default 30_000, 0 to disable + int maxTextMessageBufferSize, // default 1_048_576 + int maxEventsPerRequest, // default 10_000 + ReconnectPolicy reconnectPolicy // default: exponential backoff, infinite retries +) { + public static WebSocketClientConfig defaults() { ... } + public static Builder builder() { ... } +} +``` + +Spring autoconfiguration can populate this from `application.properties`. Programmatic users pass it to the constructor. One source of truth. + +#### 7. `pollIntervalMs` parameter is dead code + +Documented as "no longer used for polling" but still required in constructors, still validated, still stored. It's API surface that misleads users. + +**Fix:** Remove it. This is a major version — no backward-compat obligation. The constructor becomes: +```java +public StandardWebSocketClient(String relayUri, WebSocketClientConfig config) +``` + +#### 8. `send()` returns empty list on timeout (silent failure) + +```java +} catch (TimeoutException e) { + // ... + return List.of(); // Caller can't distinguish "no results" from "timed out" +} +``` + +An empty list is a valid response (relay has no matching events). Returning it on timeout makes the failure invisible. + +**Fix:** Throw a dedicated exception: +```java +} catch (TimeoutException e) { + throw new RelayTimeoutException( + "Timed out waiting for relay response after " + timeout + "ms", + clientSession.getUri().toString(), timeout); +} +``` + +Where `RelayTimeoutException extends IOException` so existing catch blocks still work but callers can distinguish the failure mode. + +#### 9. No NIP-42 AUTH support at the client level + +Relays may send `["AUTH", "challenge"]` requiring the client to respond with a signed authentication event (kind 22242). Currently there's no mechanism for this — AUTH messages are just dispatched to regular listeners with no framework support. + +**The NIP-42 protocol flow:** +``` +Client Relay + | | + |-------- connect ------------->| + | | + |<------ ["AUTH", "challenge"]--| relay challenges client + | | + |--- ["AUTH", signed_event] --->| client responds with kind 22242 event + | | + |<------ ["OK", ...] ---------| relay accepts/rejects +``` + +The signed response event must contain: +```json +{ + "kind": 22242, + "tags": [["relay", "wss://relay.example.com/"], ["challenge", "the-challenge-string"]], + "content": "", + "pubkey": "...", "id": "...", "sig": "...", "created_at": ... +} +``` + +**The dependency problem:** Building that response requires `GenericEvent` (from `nostr-java-event`) and `Identity.sign()` (from `nostr-java-identity`). But in the dependency chain `core → event → identity → client`, the client module **cannot** import event or identity — that would create a circular dependency. The client only works with raw JSON strings. + +**Fix:** Invert the dependency with a callback. The client defines the *contract*, the application provides the *implementation*: + +```java +// In nostr-java-client — knows nothing about events or signing +@FunctionalInterface +public interface RelayAuthHandler { + /** + * Called when a relay sends an AUTH challenge. + * + * @param challenge the challenge string from the relay + * @param relayUri the URI of the relay that sent the challenge + * @return a fully encoded AUTH message as JSON (e.g. ["AUTH", {signed_event}]), + * or null to skip authentication + */ + String handleAuthChallenge(String challenge, String relayUri); +} +``` + +**Application-side implementation** (has access to all modules): +```java +Identity identity = Identity.create(myPrivateKey); + +RelayAuthHandler authHandler = (challenge, relayUri) -> { + GenericEvent authEvent = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(22242) + .content("") + .tags(List.of( + GenericTag.of("relay", relayUri), + GenericTag.of("challenge", challenge) + )) + .build(); + identity.sign(authEvent); + return "[\"AUTH\"," + EventJsonMapper.toJson(authEvent) + "]"; +}; + +// Pass it when creating the client +var config = WebSocketClientConfig.builder() + .authHandler(authHandler) + .build(); +var client = new NostrWebSocketClient("wss://relay.example.com", config); +``` + +**Client-side handling** (inside `NostrWebSocketClient`): +```java +@Override +protected void handleTextMessage(WebSocketSession session, TextMessage message) { + String payload = message.getPayload(); + + // Check for AUTH challenge before normal dispatch + if (isAuthChallenge(payload)) { + handleAuth(payload); + return; // Don't dispatch AUTH challenges to regular listeners + } + + // ... normal message handling (dispatch, termination detection, etc.) +} + +private void handleAuth(String payload) { + if (authHandler == null) { + log.warn("Relay sent AUTH challenge but no RelayAuthHandler configured"); + return; + } + + String challenge = objectMapper.readTree(payload).get(1).asText(); + String relayUri = clientSession.getUri().toString(); + + try { + String response = authHandler.handleAuthChallenge(challenge, relayUri); + if (response != null) { + clientSession.sendMessage(new TextMessage(response)); + log.debug("Sent AUTH response to relay {}", relayUri); + } + } catch (Exception e) { + log.warn("Auth handler failed for relay {}", relayUri, e); + notifyError(e); + } +} +``` + +**Design decisions:** + +- **`String` return type, not a typed event** — the client module can't depend on `GenericEvent`. The application serializes the event to JSON before returning. This keeps the module boundary clean. +- **Optional (nullable handler)** — not all relays require auth, not all apps need it. If no handler is set, AUTH challenges are logged and ignored. The client still works for public relay operations. +- **Re-authentication after reconnect** — when the `ReconnectPolicy` (fix #2) re-establishes a connection, the relay sends a fresh AUTH challenge. The same handler fires again automatically — it's stateless, so no special handling is needed. +- **Late AUTH challenges** — some relays send AUTH mid-session, not just on connect. The `handleTextMessage` check runs on every inbound message, so late challenges are handled transparently. +- **AUTH intercept before dispatch** — AUTH challenges are consumed by the handler and not forwarded to regular message listeners or subscription callbacks. This prevents application code from seeing protocol-level messages it shouldn't need to handle. + +#### 10. Spring framework coupling is mandatory + +`StandardWebSocketClient` extends `TextWebSocketHandler`, requires Spring WebSocket, Spring Retry, and is annotated with `@Component`. Users who don't use Spring cannot use the client without pulling in the entire Spring Boot WebSocket dependency tree. + +**Fix:** Split into two layers: +1. **`NostrWebSocketClient`** — plain Java, no Spring dependencies. Uses `jakarta.websocket` (JSR 356) directly. Includes reconnection, heartbeat, config, auth handler. +2. **`SpringNostrWebSocketClient`** — thin Spring wrapper adding `@Component`, `@Retryable`, property binding. Delegates to `NostrWebSocketClient`. + +Users who don't use Spring get a fully functional client. Spring users get autoconfiguration on top. + +### Proposed client class structure after hardening + +``` +nostr-java-client/ +├── NostrWebSocketClient.java -- Core client (plain Java, jakarta.websocket) +├── ReconnectPolicy.java -- Reconnect config (max retries, backoff, jitter) +├── WebSocketClientConfig.java -- Unified configuration record +├── RelayAuthHandler.java -- NIP-42 auth callback interface +├── RelayTimeoutException.java -- Typed timeout exception +├── ConnectionState.java -- Enum: CONNECTING, CONNECTED, RECONNECTING, CLOSED +├── ConnectionListener.java -- Callbacks: onConnect, onReconnect, onDisconnect +├── spring/ +│ ├── SpringNostrWebSocketClient.java -- Spring wrapper (@Component, @Retryable) +│ ├── NostrRetryable.java -- Retry annotation +│ └── RetryConfig.java -- @EnableRetry config +└── (removed: WebSocketClientIF, WebSocketClientFactory, SpringWebSocketClientFactory, + StandardWebSocketClient, SpringWebSocketClient) +``` + +### Summary of hardening changes + +| Issue | Severity | Fix | +|---|---|---| +| Brittle string-prefix termination detection | High | Proper JSON parsing of message command | +| No automatic reconnection | High | `ReconnectPolicy` with exponential backoff, subscription re-registration | +| No heartbeat/ping | High | Periodic WebSocket ping frames (default 30s) | +| Unbounded message accumulation | Medium | Configurable max events per request (default 10k) | +| Silent timeout failure (empty list) | Medium | Throw `RelayTimeoutException` | +| No connection state query | Medium | `ConnectionState` enum + `isConnected()` | +| Scattered configuration | Medium | Unified `WebSocketClientConfig` record | +| Dead `pollIntervalMs` parameter | Low | Remove | +| No NIP-42 AUTH support | Medium | `RelayAuthHandler` callback interface | +| Mandatory Spring coupling | Medium | Plain Java core + optional Spring wrapper | + +### Risk: Medium +The client API changes (new constructor signatures, new exception type, removed `pollIntervalMs`) are breaking changes, but they're already part of the 2.0.0 major version. The reconnection and heartbeat additions are purely additive — they add reliability without changing existing call patterns. + +--- + +## Phase 12: Merge Modules + +After the above simplifications, the 7 remaining modules (util, crypto, base, event, id, encryption, client) become small enough to consolidate. + +### Module merges + +| Current Modules | Merged Into | Contents | +|---|---|---| +| `util` + `crypto` | **`nostr-java-core`** | Hashing, Schnorr signatures, Bech32, hex utils, exceptions | +| `base` + `event` | **`nostr-java-event`** | GenericEvent, GenericTag, Kinds, PublicKey, PrivateKey, Signature, messages, filters, serialization, ISignable, IKey/BaseKey | +| `id` + `encryption` | **`nostr-java-identity`** | Identity, MessageCipher04, MessageCipher44 | +| `client` | **`nostr-java-client`** | NostrWebSocketClient, reconnection, heartbeat, Spring wrapper | + +### Resulting dependency chain +``` +nostr-java-core → nostr-java-event → nostr-java-identity → nostr-java-client +``` + +**4 modules** instead of 9. Each module has a clear, focused purpose. + +### Risk: High (breaking for all existing consumers) +This is a major restructuring affecting all import paths. Justified by a 2.0.0 major version bump. Could be deferred to a separate release after the class-level simplifications stabilize. + +--- + +## Summary of Deletions + +| Category | Current | After | Deleted | +|---|:---:|:---:|:---:| +| Modules | 9 | 4 | 5 | +| Event classes | 40 | 1 | 39 | +| Tag classes | 18 | 1 | 17 | +| Entity classes | 27 | 0 | 27 | +| NIP API classes | 26 | 0 | 26 | +| Interfaces/abstracts dropped | — | — | 8 (ITag, IEvent, IElement, IGenericElement, IBech32Encodable, Deleteable, BaseEvent, BaseTag) | +| Other classes dropped | — | — | 3 (Kind enum, ElementAttribute, TagRegistry) | +| Filter classes | 17 | 3 | 14 | +| Factory classes | ~10 | 0 | ~10 | +| Serializer/Deserializer | ~16 | ~5 | ~11 | +| Example classes | 6 | 0 | 6 | +| Annotation classes | 3 | 0-1 | 2-3 (@Tag, @Event, possibly @Key) | +| **Total classes** | **~180** | **~40** | **~140** | + +--- + +## What Survives (Complete List) + +### `nostr-java-core` (merged util + crypto) +- `NostrUtil` — SHA-256, hex encoding, random bytes +- `Schnorr` — BIP340 Schnorr signatures +- `Bech32`, `Bech32Prefix` — Bech32 encoding/decoding +- `EncryptedDirectMessage` — NIP-04 AES-256-CBC primitives +- `Point`, `Pair` — elliptic curve math +- Exception classes — NostrException, NostrCryptoException, etc. +- Validators — HexStringValidator, Nip05Validator +- HTTP support — HttpClientProvider, DefaultHttpClientProvider + +### `nostr-java-event` (merged base + event) +- **`GenericEvent`** — the sole event class +- **`GenericTag`** — the sole tag class (code + `List`) +- **`Kinds`** — static int constants for common kinds + range check utilities +- **`PublicKey`**, **`PrivateKey`**, **`Signature`** — key/sig types +- **`BaseKey`**, **`IKey`** — shared key behavior +- **`ISignable`** — signing contract +- **`BaseMessage`** + message classes — EventMessage, ReqMessage, CloseMessage, OkMessage, EoseMessage, NoticeMessage, GenericMessage +- **`EventFilter`**, **`Filters`** — filter system +- **Serialization** — GenericEventSerializer/Deserializer, GenericTagSerializer, EventSerializer, PublicKeyDeserializer, SignatureDeserializer, EventJsonMapper +- **`IDecoder`** + codec classes — GenericEventDecoder, GenericTagDecoder, BaseMessageDecoder, BaseEventEncoder, etc. +- Supporting types — Relay, SubscriptionId, Marker, Command, Encoder, Nip05Content +- Validation — EventValidator + +### `nostr-java-identity` (merged id + encryption) +- **`Identity`** — key management and signing +- **`MessageCipher`** interface + **`MessageCipher04`**, **`MessageCipher44`** — encryption + +### `nostr-java-client` +- **`NostrWebSocketClient`** — core WebSocket client (plain Java, jakarta.websocket). Blocking `send()`, non-blocking `subscribe()`, automatic reconnection, heartbeat ping, NIP-42 auth callback, connection state tracking +- **`WebSocketClientConfig`** — unified configuration record (timeouts, buffer sizes, heartbeat interval, max events per request, reconnect policy) +- **`ReconnectPolicy`** — reconnection strategy (max retries, base delay, max delay, jitter) +- **`RelayAuthHandler`** — NIP-42 authentication callback interface +- **`RelayTimeoutException`** — typed exception for timeout failures (replaces silent empty-list return) +- **`ConnectionState`** — enum: CONNECTING, CONNECTED, RECONNECTING, CLOSED +- **`ConnectionListener`** — callbacks: onConnect, onReconnect, onDisconnect +- **`spring/SpringNostrWebSocketClient`** — Spring wrapper (@Component, @Retryable, property binding) +- **`spring/NostrRetryable`**, **`spring/RetryConfig`** — Spring Retry support + +--- + +## What Users Gain + +1. **Dramatically smaller API surface** — one event class, one tag class, ~40 total classes +2. **No version lag** — new NIPs don't require library updates. Users create `GenericEvent` with any kind integer. +3. **Predictable deserialization** — everything comes back as `GenericEvent` with `GenericTag`. No surprise polymorphism. +4. **No NPE traps** — the `GenericTag.getCode()` bug and its entire class of dual-path problems are structurally eliminated. +5. **No reflection** — tag code/value access is direct field access. Works cleanly under Java 21 JPMS. +6. **Reliable connectivity** — automatic reconnection with subscription re-registration, heartbeat ping for stale connection detection, typed timeout exceptions instead of silent failures, NIP-42 auth support. +7. **No Spring lock-in** — plain Java WebSocket client works without Spring. Spring wrapper optional. +8. **Easier to learn** — the whole library fits in your head. +9. **Easier to maintain** — ~40 classes instead of ~180. + +## What Users Lose + +1. **Type safety for specific NIPs** — no more `TextNoteEvent` vs `ReactionEvent`. Users work with kind integers. +2. **Convenience builders** — no more `NIP04.createEncryptedDM()`. Users compose events manually. +3. **Content parsing** — no more entity classes for structured content. Users parse JSON content themselves. +4. **NIP-specific validation** — tag/content validation is removed. Users validate their own events. +5. **Kind enum autocompletion** — replaced by `Kinds.TEXT_NOTE` constants (same IDE experience, less machinery). + +--- + +## Migration Path + +This is a **major version** change (2.0.0). Provide: + +1. A migration guide mapping old patterns to new: + ```java + // Old + TextNoteEvent event = new TextNoteEvent(pubKey, List.of(), "Hello"); + EventTag eTag = new EventTag("abc123", "wss://relay.example.com", Marker.REPLY); + event.addTag(eTag); + + // New + GenericEvent event = GenericEvent.builder() + .pubKey(pubKey) + .kind(Kinds.TEXT_NOTE) + .content("Hello") + .tags(List.of(GenericTag.of("e", "abc123", "wss://relay.example.com", "reply"))) + .build(); + ``` +2. Clear examples in documentation showing how to create common event types +3. Update CLAUDE.md, README, and all docs to reflect the new architecture + +--- + +## Recommended Implementation Order + +1. **Phase 1**: Remove `api` + `examples` modules (lowest risk, highest impact) +2. **Phase 4**: Remove entity classes (no dependents after Phase 1) +3. **Phase 2**: Remove concrete event subclasses +4. **Phase 3**: Remove concrete tag subclasses + TagRegistry +5. **Phase 5**: Drop `Kind` enum, add `Kinds` constants +6. **Phase 6**: Drop `ElementAttribute`, make tags use `List` +7. **Phase 7**: Drop interfaces/abstract classes, flatten hierarchy +8. **Phase 8**: Simplify filters +9. **Phase 9**: Simplify serialization +10. **Phase 10**: Replace custom hex with `java.util.HexFormat` (small, independent — can be done any time) +11. **Phase 11**: Harden WebSocket client (can be done in parallel with phases 5-9) +12. **Phase 12**: Module merges (final step, can be a separate release) + +Each phase can be a separate PR for easier review. Phases 1-4 remove dead code. Phases 5-7 reshape the core model. Phases 8-10 clean up infrastructure. Phase 11 hardens connectivity. Phase 12 restructures the build. diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md index 4f2d00867..e4cf722e2 100644 --- a/docs/explanation/architecture.md +++ b/docs/explanation/architecture.md @@ -4,593 +4,237 @@ This document explains the overall architecture of nostr-java and how its module **Purpose:** Provide a high-level mental model for contributors and integrators. **Audience:** Developers extending or integrating the library. -**Last Updated:** 2025-10-06 (Post-refactoring) +**Last Updated:** 2026-02-24 (v2.0.0 simplification) --- ## Table of Contents -1. [Module Overview](#modules) -2. [Clean Architecture Principles](#clean-architecture-principles) +1. [Design Philosophy](#design-philosophy) +2. [Module Overview](#modules) 3. [Data Flow](#data-flow) -4. [Event Lifecycle](#event-lifecycle-happy-path) -5. [Design Patterns](#design-patterns) -6. [Refactored Components](#refactored-components-2025) -7. [Error Handling](#error-handling-principles) -8. [Extensibility](#extensibility) -9. [Security](#security-notes) +4. [Event Lifecycle](#event-lifecycle) +5. [Core Classes](#core-classes) +6. [Design Patterns](#design-patterns) +7. [Error Handling](#error-handling) +8. [Security](#security-notes) --- -## Modules - -The nostr-java library is organized into 9 modules following Clean Architecture principles with clear dependency direction (lower layers have no knowledge of upper layers). - -### Layer 1: Foundation (No Dependencies) - -#### `nostr-java-util` -**Purpose:** Cross-cutting utilities and validation helpers. - -**Key Classes:** -- `NostrException` hierarchy (protocol, crypto, encoding, network exceptions) -- `NostrUtil` - Common utility methods -- Validators (hex string, Bech32, etc.) - -**Dependencies:** None (foundation layer) - -#### `nostr-java-crypto` -**Purpose:** Cryptographic primitives and implementations. - -**Key Features:** -- BIP-340 Schnorr signature implementation -- Bech32 encoding/decoding (NIP-19) -- secp256k1 elliptic curve operations -- Uses BouncyCastle provider - -**Dependencies:** `nostr-java-util` - -### Layer 2: Domain Core - -#### `nostr-java-base` -**Purpose:** Core domain types and abstractions. - -**Key Classes:** -- `PublicKey`, `PrivateKey` - Identity primitives -- `Signature` - BIP-340 signature wrapper -- `Kind` - Event kind enumeration (NIP-01) -- `Encoder`, `IEvent`, `ITag` - Core interfaces -- `RelayUri`, `SubscriptionId` - Value objects (v0.6.2+) -- `NipConstants` - Protocol constants - -**Dependencies:** `nostr-java-util`, `nostr-java-crypto` - -#### `nostr-java-id` -**Purpose:** Identity and key material management. - -**Key Classes:** -- `Identity` - User identity (public/private key pair) -- Key generation and derivation - -**Dependencies:** `nostr-java-base`, `nostr-java-crypto` - -### Layer 3: Event Model - -#### `nostr-java-event` -**Purpose:** Concrete event and tag implementations for all NIPs. - -**Key Packages:** -- `nostr.event.impl.*` - Event implementations (GenericEvent, TextNoteEvent, etc.) -- `nostr.event.tag.*` - Tag implementations (EventTag, PubKeyTag, etc.) -- `nostr.event.validator.*` - Event validation (v0.6.2+) -- `nostr.event.serializer.*` - Event serialization (v0.6.2+) -- `nostr.event.util.*` - Event utilities (v0.6.2+) -- `nostr.event.json.*` - JSON mapping utilities (v0.6.2+) -- `nostr.event.message.*` - Relay protocol messages -- `nostr.event.filter.*` - Event filters (REQ messages) +## Design Philosophy -**Recent Refactoring (v0.6.2):** -- Extracted `EventValidator` - NIP-01 validation logic -- Extracted `EventSerializer` - Canonical serialization -- Extracted `EventTypeChecker` - Kind range classification -- Extracted `EventJsonMapper` - Centralized JSON configuration +nostr-java 2.0 follows a **minimalist, protocol-aligned** design: -**Dependencies:** `nostr-java-base`, `nostr-java-id` +- **One event class** — `GenericEvent` handles every Nostr event kind via `int kind`. No subclasses. +- **One tag class** — `GenericTag` stores a code and `List` params. No subclasses, no `ElementAttribute`. +- **Integer kinds** — `Kinds` utility provides named constants (`Kinds.TEXT_NOTE`, `Kinds.CONTACT_LIST`) and range checks. Any integer is valid — no enum gating. +- **4 modules** — each with a clear, focused purpose and a strict dependency chain. +- **Virtual Threads** — relay I/O and listener dispatch use Java 21 Virtual Threads for lightweight concurrency. -#### `nostr-java-encryption` -**Purpose:** NIP-04 and NIP-44 encryption implementations. +This design reduced the library from ~180 classes across 9 modules to ~40 classes across 4 modules, eliminating all NIP-specific concrete types, entity DTOs, factory hierarchies, and annotation-driven tag registration. -**Key Features:** -- NIP-04: Encrypted direct messages (deprecated) -- NIP-44: Versioned encrypted payloads (recommended) - -**Dependencies:** `nostr-java-base`, `nostr-java-crypto` - -### Layer 4: Infrastructure - -#### `nostr-java-client` -**Purpose:** WebSocket transport and relay communication. - -**Key Classes:** -- `SpringWebSocketClient` - Spring-based WebSocket implementation -- Retry and resilience mechanisms -- Connection pooling - -**Dependencies:** `nostr-java-base`, `nostr-java-event` - -### Layer 5: Application/API - -#### `nostr-java-api` -**Purpose:** High-level fluent API and factories. - -**Key Packages:** -- `nostr.api.nip*` - NIP-specific builders (NIP01, NIP57, NIP60, etc.) -- `nostr.api.factory.*` - Event and tag factories -- `nostr.api.client.*` - Client abstractions and dispatchers - -**Recent Refactoring (v0.6.2):** -- Extracted `NIP01EventBuilder`, `NIP01TagFactory`, `NIP01MessageFactory` -- Extracted `NIP57ZapRequestBuilder`, `NIP57ZapReceiptBuilder`, `NIP57TagFactory` -- Extracted `NostrRelayRegistry`, `NostrEventDispatcher`, `NostrRequestDispatcher`, `NostrSubscriptionManager` +--- -**Dependencies:** All lower layers +## Modules -### Layer 6: Examples +``` +nostr-java-core → nostr-java-event → nostr-java-identity → nostr-java-client +``` -#### `nostr-java-examples` -**Purpose:** Usage examples and demos. +### `nostr-java-core` +**Purpose:** Foundation utilities and cryptographic primitives. -**Contents:** -- Example applications -- Integration patterns -- Best practices +**Key classes:** +- `NostrUtil` — SHA-256 hashing, hex encoding via `java.util.HexFormat`, random byte generation +- `Schnorr` — BIP-340 Schnorr signature signing and verification +- `Bech32` / `Bech32Prefix` — Bech32 encoding/decoding (NIP-19) +- `Nip05Validator` — DNS-based identity validation with async/batch support +- `DefaultHttpClientProvider` — HTTP client with shared Virtual Thread executor +- Exception hierarchy: `NostrException`, `NostrCryptoException`, `NostrEncodingException`, `NostrNetworkException` -**Dependencies:** `nostr-java-api` +**Dependencies:** None (foundation layer). External: BouncyCastle, commons-lang3, Jackson. ---- +### `nostr-java-event` +**Purpose:** Event model, tag model, filters, messages, and JSON serialization. -## Clean Architecture Principles +**Key classes:** +- `GenericEvent` — The sole event class. Supports any kind via `int`. Implements `ISignable`. +- `GenericTag` — The sole tag class. `code` + `List params`. Factory: `GenericTag.of("e", "eventId", "relay")`. +- `Kinds` — Static `int` constants for common kinds plus range-check methods (`isReplaceable()`, `isEphemeral()`, `isAddressable()`). +- `EventFilter` — Builder-based composable filter for relay REQ messages. +- `Filters` — Container for multiple `EventFilter` instances (OR logic). +- `PublicKey`, `PrivateKey`, `Signature` — Value objects with Bech32 encoding. +- `ISignable` — Signing contract implemented by `GenericEvent`. +- `BaseMessage` and subclasses — `EventMessage`, `ReqMessage`, `CloseMessage`, `OkMessage`, `EoseMessage`, `NoticeMessage`. +- Serialization: `GenericEventSerializer`, `GenericEventDeserializer`, `GenericTagSerializer`, `TagDeserializer`, `EventSerializer` (canonical NIP-01), `EventJsonMapper`. -The nostr-java codebase follows Clean Architecture principles: +**Dependencies:** `nostr-java-core`. -### Dependency Rule -**Dependencies point inward** (from outer layers to inner layers): -``` -examples → api → client/encryption → event → base/id → crypto/util -``` +### `nostr-java-identity` +**Purpose:** Identity management, signing, and message encryption. -Inner layers have **no knowledge** of outer layers. For example: -- `nostr-java-base` does not depend on `nostr-java-event` -- `nostr-java-event` does not depend on `nostr-java-api` -- `nostr-java-crypto` does not depend on Spring or any framework +**Key classes:** +- `Identity` — Key pair management, event signing via `identity.sign(event)`. +- `MessageCipher04` — NIP-04 encrypted direct messages (legacy). +- `MessageCipher44` — NIP-44 versioned encryption (recommended). -### Layer Responsibilities +**Dependencies:** `nostr-java-event`. -1. **Foundation (util, crypto):** Framework-independent, reusable utilities -2. **Domain Core (base, id):** Business entities and value objects -3. **Event Model (event, encryption):** Domain logic and protocols -4. **Infrastructure (client):** External communication (WebSocket) -5. **Application (api):** Use cases and orchestration -6. **Presentation (examples):** User-facing demos +### `nostr-java-client` +**Purpose:** WebSocket relay communication with retry, Virtual Threads, and async support. -### Benefits +**Key classes:** +- `NostrRelayClient` — Spring `TextWebSocketHandler`-based WebSocket client. Blocking `send()`, non-blocking `subscribe()`, async `connectAsync()`/`sendAsync()`/`subscribeAsync()`. +- `RelayTimeoutException` — Typed exception for relay timeouts (replaces silent empty-list returns). +- `ConnectionState` — Enum: `CONNECTING`, `CONNECTED`, `RECONNECTING`, `CLOSED`. +- `NostrRetryable` / `RetryConfig` — Spring Retry annotation and configuration. -- ✅ **Testability:** Inner layers test without outer layer dependencies -- ✅ **Flexibility:** Swap implementations (e.g., replace Spring WebSocket) -- ✅ **Maintainability:** Changes in outer layers don't affect core -- ✅ **Framework Independence:** Core domain is pure Java +**Dependencies:** `nostr-java-identity`, Spring WebSocket, Spring Retry. --- ## Data Flow -```mermaid -flowchart LR - A[API Layer\nnostr-java-api] --> B[Event Model\nnostr-java-event] - B --> C[Base Types\nnostr-java-base] - C --> D[Crypto\nnostr-java-crypto] - B --> E[Encryption\nnostr-java-encryption] - A --> F[Client\nnostr-java-client] - F -->|WebSocket| G[Relay] ``` - -1. The API layer (factories/builders) creates domain events and tags. -2. Events serialize through base encoders/decoders into canonical NIP-01 JSON. -3. Crypto module signs/verifies (BIP-340), and encryption module handles NIP-04/44. -4. Client sends/receives frames to/from relays via WebSocket. - -## Event Lifecycle (Happy Path) - -```mermaid -sequenceDiagram - actor App - participant API as API (Factory) - participant Event as Event Model - participant Crypto as Crypto - participant Client as Client - participant Relay - - App->>API: configure kind, content, tags - API->>Event: build event object - Event->>Event: canonical serialize (NIP-01) - Event->>Crypto: hash + sign (BIP-340) - Crypto-->>Event: signature - Event-->>Client: signed event - Client->>Relay: SEND ["EVENT", ...] - Relay-->>Client: OK/notice +Application + │ + ▼ +GenericEvent.builder() ← build event with kind, content, tags + │ + ▼ +Identity.sign(event) ← compute ID (SHA-256) + Schnorr signature + │ + ▼ +NostrRelayClient.send( ← send over WebSocket + new EventMessage(event)) + │ + ▼ +Relay ← receives ["EVENT", {...}] ``` ---- - -## Design Patterns - -The nostr-java library employs several well-established design patterns to ensure maintainability and extensibility. - -### 1. Facade Pattern +1. The application builds a `GenericEvent` using the builder or constructor. +2. `Identity.sign()` computes the canonical NIP-01 JSON, hashes it (SHA-256), and signs with BIP-340 Schnorr. +3. The signed event is wrapped in an `EventMessage` and sent to relays via `NostrRelayClient`. +4. Relay responses (OK, EOSE, NOTICE, EVENT) are parsed and returned or dispatched to listeners. -**Where:** NIP implementation classes (NIP01, NIP57, etc.) +## Event Lifecycle -**Purpose:** Provide a simplified interface to complex subsystems. - -**Example:** -```java -// NIP01 facade coordinates builders, factories, and event management -NIP01 nip01 = new NIP01(identity); -nip01.createTextNoteEvent("Hello World") - .sign() - .send(relayUri); - -// Internally delegates to: -// - NIP01EventBuilder for event construction -// - NIP01TagFactory for tag creation -// - NIP01MessageFactory for message formatting -// - Event signing and serialization subsystems +``` +Build → Sign → Send → Receive response ``` -**Benefits:** -- Simplified API for common use cases -- Hides complexity of event construction -- Clear separation of concerns - -### 2. Builder Pattern - -**Where:** Event construction, complex parameter objects - -**Purpose:** Construct complex objects step-by-step with readable code. - -**Examples:** ```java -// GenericEvent builder +// 1. Build GenericEvent event = GenericEvent.builder() - .pubKey(publicKey) - .kind(Kind.TEXT_NOTE) + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) .content("Hello Nostr!") - .tags(List.of(eventTag, pubKeyTag)) + .tags(List.of(GenericTag.of("t", "nostr"))) .build(); -// ZapRequestParameters (Parameter Object pattern) -ZapRequestParameters params = ZapRequestParameters.builder() - .amount(1000L) - .lnUrl("lnurl...") - .relays(relayList) - .content("Great post!") - .recipientPubKey(recipient) - .build(); - -nip57.createZapRequestEvent(params); -``` - -**Benefits:** -- Readable event construction -- Handles optional parameters elegantly -- Replaces methods with many parameters - -### 3. Template Method Pattern - -**Where:** GenericEvent validation - -**Purpose:** Define algorithm skeleton in base class, allow subclasses to override specific steps. - -**Example:** -```java -// GenericEvent.java -public void validate() { - // Validate base fields (cannot be overridden) - EventValidator.validateId(this.id); - EventValidator.validatePubKey(this.pubKey); - EventValidator.validateSignature(this.signature); - EventValidator.validateCreatedAt(this.createdAt); - - // Call protected methods (CAN be overridden by subclasses) - validateKind(); - validateTags(); - validateContent(); -} - -protected void validateTags() { - EventValidator.validateTags(this.tags); -} +// 2. Sign +identity.sign(event); -// ZapRequestEvent.java (subclass) -@Override -protected void validateTags() { - super.validateTags(); // Base validation - // Additional validation: require 'amount' tag - requireTag("amount"); - requireTag("relays"); +// 3. Send +try (NostrRelayClient client = new NostrRelayClient("wss://relay.example.com")) { + List responses = client.send(new EventMessage(event)); } ``` -**Benefits:** -- Reuses common validation logic -- Allows specialization in subclasses -- Maintains consistency across event types +--- -### 4. Value Object Pattern +## Core Classes -**Where:** RelayUri, SubscriptionId, PublicKey, PrivateKey +### GenericEvent -**Purpose:** Immutable objects representing domain concepts with no identity, only value. +The sole event class. All Nostr events are represented as `GenericEvent` regardless of kind. -**Examples:** ```java -// RelayUri - validates WebSocket URIs -RelayUri relay = new RelayUri("wss://relay.398ja.xyz"); -// Throws IllegalArgumentException if not ws:// or wss:// - -// SubscriptionId - type-safe subscription identifiers -SubscriptionId subId = SubscriptionId.of("my-subscription"); -// Throws IllegalArgumentException if blank - -// Equality based on value, not object identity -RelayUri r1 = new RelayUri("wss://relay.398ja.xyz"); -RelayUri r2 = new RelayUri("wss://relay.398ja.xyz"); -assert r1.equals(r2); // true - same value +@Data +public class GenericEvent implements ISignable { + private String id; + private PublicKey pubKey; + private Long createdAt; + private int kind; + private List tags; + private String content; + private Signature signature; +} ``` -**Benefits:** -- Compile-time type safety (can't mix up String parameters) -- Encapsulates validation logic -- Immutable (thread-safe) -- Self-documenting code +Key methods: +- `GenericEvent.builder()` — fluent builder +- `isReplaceable()`, `isEphemeral()`, `isAddressable()` — kind range checks +- `toBech32()` — NIP-19 encoding -### 5. Factory Pattern +### GenericTag -**Where:** NIP01TagFactory, NIP57TagFactory, Event factories +The sole tag class. A tag is a code and a list of string parameters — exactly what the Nostr protocol specifies. -**Purpose:** Encapsulate object creation logic. - -**Examples:** ```java -// NIP01TagFactory - creates NIP-01 standard tags -BaseTag eventTag = tagFactory.createEventTag(eventId, recommendedRelay); -BaseTag pubKeyTag = tagFactory.createPubKeyTag(publicKey, mainRelay); -BaseTag genericTag = tagFactory.createGenericTag("t", "nostr"); - -// NIP01EventBuilder - creates events with proper defaults -GenericEvent textNote = eventBuilder.buildTextNote("Hello!"); -GenericEvent metadata = eventBuilder.buildMetadata(userMetadata); -``` - -**Benefits:** -- Centralizes creation logic -- Ensures proper initialization -- Makes testing easier (mock factories) - -### 6. Utility Pattern - -**Where:** EventValidator, EventSerializer, EventTypeChecker, EventJsonMapper +GenericTag.of("e", "eventId123", "wss://relay.example.com", "reply") +// Serializes to: ["e", "eventId123", "wss://relay.example.com", "reply"] -**Purpose:** Provide static helper methods for common operations. - -**Examples:** -```java -// EventValidator - validates NIP-01 fields -EventValidator.validateId(eventId); -EventValidator.validatePubKey(publicKey); -EventValidator.validateSignature(signature); - -// EventSerializer - canonical NIP-01 serialization -String json = EventSerializer.serialize(pubKey, createdAt, kind, tags, content); -String eventId = EventSerializer.serializeAndComputeId(...); - -// EventTypeChecker - classifies event kinds -boolean isReplaceable = EventTypeChecker.isReplaceable(kind); // 10000-19999 -boolean isEphemeral = EventTypeChecker.isEphemeral(kind); // 20000-29999 -boolean isAddressable = EventTypeChecker.isAddressable(kind); // 30000-39999 - -// EventJsonMapper - centralized JSON configuration -ObjectMapper mapper = EventJsonMapper.getMapper(); -String json = mapper.writeValueAsString(event); +tag.getCode() // "e" +tag.getParams() // ["eventId123", "wss://relay.example.com", "reply"] +tag.toArray() // ["e", "eventId123", "wss://relay.example.com", "reply"] ``` -**Benefits:** -- No object instantiation needed -- Clear single purpose -- Easy to test -- Reusable across the codebase - -### 7. Delegation Pattern +### Kinds -**Where:** GenericEvent → Validators/Serializers/TypeCheckers +Static `int` constants for common event kinds. Users can use any integer — these are convenience constants, not a gating mechanism. -**Purpose:** Delegate responsibilities to specialized classes. - -**Example:** ```java -// GenericEvent delegates instead of implementing directly -public class GenericEvent extends BaseEvent { - - public void update() { - // Delegates to EventSerializer - this._serializedEvent = EventSerializer.serializeToBytes(...); - this.id = EventSerializer.computeEventId(this._serializedEvent); - } - - public void validate() { - // Delegates to EventValidator - EventValidator.validateId(this.id); - EventValidator.validatePubKey(this.pubKey); - // ... - } - - public boolean isReplaceable() { - // Delegates to EventTypeChecker - return EventTypeChecker.isReplaceable(this.kind); - } -} +Kinds.TEXT_NOTE // 1 +Kinds.SET_METADATA // 0 +Kinds.CONTACT_LIST // 3 +Kinds.ENCRYPTED_DIRECT_MESSAGE // 4 + +Kinds.isReplaceable(10002) // true (10000-19999) +Kinds.isEphemeral(20001) // true (20000-29999) +Kinds.isAddressable(30023) // true (30000-39999) ``` -**Benefits:** -- Single Responsibility Principle -- Testable independently -- Reusable logic +### EventFilter -### 8. Initialization-on-Demand Holder (Singleton) +Builder-based filter for relay subscriptions: -**Where:** NostrSpringWebSocketClient - -**Purpose:** Thread-safe lazy singleton initialization. - -**Example:** ```java -public class NostrSpringWebSocketClient { - - private static final class InstanceHolder { - private static final NostrSpringWebSocketClient INSTANCE = - new NostrSpringWebSocketClient(); - - private InstanceHolder() {} - } - - public static NostrIF getInstance() { - return InstanceHolder.INSTANCE; - } -} +EventFilter filter = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE, Kinds.REACTION)) + .authors(List.of("pubkey_hex")) + .since(timestamp) + .until(timestamp) + .addTagFilter("t", List.of("nostr")) + .limit(100) + .build(); ``` -**Benefits:** -- Thread-safe without synchronization overhead -- Lazy initialization (created on first access) -- JVM guarantees initialization safety - --- -## Refactored Components (2025) - -Recent refactoring efforts (v0.6.2) have significantly improved code organization by extracting god classes into focused, single-responsibility components. - -### GenericEvent Extraction - -**Before:** 367 lines with mixed responsibilities -**After:** 374 lines + 3 extracted utility classes (472 additional lines) - -**Extracted Classes:** - -1. **EventValidator** (158 lines) → `nostr.event.validator.EventValidator` - - Validates all NIP-01 required fields - - Provides granular validation methods - - Reusable across the codebase - -2. **EventSerializer** (151 lines) → `nostr.event.serializer.EventSerializer` - - NIP-01 canonical JSON serialization - - Event ID computation (SHA-256) - - UTF-8 byte array conversion - -3. **EventTypeChecker** (163 lines) → `nostr.event.util.EventTypeChecker` - - Kind range classification - - Type name resolution - - NIP-01 compliance helpers - -**Impact:** -- ✅ Improved testability (each class independently testable) -- ✅ Better reusability (use validators/serializers anywhere) -- ✅ Clear responsibilities (SRP compliance) -- ✅ All 170 tests still passing - -### NIP01 Extraction - -**Before:** 452 lines with multiple responsibilities -**After:** 358 lines + 3 extracted classes (228 additional lines) - -**Extracted Classes:** - -1. **NIP01EventBuilder** (92 lines) → `nostr.api.nip01.NIP01EventBuilder` - - Event creation methods - - Handles defaults and validation - -2. **NIP01TagFactory** (97 lines) → `nostr.api.nip01.NIP01TagFactory` - - Tag creation methods - - Encapsulates tag construction logic - -3. **NIP01MessageFactory** (39 lines) → `nostr.api.nip01.NIP01MessageFactory` - - Message creation methods - - Protocol message formatting - -**Impact:** -- 21% size reduction in NIP01 class -- Clear facade pattern -- Better testability - -### NIP57 Extraction - -**Before:** 449 lines with multiple responsibilities -**After:** 251 lines + 4 extracted classes (332 additional lines) - -**Extracted Classes:** - -1. **NIP57ZapRequestBuilder** (159 lines) -2. **NIP57ZapReceiptBuilder** (70 lines) -3. **NIP57TagFactory** (57 lines) -4. **ZapRequestParameters** (46 lines) - Parameter Object pattern - -**Impact:** -- 44% size reduction in NIP57 class -- Parameter object eliminates 7-parameter method -- Clear builder responsibilities - -### NostrSpringWebSocketClient Extraction - -**Before:** 369 lines with 7 responsibilities -**After:** 232 lines + 5 extracted classes (387 additional lines) - -**Extracted Classes:** - -1. **NostrRelayRegistry** (127 lines) - Relay lifecycle management -2. **NostrEventDispatcher** (68 lines) - Event transmission -3. **NostrRequestDispatcher** (78 lines) - Request handling -4. **NostrSubscriptionManager** (91 lines) - Subscription lifecycle -5. **WebSocketClientHandlerFactory** (23 lines) - Handler creation - -**Impact:** -- 37% size reduction -- Clear separation of concerns -- Each dispatcher/manager has single responsibility +## Design Patterns -### EventJsonMapper Extraction (v0.6.2) +### Builder Pattern +`GenericEvent.builder()` and `EventFilter.builder()` provide fluent, readable construction of complex objects. -**Before:** Static ObjectMapper in Encoder interface (anti-pattern) -**After:** Dedicated utility class +### Value Object Pattern +`PublicKey`, `PrivateKey`, `Signature`, `SubscriptionId` — immutable objects that compare by value, encapsulate validation, and provide Bech32 encoding. -**File:** `nostr.event.json.EventJsonMapper` (76 lines) +### Delegation Pattern +`GenericEvent` delegates to `EventValidator` for validation, `EventSerializer` for canonical serialization, and `Kinds` for kind range classification. -**Impact:** -- ✅ Removed static field from interface -- ✅ Centralized JSON configuration -- ✅ Better discoverability -- ✅ Comprehensive JavaDoc +### Factory Method Pattern +`GenericTag.of(code, params...)` provides a concise way to create tags. --- -## Error Handling Principles +## Error Handling ### Exception Hierarchy -All domain exceptions extend `NostrRuntimeException` (unchecked): - ``` NostrRuntimeException (base) ├── NostrProtocolException (NIP violations) -│ └── NostrException (legacy - protocol errors) ├── NostrCryptoException (signing, encryption) │ ├── SigningException │ └── SchnorrException @@ -599,197 +243,41 @@ NostrRuntimeException (base) │ ├── EventEncodingException │ └── Bech32EncodingException └── NostrNetworkException (relay communication) + └── RelayTimeoutException (timeout waiting for relay) ``` ### Principles -1. **Validate Early** - - Validate in constructors and setters - - Use `@NonNull` annotations - - Throw `IllegalArgumentException` for invalid input - -2. **Fail Fast** - - Don't silently swallow errors - - Provide clear, actionable error messages - - Include context (event ID, kind, field name) - -3. **Use Domain Exceptions** - - Avoid generic `Exception` or `RuntimeException` - - Use specific exceptions from the hierarchy - - Makes error handling more precise - -4. **Examples:** - ```java - // Good - specific exception with context - throw new EventEncodingException( - "Failed to encode event to JSON: " + eventId, cause); - - // Good - validation with clear message - if (kind < 0) { - throw new IllegalArgumentException( - "Invalid `kind`: Must be a non-negative integer."); - } - - // Bad - generic exception - throw new RuntimeException("Error"); // Don't do this - ``` - ---- - -## Extensibility - -### Adding a New NIP Implementation - -**Step 1:** Create event class in `nostr-java-event` -```java -@Event(name = "My Custom Event", nip = 99) -public class CustomEvent extends GenericEvent { - - public CustomEvent(PublicKey pubKey, List tags, String content) { - super(pubKey, 30099, tags, content); // Use appropriate kind - } - - @Override - protected void validateTags() { - super.validateTags(); - // Add NIP-specific validation - requireTag("custom-required-tag"); - } -} -``` - -**Step 2:** Create API facade in `nostr-java-api` -```java -public class NIP99 extends BaseNip { - - private final NIP99EventBuilder eventBuilder; - - public NIP99(Identity sender) { - super(sender); - this.eventBuilder = new NIP99EventBuilder(sender); - } - - public NIP99 createCustomEvent(String content, List tags) { - CustomEvent event = eventBuilder.buildCustomEvent(content, tags); - this.updateEvent(event); - return this; - } -} -``` - -**Step 3:** Add tests -```java -@Test -void testCustomEventCreation() { - Identity identity = new Identity(privateKey); - NIP99 nip99 = new NIP99(identity); - - nip99.createCustomEvent("test content", tags) - .sign(); - - GenericEvent event = nip99.getEvent(); - assertEquals(30099, event.getKind()); - event.validate(); // Should not throw -} -``` - -### Adding a New Tag Type - -**Step 1:** Create tag class in `nostr-java-event` -```java -@Tag(code = "x", nip = 99, name = "Custom Tag") -public class CustomTag extends BaseTag { - - public CustomTag(@NonNull String value) { - super("x"); - this.attributes.add(new Attribute(value, AttributeType.STRING)); - } - - public String getValue() { - return attributes.get(0).value().toString(); - } -} -``` - -**Step 2:** Register serializer/deserializer if needed -```java -// Usually handled automatically via @Tag annotation -// Custom serialization only if non-standard format required -``` - -**Step 3:** Add factory method -```java -// In your NIP's TagFactory -public CustomTag createCustomTag(String value) { - return new CustomTag(value); -} -``` +1. **Validate early** — constructors and builders validate input +2. **Fail fast** — `HexFormat.parseHex()` throws on invalid hex; `RelayTimeoutException` replaces silent empty returns +3. **Use domain exceptions** — specific exceptions with context, not generic `RuntimeException` --- ## Security Notes ### Key Management - -- ✅ **Private keys never leave the process** - - Signing uses in-memory data only - - No network transmission of private keys - - Use secure key storage externally - -- ✅ **Strong RNG** - - Uses `SecureRandom` with BouncyCastle provider - - Never reuse nonces or IVs - - Key generation uses cryptographically secure randomness +- Private keys never leave the process — signing is in-memory only +- Uses `SecureRandom` with BouncyCastle for key generation +- Never reuse nonces or IVs ### Signing - -- ✅ **BIP-340 Schnorr signatures** - - secp256k1 elliptic curve - - Deterministic (RFC 6979) for same message = same signature - - Verifiable by public key +- BIP-340 Schnorr signatures on secp256k1 +- Deterministic (RFC 6979) — same message produces same signature +- Verifiable by public key ### Encryption - -- ✅ **NIP-04 (deprecated)** - AES-256-CBC - - Use NIP-44 for new applications - -- ✅ **NIP-44 (recommended)** - Versioned encryption - - ChaCha20 stream cipher - - Poly1305 MAC for authentication - - Better forward secrecy - -### Best Practices - -1. **Immutability** - - Event fields should be immutable after signing - - Use constructor-based initialization - - Avoid setters on critical fields - -2. **Validation** - - Always validate events before signing - - Verify signatures before trusting content - - Check event ID matches computed hash - -3. **Dependencies** - - Keep crypto dependencies updated - - Use well-audited libraries (BouncyCastle) - - Monitor security advisories +- **NIP-04** (legacy) — AES-256-CBC. Use NIP-44 for new applications. +- **NIP-44** (recommended) — HKDF key derivation, ChaCha20-Poly1305 AEAD. --- ## Summary -The nostr-java architecture provides: - -✅ **Clean separation** of concerns across 9 modules -✅ **Clear dependency direction** following Clean Architecture -✅ **Extensive use of design patterns** for maintainability -✅ **Recent refactoring** eliminated god classes and code smells -✅ **Strong extensibility** points for new NIPs -✅ **Robust error handling** with domain-specific exceptions -✅ **Security-first** approach to cryptography and key management +nostr-java 2.0 provides: -**Grade:** A- (post-refactoring) -**Test Coverage:** 170+ event tests passing -**NIP Support:** 26 NIPs implemented -**Status:** Production-ready +- **Minimal API surface** — one event class, one tag class, ~40 total classes across 4 modules +- **Protocol-aligned design** — kinds are integers, tags are string arrays, no library-imposed type hierarchy +- **Virtual Thread concurrency** — relay I/O and listener dispatch on lightweight threads +- **Reliable connectivity** — typed timeout exceptions, connection state tracking, Spring Retry +- **Strong cryptography** — BIP-340 Schnorr, NIP-44 AEAD, BouncyCastle provider diff --git a/docs/explanation/dependency-alignment.md b/docs/explanation/dependency-alignment.md index 9662c42a9..f7a3f3be8 100644 --- a/docs/explanation/dependency-alignment.md +++ b/docs/explanation/dependency-alignment.md @@ -1,65 +1,61 @@ # Dependency Alignment Plan -This document explains how nostr-java aligns dependency versions across modules and how we will simplify the setup for the 1.0.0 release. +This document explains how nostr-java aligns dependency versions across modules and how the BOM manages consumer dependencies. -Purpose: ensure consistent, reproducible builds across all modules (api, client, event, etc.) and for consumers, with clear steps to remove temporary overrides once the BOM includes 1.0.0. +## Current state (2.0.0) -Current state (pre-1.0) - The aggregator POM imports `nostr-java-bom` to manage third-party versions. -- Temporary overrides pin each reactor module (`nostr-java-*-`) to `${project.version}` so local builds resolve to the in-repo SNAPSHOTs even if the BOM doesn’t yet list matching coordinates. +- Temporary overrides pin each reactor module (`nostr-java-core`, `nostr-java-event`, `nostr-java-identity`, `nostr-java-client`) to `${project.version}` so local builds resolve to the in-repo SNAPSHOTs even if the BOM doesn't yet list matching coordinates. - Relevant configuration lives in `pom.xml` dependencyManagement. -Goals for 1.0 -- Publish 1.0.0 of all modules. -- Bump the imported BOM to the first release that maps to the 1.0.0 module coordinates. -- Remove temporary module overrides so the BOM is the only source of truth. - -Plan and steps -1) Before 1.0.0 - - Keep the module overrides in `dependencyManagement` to guarantee the reactor uses `${project.version}`. - - Keep `nostr-java-bom.version` pointing at the latest stable BOM compatible with current development. - -2) Cut 1.0.0 - - Update `` in the root `pom.xml` to `1.0.0`. - - Build and publish all modules to your repository/Maven Central. - - Release a BOM revision that references the `1.0.0` artifacts (for example `nostr-java-bom 1.x` aligned to `1.0.0`). - -3) After BOM with 1.0.0 is available - - In the root `pom.xml`: - - Bump `` to the new BOM that includes `1.0.0`. - - Remove the module overrides from `` for: - `nostr-java-util`, `nostr-java-crypto`, `nostr-java-base`, `nostr-java-event`, `nostr-java-id`, `nostr-java-encryption`, `nostr-java-client`, `nostr-java-api`, `nostr-java-examples`. - - Remove any unused properties (e.g., `nostr-java.version` if not referenced). - -Verification -- Ensure the build resolves to 1.0.0 coordinates via the BOM: - - `mvn -q -DnoDocker=true clean verify` - - `mvn -q dependency:tree | rg "nostr-java-(api|client|event|base|crypto|util|id|encryption|examples)"` -- Consumers should import the BOM and omit versions on nostr-java dependencies: - ```xml - - - - xyz.tcheeric - nostr-java-bom - 1.0.0+ - pom - import - - - +## Module structure + +4 modules with a strict dependency chain: + +``` +nostr-java-core → nostr-java-event → nostr-java-identity → nostr-java-client +``` + +## BOM alignment + +After each release: +1. Publish all module artifacts. +2. Release a BOM revision that references the published module coordinates. +3. Remove temporary module overrides from the aggregator POM so the BOM is the only source of truth. + +## Consumer usage + +Consumers should import the BOM and omit versions on nostr-java dependencies: + +```xml + xyz.tcheeric - nostr-java-api + nostr-java-bom + 2.0.0+ + pom + import - ``` + + + + xyz.tcheeric + nostr-java-client + + +``` + +## Verification + +Ensure the build resolves to correct coordinates via the BOM: -Rollback strategy -- If a BOM update lags a module release, temporarily restore individual module overrides under `` to force-align versions in the reactor, then remove again once the BOM is refreshed. +```bash +mvn -q -DnoDocker=true clean verify +mvn -q dependency:tree | rg "nostr-java-(core|event|identity|client)" +``` -Outcome -- A single source of truth (the BOM) for dependency versions. -- No per-module overrides in the aggregator once 1.0.0 is published and the BOM is updated. +## Rollback strategy +If a BOM update lags a module release, temporarily restore individual module overrides under `` to force-align versions in the reactor, then remove again once the BOM is refreshed. diff --git a/docs/explanation/extending-events.md b/docs/explanation/extending-events.md index 97c8c35cf..913603097 100644 --- a/docs/explanation/extending-events.md +++ b/docs/explanation/extending-events.md @@ -1,596 +1,288 @@ -# Extending Events +# Working with Events and Tags -Navigation: [Docs index](../README.md) · [API how‑to](../howto/use-nostr-java-api.md) · [Custom events](../howto/custom-events.md) · [API reference](../reference/nostr-java-api.md) +Navigation: [Docs index](../README.md) · [API how-to](../howto/use-nostr-java-api.md) · [Custom events](../howto/custom-events.md) · [API reference](../reference/nostr-java-api.md) -This guide explains how to properly extend nostr-java with new event types, custom tags, and factories. The project uses factories and registries to make it easy to introduce new event types while keeping core classes stable. - -## Table of Contents - -- [Architecture Overview](#architecture-overview) -- [Adding a New Event Type](#adding-a-new-event-type) -- [Complete Example: Poll Event](#complete-example-poll-event) -- [Adding Custom Tags](#adding-custom-tags) -- [Creating Event Factories](#creating-event-factories) -- [Testing & Contribution](#testing--contribution) +This guide explains how to create Nostr events and tags using nostr-java 2.0. The library uses a single event class (`GenericEvent`) and a single tag class (`GenericTag`) for all event kinds — no subclasses, no factories, no registries. --- -## Architecture Overview - -### Event Factories - -**Event factories** (e.g. [`EventFactory`](../../nostr-java-api/src/main/java/nostr/api/factory/EventFactory.java) and its implementations) centralize event creation so that callers don't have to handle boilerplate like setting the sender, tags, or content. - -Example: [`GenericEventFactory`](../../nostr-java-api/src/main/java/nostr/api/factory/impl/GenericEventFactory.java) +## Core Concepts -### Tag Registry +### One event class: `GenericEvent` -[`TagRegistry`](../../nostr-java-event/src/main/java/nostr/event/tag/TagRegistry.java) maps tag codes to concrete implementations, allowing additional tag types to be resolved at runtime without modifying `BaseTag`. +Every Nostr event is a `GenericEvent`, differentiated by its `int kind`: -**How it works:** ```java -// Registration (done once, typically in static initializer) -TagRegistry.register("expiration", ExpirationTag::updateFields); - -// Runtime resolution -Function factory = TagRegistry.get("expiration"); -BaseTag tag = factory.apply(genericTag); +// Text note (kind 1) +GenericEvent note = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Hello Nostr!") + .build(); + +// Metadata (kind 0) +GenericEvent metadata = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.SET_METADATA) + .content("{\"name\":\"Alice\",\"about\":\"Nostr user\"}") + .build(); + +// Any custom kind +GenericEvent custom = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(30078) // any integer + .content("custom content") + .build(); ``` -### Event Hierarchy +### One tag class: `GenericTag` -``` -BaseEvent (abstract) - └── GenericEvent (concrete) - ├── ContactListEvent - ├── DeletionEvent - ├── ZapRequestEvent - └── Your custom events... -``` +Tags are a code string and a list of string parameters — exactly what the Nostr protocol specifies: ---- +```java +// Event reference: ["e", "eventId", "relay", "marker"] +GenericTag.of("e", "abc123", "wss://relay.example.com", "reply") -## Adding a New Event Type +// Public key reference: ["p", "pubkey"] +GenericTag.of("p", "deadbeef1234...") -### Step-by-Step Process +// Hashtag: ["t", "nostr"] +GenericTag.of("t", "nostr") -1. **Define the kind** – Add a constant to [`Kind`](../../nostr-java-base/src/main/java/nostr/base/Kind.java) or use a custom value per [NIP-16](https://github.com/nostr-protocol/nips/blob/master/16.md) -2. **Implement the event** – Create a class under `nostr.event.impl` that extends `GenericEvent` -3. **Add custom tags** (if needed) – Create tag classes and register them in `TagRegistry` -4. **Provide a factory** (optional) – Implement a factory extending `EventFactory` for convenience -5. **Write tests** – Add unit and integration tests -6. **Document** – Update documentation and examples +// Any custom tag +GenericTag.of("custom", "value1", "value2") +``` -### Choosing a Kind Number +Access tag data positionally: +```java +GenericTag tag = GenericTag.of("e", "abc123", "wss://relay.example.com", "reply"); +tag.getCode() // "e" +tag.getParams() // ["abc123", "wss://relay.example.com", "reply"] +tag.getParams().get(0) // "abc123" +tag.toArray() // ["e", "abc123", "wss://relay.example.com", "reply"] +``` -Per [NIP-16](https://github.com/nostr-protocol/nips/blob/master/16.md), kind numbers are grouped: +### Kind constants: `Kinds` -| Range | Type | Description | -|-------|------|-------------| -| 0-9999 | Regular | Can be deleted, standard events | -| 10000-19999 | Replaceable | Newer event replaces older (by pubkey) | -| 20000-29999 | Ephemeral | Not stored by relays | -| 30000-39999 | Parameterized Replaceable | Replaceable with `d` tag parameter | +Common kind values are available as static `int` constants. Any integer is a valid kind — these are convenience constants for discoverability: -**Example:** ```java -// In nostr.base.Kind enum -public static final Kind POLL = new Kind(30078); // Parameterized replaceable +Kinds.SET_METADATA // 0 +Kinds.TEXT_NOTE // 1 +Kinds.CONTACT_LIST // 3 +Kinds.ENCRYPTED_DIRECT_MESSAGE // 4 +Kinds.DELETION // 5 +Kinds.REPOST // 6 +Kinds.REACTION // 7 +Kinds.ZAP_REQUEST // 9734 +Kinds.ZAP_RECEIPT // 9735 + +// Range checks +Kinds.isReplaceable(10002) // true (10000-19999) +Kinds.isEphemeral(20001) // true (20000-29999) +Kinds.isAddressable(30023) // true (30000-39999) +Kinds.isValid(65536) // false (must be 0-65535) ``` --- -## Complete Example: Poll Event - -Let's implement a complete poll event (NIP-69 style) with custom tags. +## Common Event Patterns -### 1. Define the Kind +### Text note with tags ```java -// Add to nostr-java-base/src/main/java/nostr/base/Kind.java -public static final Kind POLL = new Kind(30078); +GenericEvent note = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Check out #nostr! cc @someone") + .tags(List.of( + GenericTag.of("t", "nostr"), + GenericTag.of("p", recipientPubKeyHex) + )) + .build(); + +identity.sign(note); ``` -### 2. Create Custom Tags - -**PollOptionTag.java** (in `nostr-java-event/src/main/java/nostr/event/tag/`): +### Reply to an event ```java -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@AllArgsConstructor -@NoArgsConstructor -@Tag(code = "poll_option", name = "Poll Option") -public class PollOptionTag extends BaseTag { - - @Key - @JsonProperty - private String optionId; - - @JsonProperty - private String optionText; - - public PollOptionTag(String optionId, String optionText) { - this.optionId = optionId; - this.optionText = optionText; - this.code = "poll_option"; - } - - /** - * Factory method for TagRegistry - */ - public static PollOptionTag updateFields(@NonNull GenericTag tag) { - if (!"poll_option".equals(tag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for PollOptionTag"); - } - String optionId = tag.getAttributes().get(0).value().toString(); - String optionText = tag.getAttributes().get(1).value().toString(); - return new PollOptionTag(optionId, optionText); - } -} +GenericEvent reply = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Great post!") + .tags(List.of( + GenericTag.of("e", originalEventId, "wss://relay.example.com", "reply"), + GenericTag.of("p", originalAuthorPubKey) + )) + .build(); + +identity.sign(reply); ``` -**Register the tag** in `TagRegistry`: +### Reaction ```java -// In TagRegistry static initializer -static { - // ... existing registrations ... - register("poll_option", PollOptionTag::updateFields); -} +GenericEvent reaction = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.REACTION) + .content("+") // or any emoji + .tags(List.of( + GenericTag.of("e", targetEventId), + GenericTag.of("p", targetAuthorPubKey) + )) + .build(); + +identity.sign(reaction); ``` -### 3. Create the Event Class - -**PollEvent.java** (in `nostr-java-event/src/main/java/nostr/event/impl/`): +### Replaceable event ```java -package nostr.event.impl; - -import java.util.List; -import java.util.stream.Collectors; -import lombok.*; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.tag.PollOptionTag; - -@Data -@EqualsAndHashCode(callSuper = true) -@Event(name = "Poll Event", nip = 69) -@NoArgsConstructor -public class PollEvent extends GenericEvent { - - public PollEvent(@NonNull PublicKey pubKey, @NonNull String question, - @NonNull List options) { - super(pubKey, Kind.POLL, - options.stream().map(o -> (BaseTag) o).collect(Collectors.toList()), - question); - } - - public PollEvent(@NonNull PublicKey pubKey, @NonNull String question, - @NonNull List options, @NonNull List additionalTags) { - super(pubKey, Kind.POLL, combineTags(options, additionalTags), question); - } - - private static List combineTags(List options, - List additional) { - List allTags = options.stream() - .map(o -> (BaseTag) o) - .collect(Collectors.toList()); - allTags.addAll(additional); - return allTags; - } - - /** - * Get poll options from tags - */ - public List getOptions() { - return getTags().stream() - .filter(tag -> tag instanceof PollOptionTag) - .map(tag -> (PollOptionTag) tag) - .collect(Collectors.toList()); - } - - /** - * Get the poll question - */ - public String getQuestion() { - return getContent(); - } - - @Override - protected void validateTags() { - super.validateTags(); - - long optionCount = getTags().stream() - .filter(t -> "poll_option".equals(t.getCode())) - .count(); - - if (optionCount < 2) { - throw new AssertionError("Poll must have at least 2 options"); - } - if (optionCount > 10) { - throw new AssertionError("Poll cannot have more than 10 options"); - } - } - - @Override - protected void validateKind() { - if (getKind() != Kind.POLL.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.POLL.getValue()); - } - } -} +// Contact list (kind 3) — only the latest per pubkey is kept +GenericEvent contactList = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.CONTACT_LIST) + .content("") + .tags(List.of( + GenericTag.of("p", friend1PubKey, "wss://relay1.example.com"), + GenericTag.of("p", friend2PubKey, "wss://relay2.example.com") + )) + .build(); + +identity.sign(contactList); ``` -### 4. Create a Factory (Optional but Recommended) - -**PollEventFactory.java** (in `nostr-java-api/src/main/java/nostr/api/factory/impl/`): +### Ephemeral event ```java -package nostr.api.factory.impl; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import lombok.*; -import nostr.api.factory.EventFactory; -import nostr.event.BaseTag; -import nostr.event.impl.PollEvent; -import nostr.event.tag.PollOptionTag; -import nostr.id.Identity; - -@EqualsAndHashCode(callSuper = true) -@Data -public class PollEventFactory extends EventFactory { - - private String question; - private List options; - - public PollEventFactory(Identity sender, @NonNull String question, - @NonNull List options) { - super(sender); - this.question = question; - this.options = options; - } - - @Override - public PollEvent create() { - List pollOptions = new ArrayList<>(); - for (int i = 0; i < options.size(); i++) { - pollOptions.add(new PollOptionTag(String.valueOf(i), options.get(i))); - } - - return new PollEvent( - getIdentity().getPublicKey(), - question, - pollOptions, - getTags() // Additional tags from factory - ); - } - - /** - * Convenience method to add expiration - */ - public PollEventFactory withExpiration(int timestamp) { - addTag(new nostr.event.tag.ExpirationTag(timestamp)); - return this; - } -} +// Typing indicator (kind 20001) — relays forward but don't store +GenericEvent typing = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(20001) + .content("{\"typing\":true}") + .build(); + +identity.sign(typing); ``` -### 5. Usage Example +### Addressable event with `d` tag ```java -import nostr.id.Identity; -import nostr.api.factory.impl.PollEventFactory; -import nostr.event.impl.PollEvent; -import nostr.event.message.EventMessage; -import nostr.client.springwebsocket.StandardWebSocketClient; -import java.util.List; - -public class PollExample { - public static void main(String[] args) throws Exception { - Identity identity = Identity.generateRandomIdentity(); - - // Method 1: Using the factory - PollEventFactory factory = new PollEventFactory( - identity, - "What's your favorite programming language?", - List.of("Java", "Python", "Rust", "Go") - ); - - // Optionally add expiration (1 week from now) - factory.withExpiration((int) (System.currentTimeMillis() / 1000) + 604800); - - PollEvent poll = factory.create(); - identity.sign(poll); - - // Send to relay - try (StandardWebSocketClient client = - new StandardWebSocketClient("wss://relay.398ja.xyz")) { - client.send(new EventMessage(poll)); - System.out.println("Poll created: " + poll.getId()); - } - - // Method 2: Direct construction - List options = List.of( - new PollOptionTag("0", "Java"), - new PollOptionTag("1", "Python"), - new PollOptionTag("2", "Rust") - ); - - PollEvent directPoll = new PollEvent( - identity.getPublicKey(), - "Best language for backend?", - options - ); - identity.sign(directPoll); - - // Access poll data - System.out.println("Question: " + directPoll.getQuestion()); - directPoll.getOptions().forEach(opt -> - System.out.println(" - " + opt.getOptionText()) - ); - } -} +// Long-form content (kind 30023) — replaceable by pubkey + d-tag +GenericEvent article = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(30023) + .content("# My Article\n\nFull content here...") + .tags(List.of( + GenericTag.of("d", "my-article-slug"), + GenericTag.of("title", "My Article"), + GenericTag.of("t", "blog") + )) + .build(); + +identity.sign(article); ``` --- -## Adding Custom Tags - -### Tag Implementation Pattern +## Encryption -All custom tags should: - -1. **Extend `BaseTag`** -2. **Use annotations**: `@Tag(code = "your_code", name = "Tag Name", nip = X)` -3. **Implement `updateFields` method** for TagRegistry -4. **Mark key fields** with `@Key` annotation - -**Example: Custom LocationTag** +### NIP-04 (legacy) ```java -package nostr.event.tag; - -import lombok.*; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@AllArgsConstructor -@NoArgsConstructor -@Tag(code = "location", name = "Location Tag") -public class LocationTag extends BaseTag { - - @Key - private String latitude; - - @Key - private String longitude; - - private String name; // Optional field - - public static LocationTag updateFields(@NonNull GenericTag tag) { - if (!"location".equals(tag.getCode())) { - throw new IllegalArgumentException("Invalid tag code"); - } - - String lat = tag.getAttributes().get(0).value().toString(); - String lon = tag.getAttributes().get(1).value().toString(); - String name = tag.getAttributes().size() > 2 - ? tag.getAttributes().get(2).value().toString() - : null; - - LocationTag locationTag = new LocationTag(); - locationTag.setLatitude(lat); - locationTag.setLongitude(lon); - locationTag.setName(name); - return locationTag; - } -} +MessageCipher04 cipher = new MessageCipher04( + senderIdentity.getPrivateKey(), + recipientPublicKey +); + +String encrypted = cipher.encrypt("Secret message"); +String decrypted = cipher.decrypt(encrypted); ``` -**Register in TagRegistry:** +### NIP-44 (recommended) ```java -static { - // ... existing registrations ... - register("location", LocationTag::updateFields); -} +MessageCipher44 cipher = new MessageCipher44( + senderIdentity.getPrivateKey(), + recipientPublicKey +); + +String encrypted = cipher.encrypt("Secret message"); +String decrypted = cipher.decrypt(encrypted); ``` --- -## Creating Event Factories +## Filters -Event factories provide a clean API for creating events with sensible defaults. - -### Factory Pattern +Query relays for specific events using `EventFilter`: ```java -public class MyEventFactory extends EventFactory { - - private String customField; - - public MyEventFactory(Identity sender, String customField) { - super(sender); - this.customField = customField; - } - - @Override - public MyEvent create() { - return new MyEvent( - getIdentity().getPublicKey(), - customField, - new ArrayList<>(getTags()), - getContent() - ); - } - - // Fluent methods for convenience - public MyEventFactory withSomeOption(String value) { - addTag(new SomeTag(value)); - return this; - } -} +EventFilter filter = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE, Kinds.REACTION)) + .authors(List.of(pubKeyHex)) + .since(timestampSeconds) + .limit(50) + .build(); + +Filters filters = new Filters(filter); ``` -### When to Create a Factory - -Create a factory when: -- Event construction has multiple steps -- You want to provide default tags or content -- The API should be fluent and user-friendly -- Events are created frequently in client code - -Don't create a factory when: -- Event is very simple (just use constructor) -- Event is only used internally -- Construction is straightforward - ---- - -## Testing & Contribution - -### Unit Tests - -Test your event implementation: +Tag-based filtering: ```java -@Test -void testPollEventCreation() { - PublicKey pubKey = new PublicKey(/* ... */); - - List options = List.of( - new PollOptionTag("0", "Option A"), - new PollOptionTag("1", "Option B") - ); - - PollEvent poll = new PollEvent(pubKey, "Question?", options); - - assertEquals("Question?", poll.getQuestion()); - assertEquals(2, poll.getOptions().size()); - assertEquals(Kind.POLL.getValue(), poll.getKind()); -} - -@Test -void testPollEventValidation() { - PublicKey pubKey = new PublicKey(/* ... */); - - // Should fail with < 2 options - assertThrows(AssertionError.class, () -> { - new PollEvent(pubKey, "Question?", List.of( - new PollOptionTag("0", "Only one option") - )); - }); -} +EventFilter filter = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE)) + .addTagFilter("t", List.of("nostr", "bitcoin")) + .addTagFilter("p", List.of(specificPubKey)) + .build(); ``` -### Serialization Tests +--- -Test JSON encoding/decoding: +## Testing ```java @Test -void testPollEventSerialization() throws Exception { - PollEvent original = createTestPoll(); +void testEventCreation() { + Identity identity = Identity.generateRandomIdentity(); - // Serialize - String json = new EventMessage(original).encode(); + GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Test content") + .tags(List.of(GenericTag.of("t", "test"))) + .build(); - // Deserialize - BaseMessage decoded = BaseMessage.read(json); - assertTrue(decoded instanceof EventMessage); + identity.sign(event); - PollEvent deserialized = (PollEvent) ((EventMessage) decoded).getEvent(); - assertEquals(original.getQuestion(), deserialized.getQuestion()); - assertEquals(original.getOptions().size(), deserialized.getOptions().size()); + assertNotNull(event.getId()); + assertNotNull(event.getSignature()); + assertEquals(Kinds.TEXT_NOTE, event.getKind()); + assertEquals("t", event.getTags().get(0).getCode()); + assertEquals("test", event.getTags().get(0).getParams().get(0)); } -``` - -### Integration Tests - -Test with real relay (using Testcontainers): -```java @Test -void testSendPollToRelay() throws Exception { - // Use testcontainer relay or local relay - String relayUrl = "ws://localhost:5555"; +void testSerialization() throws Exception { + GenericEvent event = createAndSignEvent(); - Identity identity = Identity.generateRandomIdentity(); - PollEvent poll = createTestPoll(identity.getPublicKey()); - identity.sign(poll); + String json = new EventMessage(event).encode(); + BaseMessage decoded = BaseMessage.read(json); - try (StandardWebSocketClient client = new StandardWebSocketClient(relayUrl)) { - List responses = client.send(new EventMessage(poll)); - assertFalse(responses.isEmpty()); - } + assertTrue(decoded instanceof EventMessage); + GenericEvent deserialized = ((EventMessage) decoded).getEvent(); + assertEquals(event.getId(), deserialized.getId()); } ``` -### Contribution Checklist - -Before submitting a PR: - -- [ ] Run `mvn -q verify` – all tests pass -- [ ] Event complies with relevant NIP -- [ ] Added unit tests (>80% coverage) -- [ ] Added integration tests if applicable -- [ ] Updated documentation -- [ ] Added example usage -- [ ] Removed unused imports -- [ ] Followed code style (use formatter) -- [ ] Updated CHANGELOG or release notes -- [ ] Tested with real relay - -### Contributing Guidelines - -1. **Run verification**: - ```bash - mvn -q verify - ``` - -2. **Ensure NIP compliance**: Events should follow Nostr specifications - -3. **Include comprehensive tests**: Cover edge cases and error conditions - -4. **Document your changes**: Add examples and update relevant docs - -5. **Follow PR template**: Complete all sections in `.github/pull_request_template.md` - -For complete contribution guidelines, see [CONTRIBUTING.md](../../CONTRIBUTING.md). - --- -## Real-World Examples - -Study these existing implementations: - -- **Simple event**: [`ContactListEvent`](../../nostr-java-event/src/main/java/nostr/event/impl/ContactListEvent.java) – basic validation -- **Complex event**: [`CalendarRsvpEvent`](../../nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpEvent.java) – custom content type -- **Tag implementation**: [`ExpirationTag`](../../nostr-java-event/src/main/java/nostr/event/tag/ExpirationTag.java) – tag with updateFields -- **Factory**: [`GenericEventFactory`](../../nostr-java-api/src/main/java/nostr/api/factory/impl/GenericEventFactory.java) – flexible factory pattern - ## See Also -- [Custom Events How-To](../howto/custom-events.md) – Basic custom event creation -- [API Reference](../reference/nostr-java-api.md) – API documentation -- [NIP-16](https://github.com/nostr-protocol/nips/blob/master/16.md) – Event kind ranges -- [Contributing Guide](../../CONTRIBUTING.md) – Full contribution guidelines +- [Custom events how-to](../howto/custom-events.md) — Sending custom event kinds +- [Streaming subscriptions](../howto/streaming-subscriptions.md) — Long-lived relay subscriptions +- [API reference](../reference/nostr-java-api.md) — Full class and method reference +- [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) — Basic protocol +- [NIP-16](https://github.com/nostr-protocol/nips/blob/master/16.md) — Event kind ranges diff --git a/docs/explanation/roadmap-1.0.md b/docs/explanation/roadmap-1.0.md index 92e31cb64..51298b5d7 100644 --- a/docs/explanation/roadmap-1.0.md +++ b/docs/explanation/roadmap-1.0.md @@ -1,6 +1,8 @@ -# 1.0 Roadmap +# 1.0 Roadmap (Historical) -This explanation outlines the outstanding work required to promote `nostr-java` from the current 0.6.x snapshots to a stable 1.0.0 release. Items are grouped by theme so maintainers can prioritize stabilization, hardening, and release-readiness tasks. +> **Note:** This roadmap was completed with the 1.0.0 release. The library has since been simplified in 2.0.0 — see [SIMPLIFICATION_PROPOSAL.md](../developer/SIMPLIFICATION_PROPOSAL.md) and [CHANGELOG.md](../../CHANGELOG.md). + +This explanation outlines the outstanding work that was required to promote `nostr-java` from the 0.6.x snapshots to a stable 1.0.0 release. Items are grouped by theme so maintainers can prioritize stabilization, hardening, and release-readiness tasks. ## Release-readiness snapshot diff --git a/docs/howto/api-examples.md b/docs/howto/api-examples.md index a4d3a22c9..77607645f 100644 --- a/docs/howto/api-examples.md +++ b/docs/howto/api-examples.md @@ -1,720 +1,314 @@ # API Examples Guide -Navigation: [Docs index](../README.md) · [Getting started](../GETTING_STARTED.md) · [API how‑to](use-nostr-java-api.md) · [API reference](../reference/nostr-java-api.md) +Navigation: [Docs index](../README.md) · [Getting started](../GETTING_STARTED.md) · [API how-to](use-nostr-java-api.md) · [API reference](../reference/nostr-java-api.md) -This guide walks through the comprehensive examples in [`NostrApiExamples.java`](../../nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java), demonstrating 13+ common use cases for the nostr-java API. +This guide demonstrates common use cases for the nostr-java library using `GenericEvent`, `GenericTag`, and `NostrRelayClient`. ## Table of Contents 1. [Setup](#setup) -2. [Metadata Events (NIP-01)](#metadata-events-nip-01) -3. [Text Notes (NIP-01)](#text-notes-nip-01) -4. [Encrypted Direct Messages (NIP-04)](#encrypted-direct-messages-nip-04) +2. [Text Notes (NIP-01)](#text-notes-nip-01) +3. [Metadata Events (NIP-01)](#metadata-events-nip-01) +4. [Encrypted Direct Messages (NIP-04/44)](#encrypted-direct-messages-nip-0444) 5. [Event Deletion (NIP-09)](#event-deletion-nip-09) -6. [Ephemeral Events](#ephemeral-events) -7. [Reactions (NIP-25)](#reactions-nip-25) -8. [Replaceable Events](#replaceable-events) -9. [Internet Identifiers (NIP-05)](#internet-identifiers-nip-05) -10. [Filters and Subscriptions](#filters-and-subscriptions) -11. [Public Channels (NIP-28)](#public-channels-nip-28) -12. [Running the Examples](#running-the-examples) +6. [Reactions (NIP-25)](#reactions-nip-25) +7. [Replaceable Events](#replaceable-events) +8. [Ephemeral Events](#ephemeral-events) +9. [Filters and Subscriptions](#filters-and-subscriptions) +10. [Async Operations](#async-operations) --- ## Setup -All examples use two identities (sender and recipient) and a local relay: +All examples use two identities (sender and recipient) and a relay: ```java -private static final Identity RECIPIENT = Identity.generateRandomIdentity(); +import nostr.base.Kinds; +import nostr.client.springwebsocket.NostrRelayClient; +import nostr.event.impl.GenericEvent; +import nostr.event.message.EventMessage; +import nostr.event.tag.GenericTag; +import nostr.id.Identity; + private static final Identity SENDER = Identity.generateRandomIdentity(); -private static final Map RELAYS = Map.of("local", "localhost:5555"); +private static final Identity RECIPIENT = Identity.generateRandomIdentity(); +private static final String RELAY = "wss://relay.398ja.xyz"; ``` -**For testing**, you can: -- Use a local relay (e.g., [nostr-rs-relay](https://github.com/scsibug/nostr-rs-relay)) -- Replace with public relays: `Map.of("398ja", "wss://relay.398ja.xyz")` - --- -## Metadata Events (NIP-01) +## Text Notes (NIP-01) -**Purpose**: Publish user profile information (name, picture, about, NIP-05 identifier) +**Purpose**: Post public text messages. -**Example**: ```java -private static GenericEvent metaDataEvent() { - // Create a user profile - UserProfile profile = new UserProfile( - SENDER.getPublicKey(), - "Nostr Guy", // name - "guy@nostr-java.io", // nip05 identifier - "It's me!", // about/bio - null // lud16 (Lightning address) - ); - - // Set profile picture - profile.setPicture( - new URI("https://example.com/avatar.jpg").toURL() - ); - - // Create and send metadata event - var nip01 = new NIP01(SENDER); - nip01.createMetadataEvent(profile) - .sign() - .send(RELAYS); - - return nip01.getEvent(); +GenericEvent note = GenericEvent.builder() + .pubKey(SENDER.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Hello world, I'm here on nostr-java!") + .tags(List.of( + GenericTag.of("p", RECIPIENT.getPublicKey().toString()) + )) + .build(); + +SENDER.sign(note); + +try (NostrRelayClient client = new NostrRelayClient(RELAY)) { + client.send(new EventMessage(note)); } ``` -**What it does**: -- Creates a kind `0` (metadata) event -- Encodes profile data as JSON in event content -- Signs and publishes to configured relays - -**Use case**: User profile updates, onboarding new users +**With hashtags:** +```java +GenericEvent tagged = GenericEvent.builder() + .pubKey(SENDER.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Check out #nostr! cc @someone") + .tags(List.of( + GenericTag.of("t", "nostr"), + GenericTag.of("p", somePublicKeyHex) + )) + .build(); +``` --- -## Text Notes (NIP-01) +## Metadata Events (NIP-01) -**Purpose**: Post public text messages (tweets/notes) +**Purpose**: Publish user profile information. -**Example**: ```java -private static GenericEvent sendTextNoteEvent() { - // Create tags (e.g., mention another user) - List tags = List.of( - new PubKeyTag(RECIPIENT.getPublicKey()) - ); +String profileJson = """ + { + "name": "Nostr Guy", + "about": "It's me!", + "picture": "https://example.com/avatar.jpg", + "nip05": "guy@nostr-java.io" + } + """; - // Create and send text note - var nip01 = new NIP01(SENDER); - nip01.createTextNoteEvent(tags, "Hello world, I'm here on nostr-java API!") - .sign() - .send(RELAYS); +GenericEvent metadata = GenericEvent.builder() + .pubKey(SENDER.getPublicKey()) + .kind(Kinds.SET_METADATA) + .content(profileJson) + .build(); - return nip01.getEvent(); -} +SENDER.sign(metadata); ``` -**What it does**: -- Creates a kind `1` (text note) event -- Adds tags (mentions, hashtags, etc.) -- Signs and broadcasts to relays +--- -**Use case**: Social media posts, announcements, public messages +## Encrypted Direct Messages (NIP-04/44) -**Variations**: +**NIP-04 (legacy):** ```java -// Simple note without tags -nip01.createTextNoteEvent("Hello Nostr!") - .sign() - .send(RELAYS); - -// With multiple tags -List tags = List.of( - new PubKeyTag(user1PublicKey), - new HashtagTag("nostr"), - new ExpirationTag((int) (System.currentTimeMillis() / 1000) + 3600) // 1 hour -); -nip01.createTextNoteEvent(tags, "Check out #nostr!") - .sign() - .send(RELAYS); -``` +import nostr.encryption.MessageCipher04; ---- +MessageCipher04 cipher = new MessageCipher04( + SENDER.getPrivateKey(), + RECIPIENT.getPublicKey() +); -## Encrypted Direct Messages (NIP-04) +String encrypted = cipher.encrypt("Hello Nakamoto!"); -**Purpose**: Send private encrypted messages between users +GenericEvent dm = GenericEvent.builder() + .pubKey(SENDER.getPublicKey()) + .kind(Kinds.ENCRYPTED_DIRECT_MESSAGE) + .content(encrypted) + .tags(List.of( + GenericTag.of("p", RECIPIENT.getPublicKey().toString()) + )) + .build(); -**Example**: -```java -private static void sendEncryptedDirectMessage() { - // Create NIP-04 instance with sender and recipient - var nip04 = new NIP04(SENDER, RECIPIENT.getPublicKey()); - - // Create and send encrypted DM - nip04.createDirectMessageEvent("Hello Nakamoto!") - .sign() - .send(RELAYS); -} +SENDER.sign(dm); ``` -**Decryption**: +**NIP-44 (recommended):** ```java -// Recipient decrypts the message -NIP04Event dmEvent = /* received event */; -String plaintext = NIP04.decrypt(RECIPIENT, dmEvent); -System.out.println("Decrypted: " + plaintext); -``` +import nostr.encryption.MessageCipher44; -**What it does**: -- Encrypts message using NIP-04 encryption (sender private key + recipient public key) -- Creates a kind `4` event with encrypted content -- Only sender and recipient can decrypt - -**Security note**: NIP-04 is considered legacy. For new applications, consider NIP-44 encryption: -```java MessageCipher44 cipher = new MessageCipher44( SENDER.getPrivateKey(), RECIPIENT.getPublicKey() ); + String encrypted = cipher.encrypt("Secret message"); +String decrypted = cipher.decrypt(encrypted); ``` --- ## Event Deletion (NIP-09) -**Purpose**: Request deletion of previously published events +**Purpose**: Request deletion of previously published events. -**Example**: ```java -private static void deletionEvent() { - // Create an event to delete - var event = sendTextNoteEvent(); - - // Create deletion request - var nip09 = new NIP09(SENDER); - nip09.createDeletionEvent(event) - .sign() - .send(); -} -``` - -**What it does**: -- Creates a kind `5` (deletion) event -- References the event to delete via `e` tag -- Relays may or may not honor deletion requests - -**Important**: -- Only the event author can request deletion -- Relays decide whether to honor the request -- No guarantee the event will be deleted from all relays +GenericEvent deletion = GenericEvent.builder() + .pubKey(SENDER.getPublicKey()) + .kind(Kinds.DELETION) + .content("Deleting old posts") + .tags(List.of( + GenericTag.of("e", eventIdToDelete1), + GenericTag.of("e", eventIdToDelete2) + )) + .build(); -**Deleting multiple events**: -```java -List eventsToDelete = List.of(event1, event2, event3); -nip09.createDeletionEvent(eventsToDelete) - .sign() - .send(); -``` - ---- - -## Ephemeral Events - -**Purpose**: Create events that relays should not persist - -**Example**: -```java -private static void ephemeralEvent() { - var nip01 = new NIP01(SENDER); - nip01.createEphemeralEvent( - Kind.EPHEMEREAL_EVENT.getValue(), // kind: 20000-29999 - "An ephemeral event" - ) - .sign() - .send(RELAYS); -} -``` - -**What it does**: -- Creates an ephemeral event (kind 20000-29999 per NIP-16) -- Relays forward but don't store these events -- Useful for real-time, transient data - -**Use cases**: -- Typing indicators -- Online presence status -- Temporary notifications -- Real-time collaborative editing - -```java -// Typing indicator -nip01.createEphemeralEvent(20001, "{\"typing\":true}") - .sign() - .send(RELAYS); - -// Online status -nip01.createEphemeralEvent(20002, "{\"status\":\"online\"}") - .sign() - .send(RELAYS); +SENDER.sign(deletion); ``` --- ## Reactions (NIP-25) -**Purpose**: React to events with likes, emoji, or custom reactions +**Purpose**: React to events with likes or emoji. -**Example**: ```java -private static void reactionEvent() { - // 1. Create a post to react to - List tags = List.of( - NIP30.createEmojiTag( - "soapbox", - "https://gleasonator.com/emoji/Gleasonator/soapbox.png" - ) - ); - - var nip01 = new NIP01(SENDER); - var event = nip01.createTextNoteEvent( - tags, - "Hello Astral, Please like me! :soapbox:" - ); - event.signAndSend(RELAYS); - - // 2. Like reaction - var nip25 = new NIP25(RECIPIENT); - nip25.createReactionEvent( - event.getEvent(), - Reaction.LIKE, // "+" - new Relay("localhost:5555") - ) - .signAndSend(RELAYS); - - // 3. Emoji reaction - nip25.createReactionEvent( - event.getEvent(), - "💩", // Any emoji - new Relay("localhost:5555") - ) - .signAndSend(); - - // 4. Custom emoji reaction (using NIP-30) - BaseTag eventTag = NIP01.createEventTag(event.getEvent().getId()); - nip25.createReactionEvent( - eventTag, - NIP30.createEmojiTag( - "ablobcatrainbow", - "https://gleasonator.com/emoji/blobcat/ablobcatrainbow.png" - ) - ) - .signAndSend(); -} +// Like reaction +GenericEvent like = GenericEvent.builder() + .pubKey(RECIPIENT.getPublicKey()) + .kind(Kinds.REACTION) + .content("+") + .tags(List.of( + GenericTag.of("e", targetEventId), + GenericTag.of("p", targetAuthorPubKey) + )) + .build(); + +RECIPIENT.sign(like); + +// Emoji reaction +GenericEvent emoji = GenericEvent.builder() + .pubKey(RECIPIENT.getPublicKey()) + .kind(Kinds.REACTION) + .content("\uD83D\uDD25") // fire emoji + .tags(List.of( + GenericTag.of("e", targetEventId), + GenericTag.of("p", targetAuthorPubKey) + )) + .build(); + +RECIPIENT.sign(emoji); ``` -**Reaction types**: -- `Reaction.LIKE` → `"+"` -- `Reaction.DISLIKE` → `"-"` -- Any Unicode emoji: `"❤️"`, `"🔥"`, `"👍"` -- Custom emoji via NIP-30 - --- ## Replaceable Events -**Purpose**: Create events that replace previous events of the same kind +**Purpose**: Events that replace previous events of the same kind per pubkey. -**Example**: ```java -private static void replaceableEvent() { - var nip01 = new NIP01(SENDER); - - // Create initial event - var event = nip01.createTextNoteEvent("Hello Astral, Please replace me!"); - event.signAndSend(RELAYS); - - // Create replaceable event (kind 10000-19999) - nip01.createReplaceableEvent( - List.of(NIP01.createEventTag(event.getEvent().getId())), - Kind.REPLACEABLE_EVENT.getValue(), // kind: 10000-19999 - "New content" - ) - .signAndSend(); -} -``` - -**What it does**: -- Replaceable events (kind 10000-19999) replace older events by the same author -- Relays keep only the most recent event of each kind per pubkey -- Useful for settings, profiles, status updates +// Contact list (kind 3) — only the latest is kept +GenericEvent contactList = GenericEvent.builder() + .pubKey(SENDER.getPublicKey()) + .kind(Kinds.CONTACT_LIST) + .content("") + .tags(List.of( + GenericTag.of("p", friend1PubKey, "wss://relay1.example.com"), + GenericTag.of("p", friend2PubKey, "wss://relay2.example.com") + )) + .build(); -**Use cases**: -```java -// User preferences (kind 10000) -nip01.createReplaceableEvent( - List.of(), - 10000, - "{\"theme\":\"dark\",\"language\":\"en\"}" -).signAndSend(RELAYS); - -// Contact list (kind 3) -List contacts = List.of( - new PubKeyTag(friend1PubKey), - new PubKeyTag(friend2PubKey) -); -nip01.createReplaceableEvent(contacts, 3, "").signAndSend(RELAYS); +SENDER.sign(contactList); ``` --- -## Internet Identifiers (NIP-05) +## Ephemeral Events -**Purpose**: Link Nostr public key to a DNS-based identifier (name@domain.com) +**Purpose**: Events that relays should not persist (kind 20000-29999). -**Example**: ```java -private static void internetIdMetadata() { - var profile = UserProfile.builder() - .name("Guilherme Gps") - .publicKey(new PublicKey( - "21ef0d8541375ae4bca85285097fba370f7e540b5a30e5e75670c16679f9d144" - )) - .nip05("me@guilhermegps.com.br") // NIP-05 identifier - .build(); - - var nip05 = new NIP05(SENDER); - nip05.createInternetIdentifierMetadataEvent(profile) - .sign() - .send(RELAYS); -} -``` +GenericEvent typing = GenericEvent.builder() + .pubKey(SENDER.getPublicKey()) + .kind(20001) // ephemeral range + .content("{\"typing\":true}") + .build(); -**What it does**: -- Creates a kind `0` metadata event with NIP-05 identifier -- Links public key to human-readable identifier -- Clients can verify the link via `.well-known/nostr.json` - -**Verification** (server-side): -Create `https://yourdomain.com/.well-known/nostr.json`: -```json -{ - "names": { - "username": "21ef0d8541375ae4bca85285097fba370f7e540b5a30e5e75670c16679f9d144" - } -} +SENDER.sign(typing); ``` --- ## Filters and Subscriptions -**Purpose**: Query relays for specific events +**Purpose**: Query relays for specific events. -**Example**: ```java -private static void filters() throws InterruptedException { - var date = Calendar.getInstance(); - date.add(Calendar.DAY_OF_MONTH, -5); // 5 days ago - - var nip01 = NIP01.getInstance(); - nip01.setRelays(RELAYS) - .sendRequest( - new Filters( - new KindFilter<>(Kind.EPHEMEREAL_EVENT), - new KindFilter<>(Kind.TEXT_NOTE), - new AuthorFilter<>(new PublicKey( - "21ef0d8541375ae4bca85285097fba370f7e540b5a30e5e75670c16679f9d144" - )), - new SinceFilter(date.getTimeInMillis() / 1000) - ), - "subId" + System.currentTimeMillis() - ); - - Thread.sleep(5000); // Wait for responses +import nostr.event.filter.EventFilter; +import nostr.event.filter.Filters; +import nostr.event.message.ReqMessage; + +// Build filters +EventFilter filter = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE, Kinds.REACTION)) + .authors(List.of(pubKeyHex)) + .since(System.currentTimeMillis() / 1000 - 86400) // last 24 hours + .limit(50) + .build(); + +Filters filters = new Filters(filter); +String subId = "my-sub-" + System.currentTimeMillis(); +ReqMessage req = new ReqMessage(subId, filters); + +// Blocking request +try (NostrRelayClient client = new NostrRelayClient(RELAY)) { + List responses = client.send(req); + responses.forEach(System.out::println); } -``` - -**Filter types**: -- `KindFilter` – Filter by event kind -- `AuthorFilter` – Filter by author public key -- `SinceFilter` – Events since timestamp -- `UntilFilter` – Events until timestamp -- `IdsFilter` – Specific event IDs -- `LimitFilter` – Limit number of results -**Advanced filtering**: -```java -Filters filters = new Filters( - new KindFilter<>(Kind.TEXT_NOTE), - new AuthorFilter<>(authorPubKey) -); -filters.setLimit(50); // Return max 50 events - -// Multiple authors -Filters multiAuthor = new Filters( - new AuthorFilter<>(author1, author2, author3), - new KindFilter<>(Kind.TEXT_NOTE) -); -``` - -**Non-blocking subscriptions**: -For long-lived subscriptions, see [streaming-subscriptions.md](streaming-subscriptions.md): -```java -AutoCloseable subscription = client.subscribe( - filters, - "my-subscription", - message -> System.out.println("Received: " + message), - error -> System.err.println("Error: " + error) -); -``` - ---- - -## Public Channels (NIP-28) - -NIP-28 provides IRC-like public channels. - -### Create a Channel - -```java -private static GenericEvent createChannel() { - var channel = new ChannelProfile( - "JNostr Channel", - "This is a channel to test NIP28 in nostr-java", - "https://cdn.pixabay.com/photo/2020/05/19/13/48/cartoon-5190942_960_720.jpg" +// Non-blocking subscription +try (NostrRelayClient client = new NostrRelayClient(RELAY)) { + AutoCloseable subscription = client.subscribe( + req, + message -> System.out.println("Event: " + message), + error -> System.err.println("Error: " + error.getMessage()), + () -> System.out.println("Closed") ); - var nip28 = new NIP28(SENDER); - nip28.createChannelCreateEvent(channel) - .sign() - .send(); - - return nip28.getEvent(); -} -``` - -### Update Channel Metadata - -```java -private static void updateChannelMetadata() { - var channelCreateEvent = createChannel(); - - var updatedChannel = new ChannelProfile( - "Updated Channel Name", - "Updated description", - "https://example.com/new-image.jpg" - ); - - var nip28 = new NIP28(SENDER); - nip28.updateChannelMetadataEvent( - channelCreateEvent, - updatedChannel, - null // relay recommendations - ) - .sign() - .send(); -} -``` - -### Send Channel Message - -```java -private static GenericEvent sendChannelMessage() { - var channelCreateEvent = createChannel(); - - var nip28 = new NIP28(SENDER); - nip28.createChannelMessageEvent( - channelCreateEvent, - new Relay("localhost:5555"), - "Hello everybody!" - ) - .sign() - .send(); - - return nip28.getEvent(); -} -``` - -### Hide Message - -```java -private static void hideMessage() { - var channelMessageEvent = sendChannelMessage(); - - var nip28 = new NIP28(SENDER); - nip28.createHideMessageEvent( - channelMessageEvent, - "Spam" // reason - ) - .sign() - .send(); -} -``` - -### Mute User - -```java -private static void muteUser() { - var nip28 = new NIP28(SENDER); - nip28.createMuteUserEvent( - RECIPIENT.getPublicKey(), - "Posting spam" // reason - ) - .sign() - .send(); -} -``` - -**Channel operations**: -- `createChannelCreateEvent` – Create new channel (kind 40) -- `updateChannelMetadataEvent` – Update channel info (kind 41) -- `createChannelMessageEvent` – Post to channel (kind 42) -- `createHideMessageEvent` – Hide message (kind 43) -- `createMuteUserEvent` – Mute user (kind 44) - ---- - -## Running the Examples - -### Prerequisites - -1. **Java 21+** -2. **Local relay** (optional but recommended): - ```bash - # Using Docker - docker run -p 5555:8080 scsibug/nostr-rs-relay - - # Or use public relays (update RELAYS constant) - ``` - -### Run All Examples - -```bash -# Clone the repository -git clone https://github.com/tcheeric/nostr-java.git -cd nostr-java - -# Build the project -./mvnw clean install - -# Run the examples -cd nostr-java-examples -mvn exec:java -Dexec.mainClass="nostr.examples.NostrApiExamples" -``` - -### Run Specific Examples - -Modify `NostrApiExamples.java` to run only specific examples: - -```java -public void run() throws Exception { - logAccountsData(); - - // Comment out examples you don't want to run - // metaDataEvent(); - sendTextNoteEvent(); - // sendEncryptedDirectMessage(); - // ... + Thread.sleep(10_000); // listen for 10 seconds + subscription.close(); } ``` -### Expected Output - -``` -################################ ACCOUNTS BEGINNING ################################ -*** RECEIVER *** - -* PrivateKey: nsec1... -* PublicKey: npub1... - -*** SENDER *** - -* PrivateKey: nsec1... -* PublicKey: npub1... -################################ ACCOUNTS END ################################ - -############################## - sendTextNoteEvent -############################## -[Event sent output...] - -############################## - sendEncryptedDirectMessage -############################## -[DM sent output...] - -... -``` - -### Using with Public Relays - -Replace the relay constant: - -```java -// Instead of local relay -private static final Map RELAYS = - Map.of("local", "localhost:5555"); - -// Use public relays -private static final Map RELAYS = Map.of( - "398ja", "wss://relay.398ja.xyz", - "nos", "wss://nos.lol" -); -``` - --- -## Example Variations - -### Batch Operations - -Send multiple events: - -```java -var nip01 = new NIP01(SENDER); -List.of("Message 1", "Message 2", "Message 3") - .forEach(content -> - nip01.createTextNoteEvent(content) - .sign() - .send(RELAYS) - ); -``` - -### Error Handling - -Handle failures gracefully: - -```java -try { - var nip01 = new NIP01(SENDER); - nip01.createTextNoteEvent("Hello Nostr!") - .sign() - .send(RELAYS); -} catch (IOException e) { - System.err.println("Failed to send event: " + e.getMessage()); - // Retry logic or queue for later -} -``` - -### Async Publishing +## Async Operations -Send events asynchronously: +**Purpose**: Non-blocking relay operations using Virtual Threads. ```java -CompletableFuture future = CompletableFuture.runAsync(() -> { - var nip01 = new NIP01(SENDER); - nip01.createTextNoteEvent("Async message") - .sign() - .send(RELAYS); -}); - -future.thenRun(() -> System.out.println("Event sent!")); +// Connect and send asynchronously +NostrRelayClient.connectAsync(RELAY) + .thenCompose(client -> client.sendAsync(new EventMessage(event))) + .thenAccept(responses -> { + System.out.println("Sent! Responses: " + responses); + }) + .exceptionally(ex -> { + System.err.println("Failed: " + ex.getMessage()); + return null; + }) + .join(); + +// Async subscription +NostrRelayClient.connectAsync(RELAY) + .thenCompose(client -> client.subscribeAsync( + req.encode(), + message -> System.out.println("Event: " + message), + error -> System.err.println("Error: " + error), + () -> System.out.println("Done") + )) + .thenAccept(subscription -> { + // Close when done: subscription.close() + }); ``` --- ## See Also -- [API How-To](use-nostr-java-api.md) – Basic API usage -- [Streaming Subscriptions](streaming-subscriptions.md) – Long-lived subscriptions -- [Custom Events](custom-events.md) – Creating custom event types -- [API Reference](../reference/nostr-java-api.md) – Complete API documentation -- [NostrApiExamples.java source](../../nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java) – Full example code - -## Related NIPs - -- [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) – Basic protocol -- [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) – Encrypted direct messages -- [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md) – DNS identifiers -- [NIP-09](https://github.com/nostr-protocol/nips/blob/master/09.md) – Event deletion -- [NIP-16](https://github.com/nostr-protocol/nips/blob/master/16.md) – Event kinds -- [NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md) – Reactions -- [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md) – Public channels -- [NIP-30](https://github.com/nostr-protocol/nips/blob/master/30.md) – Custom emoji +- [API how-to](use-nostr-java-api.md) — Minimal setup and quick start +- [Streaming subscriptions](streaming-subscriptions.md) — Long-lived subscriptions +- [Custom events](custom-events.md) — Working with custom event kinds +- [Events and tags](../explanation/extending-events.md) — In-depth guide to GenericEvent and GenericTag +- [API reference](../reference/nostr-java-api.md) — Full class and method reference diff --git a/docs/howto/custom-events.md b/docs/howto/custom-events.md index 5bcd00b9e..06ce0f171 100644 --- a/docs/howto/custom-events.md +++ b/docs/howto/custom-events.md @@ -1,6 +1,6 @@ # Custom Nostr Events -Navigation: [Docs index](../README.md) · [Getting started](../GETTING_STARTED.md) · [API how‑to](use-nostr-java-api.md) · [Streaming subscriptions](streaming-subscriptions.md) · [API reference](../reference/nostr-java-api.md) +Navigation: [Docs index](../README.md) · [Getting started](../GETTING_STARTED.md) · [API how-to](use-nostr-java-api.md) · [Streaming subscriptions](streaming-subscriptions.md) · [API reference](../reference/nostr-java-api.md) This guide shows how to construct and publish a Nostr event with a non-standard `kind` using **nostr-java**. @@ -18,29 +18,43 @@ Every Nostr event must include the fields defined in [NIP-01](https://github.com Kinds that are not defined by existing NIPs may still be used. [NIP-16](https://github.com/nostr-protocol/nips/blob/master/16.md) describes how kind numbers are grouped (regular, replaceable, ephemeral and parameterized replaceable). Choose a value that does not collide with other applications. +| Range | Type | Description | +|-------|------|-------------| +| 0-9999 | Regular | Standard events, can be deleted | +| 10000-19999 | Replaceable | Newer event replaces older (by pubkey) | +| 20000-29999 | Ephemeral | Not stored by relays | +| 30000-39999 | Parameterized Replaceable | Replaceable with `d` tag parameter | + ## Example ```java import java.util.List; -import nostr.client.springwebsocket.StandardWebSocketClient; -import nostr.event.BaseTag; +import nostr.client.springwebsocket.NostrRelayClient; import nostr.event.impl.GenericEvent; import nostr.event.message.EventMessage; +import nostr.event.tag.GenericTag; import nostr.id.Identity; public class CustomEventExample { public static void main(String[] args) throws Exception { Identity identity = Identity.generateRandomIdentity(); - int CUSTOM_KIND = 9000; // Non-standard kind - GenericEvent event = new GenericEvent(identity.getPublicKey(), CUSTOM_KIND, List.of(), - "Hello from a custom kind!"); - - // Required fields `id` and `sig` are populated when signing + int CUSTOM_KIND = 9000; // Non-standard kind + GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(CUSTOM_KIND) + .content("Hello from a custom kind!") + .tags(List.of( + GenericTag.of("d", "my-identifier"), + GenericTag.of("t", "custom") + )) + .build(); + + // id and sig are populated when signing identity.sign(event); - try (StandardWebSocketClient client = new StandardWebSocketClient("wss://relay.398ja.xyz")) { + try (NostrRelayClient client = new NostrRelayClient("wss://relay.398ja.xyz")) { client.send(new EventMessage(event)); } } @@ -49,8 +63,31 @@ public class CustomEventExample { ## Steps Explained -1. **Construct the event** – Provide the public key, custom kind, any tags, and content. The constructor fills in `created_at` automatically and initializes the list of `tags`. -2. **Sign** – `Identity.sign(event)` computes the event `id` and `sig` using the private key. Relays verify these fields against the serialized event bytes as defined in NIP‑01. -3. **Submit to a relay** – Send the event using an `EVENT` message. The example uses `StandardWebSocketClient`, but any Nostr-compatible relay transport will work. +1. **Construct the event** — Use `GenericEvent.builder()` with any `int` kind, content, and tags. The builder fills in `created_at` automatically. +2. **Add tags** — Use `GenericTag.of(code, params...)` to create tags. Tags are just a code string and a list of string parameters. +3. **Sign** — `Identity.sign(event)` computes the event `id` and `sig` using the private key. Relays verify these fields against the serialized event bytes as defined in NIP-01. +4. **Send to a relay** — Send the event using an `EVENT` message via `NostrRelayClient`. + +## Async alternative + +```java +NostrRelayClient.connectAsync("wss://relay.398ja.xyz") + .thenCompose(client -> client.sendAsync(new EventMessage(event))) + .thenAccept(responses -> System.out.println("Responses: " + responses)) + .join(); +``` + +## Kind range checks + +Use `Kinds` utility methods to classify kinds: + +```java +import nostr.base.Kinds; + +Kinds.isReplaceable(CUSTOM_KIND) // true if 10000-19999 +Kinds.isEphemeral(CUSTOM_KIND) // true if 20000-29999 +Kinds.isAddressable(CUSTOM_KIND) // true if 30000-39999 +Kinds.isValid(CUSTOM_KIND) // true if 0-65535 +``` For more information about event structure and relay communication, consult [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) and [NIP-16](https://github.com/nostr-protocol/nips/blob/master/16.md). diff --git a/docs/howto/diagnostics.md b/docs/howto/diagnostics.md index 661fa7847..45714e2db 100644 --- a/docs/howto/diagnostics.md +++ b/docs/howto/diagnostics.md @@ -1,68 +1,66 @@ # Diagnostics: Relay Failures and Troubleshooting -This how‑to shows how to inspect, capture, and react to relay send failures when broadcasting events via the API client. +This how-to shows how to inspect and react to relay send failures when broadcasting events. ## Overview -- `DefaultNoteService` attempts to send an event to all configured relays. -- Failures on individual relays are tolerated; other relays are still attempted. -- After the send completes, you can inspect failures and structured details. -- You can also register a listener to receive failures in real time. +- `NostrRelayClient` sends events to a single relay per connection. +- Failures throw exceptions (`IOException`, `RelayTimeoutException`) that can be caught and handled. +- Spring Retry (`@NostrRetryable`) automatically retries transient I/O failures with exponential backoff. -## Inspect last failures +## Catching failures ```java -NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(sender); -client.setRelays(Map.of( - "relayA", "wss://relayA.example.com", - "relayB", "wss://relayB.example.com" -)); - -List responses = client.sendEvent(event); - -// Map: relay name to exception -Map failures = client.getLastSendFailures(); -failures.forEach((relay, error) -> System.err.printf( - "Relay %s failed: %s%n", relay, error.getMessage() -)); - -// Structured details (timestamp, relay URI, cause chain summary) -Map details = client.getLastSendFailureDetails(); -details.forEach((relay, info) -> System.err.printf( - "[%d] %s (%s) failed: %s | root: %s - %s%n", - info.timestampEpochMillis, - info.relayName, - info.relayUri, - info.message, - info.rootCauseClass, - info.rootCauseMessage -)); +try (NostrRelayClient client = new NostrRelayClient("wss://relay.example.com")) { + List responses = client.send(new EventMessage(event)); + System.out.println("Responses: " + responses); +} catch (RelayTimeoutException e) { + System.err.printf("Timeout after %dms on relay %s%n", e.getTimeoutMs(), e.getRelayUri()); +} catch (IOException e) { + System.err.println("Send failed: " + e.getMessage()); +} ``` -Note: If you use a custom `NoteService`, these accessors return empty maps unless the implementation exposes diagnostics. - -## Receive failures with a listener +## Sending to multiple relays -Register a callback to receive the failures map immediately after each send attempt: +Send to multiple relays and collect results: ```java -client.onSendFailures(failureMap -> { - failureMap.forEach((relay, t) -> System.err.printf( - "Failure on %s: %s: %s%n", - relay, t.getClass().getSimpleName(), t.getMessage() - )); -}); +List relays = List.of("wss://relay1.example.com", "wss://relay2.example.com"); +Map> results = new HashMap<>(); +Map failures = new HashMap<>(); + +for (String relay : relays) { + try (NostrRelayClient client = new NostrRelayClient(relay)) { + results.put(relay, client.send(new EventMessage(event))); + } catch (Exception e) { + failures.put(relay, e); + } +} + +failures.forEach((relay, error) -> + System.err.printf("Relay %s failed: %s%n", relay, error.getMessage()) +); ``` -## Tips +## Async multi-relay send + +```java +List relays = List.of("wss://relay1.example.com", "wss://relay2.example.com"); + +List>> futures = relays.stream() + .map(relay -> NostrRelayClient.connectAsync(relay) + .thenCompose(client -> client.sendAsync(new EventMessage(event)))) + .toList(); -- Partial success is common on public relays; prefer aggregating successful responses. -- Use `getLastSendFailureDetails()` when you need to correlate failures with relay URIs or log timestamps. -- Combine diagnostics with your retry/backoff strategy at the application level if needed. +CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenRun(() -> System.out.println("All relays attempted")) + .join(); +``` ## MDC snippet (correlate logs per send) -Use SLF4J MDC to attach a correlation id for a send. Remember to clear the MDC in `finally`. +Use SLF4J MDC to attach a correlation id for a send: ```java import org.slf4j.MDC; @@ -71,11 +69,10 @@ import java.util.UUID; String correlationId = UUID.randomUUID().toString(); MDC.put("corrId", correlationId); try { - var responses = client.sendEvent(event); - // Your logging here; include %X{corrId} in your log pattern - log.info("Sent event id={} corrId={} responses={}", event.getId(), correlationId, responses.size()); + var responses = client.send(new EventMessage(event)); + log.info("Sent event id={} corrId={} responses={}", event.getId(), correlationId, responses.size()); } finally { - MDC.remove("corrId"); + MDC.remove("corrId"); } ``` @@ -84,3 +81,9 @@ Logback pattern example: ```properties logging.pattern.console=%d{HH:mm:ss.SSS} %-5level [%X{corrId}] %logger{36} - %msg%n ``` + +## Tips + +- Partial success is common on public relays; send to multiple relays for redundancy. +- `RelayTimeoutException` provides `getRelayUri()` and `getTimeoutMs()` for structured diagnostics. +- Spring Retry handles transient failures automatically (3 attempts, exponential backoff from 500ms). diff --git a/docs/howto/streaming-subscriptions.md b/docs/howto/streaming-subscriptions.md index 4a3204513..2fca6b180 100644 --- a/docs/howto/streaming-subscriptions.md +++ b/docs/howto/streaming-subscriptions.md @@ -1,83 +1,113 @@ # Streaming Subscriptions -Navigation: [Docs index](../README.md) · [Getting started](../GETTING_STARTED.md) · [API how‑to](use-nostr-java-api.md) · [Custom events](custom-events.md) · [API reference](../reference/nostr-java-api.md) +Navigation: [Docs index](../README.md) · [Getting started](../GETTING_STARTED.md) · [API how-to](use-nostr-java-api.md) · [Custom events](custom-events.md) · [API reference](../reference/nostr-java-api.md) -This guide explains how to open and manage long‑lived, non‑blocking subscriptions to Nostr relays -using the `nostr-java` API. It covers lifecycle, concurrency/backpressure, multiple relays, and -error handling. +This guide explains how to open and manage long-lived, non-blocking subscriptions to Nostr relays using `NostrRelayClient`. ## Overview -- Use `NostrSpringWebSocketClient.subscribe` to open a REQ subscription that streams relay messages - to your callback. -- The method returns immediately with an `AutoCloseable`. Calling `close()` sends a `CLOSE` to the - relay(s) and frees the underlying WebSocket resource(s). -- Callbacks run on the WebSocket thread; offload heavy work to your own executor/queue to keep the - socket responsive. +- Use `NostrRelayClient.subscribe()` to open a REQ subscription that streams relay messages to your callback. +- The method returns immediately with an `AutoCloseable`. Calling `close()` sends a `CLOSE` to the relay and frees the underlying WebSocket resource. +- Callbacks are dispatched on Virtual Threads, so expensive listener logic does not block inbound WebSocket I/O. ## Quick start ```java -import java.util.Map; -import nostr.api.NostrSpringWebSocketClient; -import nostr.base.Kind; +import nostr.base.Kinds; +import nostr.client.springwebsocket.NostrRelayClient; +import nostr.event.filter.EventFilter; import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; - -Map relays = Map.of("398ja", "wss://relay.398ja.xyz"); - -NostrSpringWebSocketClient client = new NostrSpringWebSocketClient().setRelays(relays); - -Filters filters = new Filters(new KindFilter<>(Kind.TEXT_NOTE)); - -AutoCloseable subscription = client.subscribe( - filters, - "example-subscription", - message -> { - // Handle EVENT/EOSE/NOTICE payloads here. Offload if heavy. - }, - error -> { - // Log/report errors. Consider retry or metrics. - } -); +import nostr.event.message.ReqMessage; + +import java.util.List; + +// Build a filter +EventFilter filter = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE)) + .limit(100) + .build(); + +Filters filters = new Filters(filter); +String subscriptionId = "my-sub-" + System.currentTimeMillis(); +ReqMessage req = new ReqMessage(subscriptionId, filters); + +// Open subscription +try (NostrRelayClient client = new NostrRelayClient("wss://relay.398ja.xyz")) { + AutoCloseable subscription = client.subscribe( + req, + message -> { + // Handle EVENT/EOSE/NOTICE payloads here + System.out.println("Received: " + message); + }, + error -> { + System.err.println("Error: " + error.getMessage()); + }, + () -> { + System.out.println("Connection closed"); + } + ); + + // Keep the subscription open while processing events + Thread.sleep(30_000); + + subscription.close(); // sends CLOSE and releases resources +} +``` -// ... keep the subscription open while processing events ... +## Async subscription (Virtual Threads) -subscription.close(); // sends CLOSE and releases resources -client.close(); // closes any remaining relay connections +```java +NostrRelayClient.connectAsync("wss://relay.398ja.xyz") + .thenCompose(client -> client.subscribeAsync( + req.encode(), + message -> System.out.println("Event: " + message), + error -> System.err.println("Error: " + error), + () -> System.out.println("Closed") + )) + .thenAccept(subscription -> { + // subscription is AutoCloseable — close when done + }); ``` -See a runnable example in [../../nostr-java-examples/src/main/java/nostr/examples/SpringSubscriptionExample.java](../../nostr-java-examples/src/main/java/nostr/examples/SpringSubscriptionExample.java). - ## Lifecycle and closing -- Each `subscribe` call opens a dedicated WebSocket per relay. Keep the handle while you need the - stream and call `close()` when done. +- Each `subscribe()` call opens a dedicated WebSocket connection. Keep the returned handle while you need the stream and call `close()` when done. - Always close subscriptions to ensure a `CLOSE` frame is sent to the relay and resources are freed. - After `close()`, no further messages will be delivered to your listener. ## Concurrency and backpressure -- Message callbacks execute on the WebSocket thread; avoid blocking. If processing may block, hand - off to a separate executor or queue. -- For high‑throughput feeds, consider batching or asynchronous processing to prevent socket stalls. +- Message callbacks execute on Virtual Threads; processing can block safely, but bounded queues are still recommended when downstream systems are slower than relay throughput. +- For high-throughput feeds, consider batching or asynchronous processing to prevent socket stalls. +- The client limits accumulated events per blocking request to 10,000 by default (configurable) to prevent unbounded memory growth. -## Multiple relays +## Filter examples -- When multiple relays are configured via `setRelays`, the client opens one WebSocket per relay and - fans out the same REQ. Your listener receives messages from all configured relays. -- Include an identifier (e.g., relay name/URL) in logs/metrics if you need per‑relay visibility. +```java +// Filter by multiple kinds and authors +EventFilter filter = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE, Kinds.REACTION)) + .authors(List.of(pubKeyHex1, pubKeyHex2)) + .since(System.currentTimeMillis() / 1000 - 86400) // last 24 hours + .limit(200) + .build(); + +// Filter by tag values +EventFilter tagFilter = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE)) + .addTagFilter("t", List.of("nostr", "bitcoin")) + .build(); +``` ## Error handling - Provide an `errorListener` to capture exceptions raised during subscription or message handling. -- Consider transient vs. fatal errors. You can implement retry logic at the application level if - desired. +- If the relay times out on a blocking send, `NostrRelayClient` throws `RelayTimeoutException` (not an empty list). +- Consider transient vs. fatal errors. The client uses Spring Retry with exponential backoff for transient I/O failures. ## Related API -- Client: `nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java` -- WebSocket wrapper: `nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClient.java` -- Interface: `nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClientIF.java` +- Client: `nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java` +- Filters: `nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java` -For method signatures and additional details, see the API reference: [../reference/nostr-java-api.md](../reference/nostr-java-api.md). +For method signatures and additional details, see the [API reference](../reference/nostr-java-api.md). diff --git a/docs/howto/use-nostr-java-api.md b/docs/howto/use-nostr-java-api.md index cbf1010bd..ae328d6b2 100644 --- a/docs/howto/use-nostr-java-api.md +++ b/docs/howto/use-nostr-java-api.md @@ -1,4 +1,4 @@ -# Using the nostr-java API +# Using nostr-java Navigation: [Docs index](../README.md) · [Getting started](../GETTING_STARTED.md) · [Streaming subscriptions](streaming-subscriptions.md) · [Custom events](custom-events.md) · [API reference](../reference/nostr-java-api.md) @@ -6,7 +6,7 @@ This guide shows how to set up the library and publish a basic [Nostr](https://g ## Minimal setup -Add the API module to your project (with the BOM): +Add the client module to your project (with the BOM): ```xml @@ -24,39 +24,62 @@ Add the API module to your project (with the BOM): xyz.tcheeric - nostr-java-api + nostr-java-client ``` +The `nostr-java-client` module transitively brings in all other modules (`identity`, `event`, `core`). + Check the [releases page](https://github.com/tcheeric/nostr-java/releases) for the latest BOM version. ## Create, sign, and publish an event ```java -import nostr.api.NIP01; +import nostr.base.Kinds; +import nostr.client.springwebsocket.NostrRelayClient; +import nostr.event.impl.GenericEvent; +import nostr.event.message.EventMessage; +import nostr.event.tag.GenericTag; import nostr.id.Identity; -import java.util.Map; +import java.util.List; public class QuickStart { - public static void main(String[] args) { + public static void main(String[] args) throws Exception { Identity identity = Identity.generateRandomIdentity(); - Map relays = Map.of("398ja", "wss://relay.398ja.xyz"); - new NIP01(identity) - .createTextNoteEvent("Hello nostr") - .sign() - .send(relays); + GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Hello Nostr!") + .tags(List.of(GenericTag.of("t", "nostr-java"))) + .build(); + + identity.sign(event); + + try (NostrRelayClient client = new NostrRelayClient("wss://relay.398ja.xyz")) { + client.send(new EventMessage(event)); + } } } ``` +### Async alternative (Virtual Threads) + +```java +NostrRelayClient.connectAsync("wss://relay.398ja.xyz") + .thenCompose(client -> client.sendAsync(new EventMessage(event))) + .thenAccept(responses -> System.out.println("Sent! Responses: " + responses)) + .join(); +``` + ### Reference -- [`Identity.generateRandomIdentity`](../../nostr-java-id/src/main/java/nostr/id/Identity.java) -- [`NIP01.createTextNoteEvent`](../../nostr-java-api/src/main/java/nostr/api/NIP01.java) -- [`EventNostr.sign`](../../nostr-java-api/src/main/java/nostr/api/EventNostr.java) -- [`EventNostr.send`](../../nostr-java-api/src/main/java/nostr/api/EventNostr.java) +- [`Identity.generateRandomIdentity`](../../nostr-java-identity/src/main/java/nostr/id/Identity.java) +- [`GenericEvent.builder`](../../nostr-java-event/src/main/java/nostr/event/impl/GenericEvent.java) +- [`NostrRelayClient`](../../nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java) ### Next steps - Streaming, lifecycle, and backpressure: [streaming-subscriptions.md](streaming-subscriptions.md) +- Working with custom kinds: [custom-events.md](custom-events.md) +- Events and tags in depth: [../explanation/extending-events.md](../explanation/extending-events.md) diff --git a/docs/howto/version-uplift-workflow.md b/docs/howto/version-uplift-workflow.md index e213601da..3e58ceb0e 100644 --- a/docs/howto/version-uplift-workflow.md +++ b/docs/howto/version-uplift-workflow.md @@ -126,7 +126,7 @@ scripts/release.sh next-snapshot --version 1.0.1-SNAPSHOT xyz.tcheeric - nostr-java-api + nostr-java-client ``` diff --git a/docs/operations/configuration.md b/docs/operations/configuration.md index a1e4bb50a..9f10efbef 100644 --- a/docs/operations/configuration.md +++ b/docs/operations/configuration.md @@ -4,39 +4,48 @@ Tune WebSocket behavior and retries for your environment. ## Purpose -- Adjust timeouts and poll intervals for send operations. +- Adjust timeouts for send operations. - Understand retry behavior for transient I/O failures. +- Configure WebSocket buffer sizes for large events. ## WebSocket client settings -The Spring WebSocket client reads the following properties (with defaults): +`NostrRelayClient` reads the following properties (with defaults): - `nostr.websocket.await-timeout-ms` (default: `60000`) — Max time to await a response after send. -- `nostr.websocket.poll-interval-ms` (default: `500`) — Poll interval used during await. -- `nostr.websocket.max-idle-timeout-ms` (default: `3600000`) — Max idle timeout for WebSocket sessions. Set to `0` for no timeout. This prevents premature connection closures when relays have periods of inactivity. +- `nostr.websocket.max-idle-timeout-ms` (default: `3600000`) — Max idle timeout for WebSocket sessions. Set to `0` for no timeout. Prevents premature connection closures when relays have periods of inactivity. +- `nostr.websocket.max-text-message-buffer-size` (default: `1048576`) — WebSocket text message buffer size in bytes. +- `nostr.websocket.max-binary-message-buffer-size` (default: `1048576`) — WebSocket binary message buffer size in bytes. Example (application.properties): ``` nostr.websocket.await-timeout-ms=30000 -nostr.websocket.poll-interval-ms=250 -nostr.websocket.max-idle-timeout-ms=7200000 # 2 hours +nostr.websocket.max-idle-timeout-ms=7200000 +nostr.websocket.max-text-message-buffer-size=2097152 ``` ## Retry behavior -WebSocket send and subscribe operations are annotated with a common retry policy: +WebSocket send and subscribe operations are annotated with `@NostrRetryable`: - Included exception: `IOException` - Max attempts: `3` - Backoff: initial `500ms`, multiplier `2.0` -These values are defined in the `@NostrRetryable` annotation. To customize globally, consider: +To customize globally, create a custom annotation or replace `@NostrRetryable` with your configuration. -- Creating a custom annotation or replacing `@NostrRetryable` with your configuration. -- Providing your own `NoteService` or client wrapper that applies your retry strategy. +## Virtual Thread dispatch + +Relay subscription callbacks and async operations (`connectAsync`, `sendAsync`, `subscribeAsync`) are dispatched on Virtual Threads using named thread factories: + +- `nostr-relay-io-*` — relay I/O operations +- `nostr-listener-*` — listener/callback dispatch +- `nostr-http-*` — HTTP client operations (NIP-05 validation) ## Notes - Timeouts apply per send; long-running subscriptions are managed separately. -- Ensure your relay endpoints’ SLAs align with chosen timeouts and backoff. +- If a relay response is not received before the timeout elapses, `NostrRelayClient` throws `RelayTimeoutException`. +- The maximum number of events accumulated per blocking request defaults to 10,000 to prevent unbounded memory growth. +- Ensure your relay endpoints' SLAs align with chosen timeouts and backoff. diff --git a/docs/operations/metrics.md b/docs/operations/metrics.md index 6d359bf8f..37578a016 100644 --- a/docs/operations/metrics.md +++ b/docs/operations/metrics.md @@ -7,35 +7,34 @@ Capture simple client metrics (successes/failures) without bringing a full metri - Track successful and failed relay sends. - Provide hooks for plugging into your metrics/observability system. -## Minimal counters via listener +## Minimal counters ```java +import java.util.concurrent.atomic.AtomicLong; + class Counters { - final java.util.concurrent.atomic.AtomicLong sendsOk = new java.util.concurrent.atomic.AtomicLong(); - final java.util.concurrent.atomic.AtomicLong sendsFailed = new java.util.concurrent.atomic.AtomicLong(); + final AtomicLong sendsOk = new AtomicLong(); + final AtomicLong sendsFailed = new AtomicLong(); } Counters metrics = new Counters(); -NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(sender); - -client.onSendFailures(failureMap -> { - // Any failure increments failed; actual successes counted after sendEvent - metrics.sendsFailed.addAndGet(failureMap.size()); -}); -var responses = client.sendEvent(event); -metrics.sendsOk.addAndGet(responses.size()); +try (NostrRelayClient client = new NostrRelayClient("wss://relay.example.com")) { + List responses = client.send(new EventMessage(event)); + metrics.sendsOk.incrementAndGet(); +} catch (Exception e) { + metrics.sendsFailed.incrementAndGet(); +} ``` ## Integrating with your stack -- Micrometer: Wrap the listener to increment `Counter` instances and register with your registry. -- Prometheus: Expose counters using your HTTP endpoint and update from the listener. -- Logs: Periodically log counters as structured JSON for ingestion by your log pipeline. +- Micrometer: Wrap send calls with `Timer` and `Counter` instances. +- Prometheus: Expose counters using your HTTP endpoint. +- Logs: Periodically log counters as structured JSON. ## Notes -- Listener runs on the calling thread; keep callbacks fast and non-blocking. - Prefer batching external calls (e.g., ship metrics on a schedule) over per-event network calls. ## Micrometer example (with Prometheus) @@ -43,151 +42,115 @@ metrics.sendsOk.addAndGet(responses.size()); Add Micrometer + Prometheus dependencies (Spring Boot example): ```xml - io.micrometer micrometer-core runtime - - io.micrometer micrometer-registry-prometheus runtime - ``` -Register counters and a timer, then wire the failure listener: +Register counters and a timer: ```java import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import nostr.api.NostrSpringWebSocketClient; -import nostr.base.IEvent; +import nostr.client.springwebsocket.NostrRelayClient; +import nostr.event.message.EventMessage; public class NostrMetrics { - private final Counter sendsOk; - private final Counter sendsFailed; - private final Timer sendTimer; - - public NostrMetrics(MeterRegistry registry) { - this.sendsOk = Counter.builder("nostr.sends.ok").description("Successful relay responses").register(registry); - this.sendsFailed = Counter.builder("nostr.sends.failed").description("Failed relay sends").register(registry); - this.sendTimer = Timer.builder("nostr.send.timer").description("Send latency per event").publishPercentileHistogram().register(registry); - } - - public void instrument(NostrSpringWebSocketClient client) { - // Count failures per send call (sum of relays that failed) - client.onSendFailures((Map failures) -> sendsFailed.increment(failures.size())); - } - - public List timedSend(NostrSpringWebSocketClient client, IEvent event) { - return sendTimer.record(() -> client.sendEvent(event)); - } + private final Counter sendsOk; + private final Counter sendsFailed; + private final Timer sendTimer; + + public NostrMetrics(MeterRegistry registry) { + this.sendsOk = Counter.builder("nostr.sends.ok") + .description("Successful relay responses").register(registry); + this.sendsFailed = Counter.builder("nostr.sends.failed") + .description("Failed relay sends").register(registry); + this.sendTimer = Timer.builder("nostr.send.timer") + .description("Send latency per event") + .publishPercentileHistogram().register(registry); + } + + public List timedSend(NostrRelayClient client, EventMessage message) { + return sendTimer.record(() -> { + try { + List responses = client.send(message); + sendsOk.increment(); + return responses; + } catch (Exception e) { + sendsFailed.increment(); + throw new RuntimeException(e); + } + }); + } } ``` -Labeling failures by relay (beware high cardinality): - -```java -client.onSendFailures(failures -> failures.forEach((relay, t) -> - Counter.builder("nostr.sends.failed") - .tag("relay", relay) // cardinality grows with number of relays - .tag("exception", t.getClass().getSimpleName()) - .register(registry) - .increment() -)); -``` - -Expose Prometheus metrics (Spring Boot): - -```properties -# application.properties -management.endpoints.web.exposure.include=prometheus -management.endpoint.prometheus.enabled=true -``` - -Navigate to `/actuator/prometheus` to scrape metrics. - ## Spring Boot wiring example -Create a configuration that wires the client, metrics, and listener: - ```java -// src/main/java/com/example/nostr/NostrConfig.java import io.micrometer.core.instrument.MeterRegistry; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import nostr.api.NostrSpringWebSocketClient; import nostr.id.Identity; @Configuration public class NostrConfig { - @Bean - public Identity nostrIdentity() { - // Replace with a real private key or a managed Identity - return Identity.generateRandomIdentity(); - } - - @Bean - public NostrSpringWebSocketClient nostrClient(Identity identity) { - return new NostrSpringWebSocketClient(identity); - } - - @Bean - public NostrMetrics nostrMetrics(MeterRegistry registry, NostrSpringWebSocketClient client) { - NostrMetrics metrics = new NostrMetrics(registry); - metrics.instrument(client); - return metrics; - } + @Bean + public Identity nostrIdentity() { + return Identity.generateRandomIdentity(); + } + + @Bean + public NostrMetrics nostrMetrics(MeterRegistry registry) { + return new NostrMetrics(registry); + } } ``` -Use the instrumented client and timer in your service: +Use in your service: ```java -// src/main/java/com/example/nostr/NostrService.java -import java.util.List; -import org.springframework.stereotype.Service; -import lombok.RequiredArgsConstructor; -import nostr.api.NostrSpringWebSocketClient; +import nostr.base.Kinds; +import nostr.client.springwebsocket.NostrRelayClient; import nostr.event.impl.GenericEvent; -import nostr.base.Kind; +import nostr.event.message.EventMessage; +import nostr.event.tag.GenericTag; +import nostr.id.Identity; @Service @RequiredArgsConstructor public class NostrService { - private final NostrSpringWebSocketClient client; - private final NostrMetrics metrics; - - public List publish(String content) { - GenericEvent event = GenericEvent.builder() - .pubKey(client.getSender().getPublicKey()) - .kind(Kind.TEXT_NOTE) - .content(content) - .build(); - event.update(); - client.sign(client.getSender(), event); - return metrics.timedSend(client, event); - } + private final Identity identity; + private final NostrMetrics metrics; + + public List publish(String content) throws Exception { + GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content(content) + .build(); + identity.sign(event); + + try (NostrRelayClient client = new NostrRelayClient("wss://relay.398ja.xyz")) { + return metrics.timedSend(client, new EventMessage(event)); + } + } } ``` -Application properties (example): +Application properties: ```properties -# Expose Prometheus endpoint management.endpoints.web.exposure.include=prometheus management.endpoint.prometheus.enabled=true - -# Optional: tune WebSocket timeouts nostr.websocket.await-timeout-ms=30000 -nostr.websocket.poll-interval-ms=250 ``` diff --git a/docs/problems/GENERIC_TAG_GETCODE_FRAGILITY.md b/docs/problems/GENERIC_TAG_GETCODE_FRAGILITY.md new file mode 100644 index 000000000..8557eaff8 --- /dev/null +++ b/docs/problems/GENERIC_TAG_GETCODE_FRAGILITY.md @@ -0,0 +1,266 @@ +# Problem: GenericTag.getCode() NPE and Downstream Tag Resolution Failures + +**Date:** 2026-02-23 +**Severity:** High — causes silent data loss in downstream consumers +**Affected class:** `nostr.event.tag.GenericTag` +**Root method:** `GenericTag.getCode()` + +--- + +## Summary + +`GenericTag.getCode()` delegates to `super.getCode()` when its `code` field is empty. +`BaseTag.getCode()` reads the `@Tag` annotation via reflection — but `GenericTag` has no +`@Tag` annotation, so the delegation always throws `NullPointerException`. This is a +**design contradiction**: `GenericTag` exists specifically for tags without a registered +annotation, yet its fallback path assumes one exists. + +Even when `code` is non-empty (the common case), the method's structure forces downstream +consumers to either use reflection to read the `code` field directly, or wrap `getCode()` +in a try-catch — both of which are fragile and have caused production bugs. + +--- + +## Detailed Analysis + +### The current implementation + +```java +// GenericTag.java (line ~47) +@Override +public String getCode() { + return "".equals(this.code) ? super.getCode() : this.code; +} +``` + +```java +// BaseTag.java +@Override +public String getCode() { + return this.getClass().getAnnotation(Tag.class).code(); +} +``` + +### Why the fallback is unreachable by design + +`GenericTag` is the **catch-all** tag type. It handles any tag code not registered in +`TagRegistry`. By definition, it carries its own `code` field and does not have a `@Tag` +annotation. The `super.getCode()` fallback assumes the subclass has `@Tag`, which is true +for `PubKeyTag`, `EventTag`, `IdentifierTag`, etc. — but never for `GenericTag`. + +The annotation-based path exists for **registered** tags: + +| Class | `@Tag` annotation | `getCode()` source | +|------------------|-----------------------|-----------------------| +| `PubKeyTag` | `@Tag(code = "p")` | Annotation (via super)| +| `EventTag` | `@Tag(code = "e")` | Annotation (via super)| +| `IdentifierTag` | `@Tag(code = "d")` | Annotation (via super)| +| `AddressTag` | `@Tag(code = "a")` | Annotation (via super)| +| `GenericTag` | **None** | Instance `code` field | + +The ternary in `GenericTag.getCode()` creates a dead branch that NPEs when reached. + +### How this causes downstream failures + +#### 1. Direct NPE on empty code + +```java +new GenericTag("").getCode() // NPE +new GenericTag().getCode() // NPE (no-arg constructor sets code = "") +``` + +While the no-arg and empty-code constructors are rarely used with real Nostr events, +they are valid API surface (public constructors, `@NonNull` only rejects null). + +#### 2. Forcing consumers into reflection + +Because `getCode()` is unreliable for `GenericTag`, downstream code resorts to +reflection to read the private `code` field: + +```java +// Pattern found in imani-bridge (two separate files) +private static final Field GENERIC_TAG_CODE_FIELD; +static { + GENERIC_TAG_CODE_FIELD = GenericTag.class.getDeclaredField("code"); + GENERIC_TAG_CODE_FIELD.setAccessible(true); +} + +private static String extractGenericTagCode(GenericTag genericTag) { + try { + Object raw = GENERIC_TAG_CODE_FIELD.get(genericTag); + return raw != null ? raw.toString() : ""; + } catch (IllegalAccessException ignored) { + return ""; // Silent failure! + } +} +``` + +This reflection pattern has two critical problems: + +**a) Java module system blocks access silently.** Under Java 21 with JPMS, `setAccessible(true)` +may fail with `InaccessibleObjectException` (if modules are enforced) or succeed but later +`Field.get()` throws `IllegalAccessException` at runtime. The catch block returns `""`, +making every tag code appear empty — which silently breaks all tag-based filtering. + +**b) Duplicated fragile code.** Every consumer that needs `GenericTag` codes must independently +implement the same reflection hack. In imani-bridge, this pattern was duplicated in two files +(`NostrEventAdapter` and `NostrEventJsonConverter`), both with the same silent-failure bug. + +#### 3. Production impact: lost ecash tokens + +In the imani-bridge Cashu gateway, the reflection failure caused `matchesEventTagFilters()` +to reject all gift-wrap events (kind 1059) from the local nostrdb cache. The `#p` tag filter +expected `tag.get(0) = "p"` but got `tag.get(0) = ""` because the reflection silently failed. + +This manifested as **intermittent** token loss: +- When the local cache was "fresh", only nostrdb events were returned — all rejected +- When the cache was "stale", relay events were also fetched — these used `PubKeyTag` + (which has `@Tag(code="p")`) and passed the filter correctly +- The behavior appeared random depending on cache timing + +The fix was to replace reflection with `getCode()` wrapped in a NPE catch. This works for +non-empty codes (the production case) but is still a workaround for the underlying design issue. + +--- + +## The Two-Path Tag Architecture + +nostr-java has two fundamentally different tag models: + +### Path A: Annotation-based (registered tags) + +``` +JSON ["p", "abc123"] + → TagDeserializer recognizes "p" + → PubKeyTag.deserialize(node) + → PubKeyTag { publicKey = "abc123" } + → getCode() reads @Tag(code="p") via reflection → "p" +``` + +Serialization uses `@Key`-annotated fields and `getSupportedFields()` introspection. + +### Path B: Attribute-based (generic tags) + +``` +JSON ["x", "value1", "value2"] + → TagDeserializer doesn't recognize "x" + → GenericTagDecoder → GenericTag("x", [attr0="value1", attr1="value2"]) + → getCode() returns this.code → "x" +``` + +Serialization uses `ElementAttribute` list via `GenericTagSerializer.applyCustomAttributes()`. + +These two paths have **incompatible interfaces** for accessing tag values: + +| Operation | Registered tags | GenericTag | +|--------------------|------------------------------|---------------------------------| +| Get code | `@Tag` annotation | Instance `code` field | +| Get values | `@Key` fields + reflection | `getAttributes()` list | +| Serialize | `BaseTagSerializer` | `GenericTagSerializer` | +| Deserialize | Type-specific deserializer | `GenericTagDecoder` | + +Consumers converting tags to a uniform `List>` format (the NIP-01 wire format) +must handle both paths, which is error-prone. + +--- + +## Proposed Fixes + +### Minimal fix: Make GenericTag.getCode() self-contained + +```java +@Override +public String getCode() { + return this.code; // Never delegate to super +} +``` + +This is the smallest safe change. `GenericTag` always knows its own code. The `""` fallback +to `super.getCode()` was never reachable without NPE, so removing it changes no observable +behavior for valid inputs. For empty-code inputs, it returns `""` instead of throwing NPE. + +### Better fix: Add null-safety to BaseTag.getCode() + +```java +// BaseTag.java +@Override +public String getCode() { + Tag annotation = this.getClass().getAnnotation(Tag.class); + return annotation != null ? annotation.code() : null; +} +``` + +This protects all subclasses, not just `GenericTag`. Any future tag subclass that forgets +`@Tag` would get `null` instead of NPE. + +### Ideal fix: Unify the tag value access model + +Add a method to `BaseTag` that returns the NIP-01 wire format uniformly: + +```java +// BaseTag.java +public List toTagArray() { + List result = new ArrayList<>(); + result.add(getCode()); + getSupportedFields().forEach(f -> + getFieldValue(f).ifPresent(result::add)); + return result; +} + +// GenericTag.java (override) +@Override +public List toTagArray() { + List result = new ArrayList<>(); + result.add(getCode()); + attributes.forEach(a -> + result.add(a.value() != null ? a.value().toString() : "")); + return result; +} +``` + +This would let consumers work with tags uniformly without knowing whether they're +registered or generic, eliminating the dual-path handling entirely. + +--- + +## Reproducer + +```java +@Test +void genericTag_getCode_npe_on_empty_code() { + // This throws NullPointerException + GenericTag tag = new GenericTag(""); + assertThrows(NullPointerException.class, tag::getCode); +} + +@Test +void genericTag_getCode_works_with_nonempty_code() { + GenericTag tag = new GenericTag("p"); + assertEquals("p", tag.getCode()); // Works — but only because fallback is not reached +} + +@Test +void baseTag_getCode_npe_without_annotation() { + // Any subclass without @Tag will NPE + BaseTag tag = new GenericTag("x"); + // This works because code is non-empty, but the API contract is misleading + assertEquals("x", tag.getCode()); +} +``` + +--- + +## Affected Downstream Projects + +- **imani-bridge** (Cashu gateway) — tag filtering, event conversion, nostrdb caching +- Any project that stores/retrieves Nostr events through `GenericTag` and needs to read tag codes + +--- + +## References + +- `nostr.event.tag.GenericTag` — the affected class +- `nostr.event.BaseTag.getCode()` — the annotation-based fallback +- `nostr.event.tag.TagRegistry` — factory that determines GenericTag vs registered tag +- `nostr.event.json.deserializer.TagDeserializer` — deserializer entry point +- NIP-01 event format: https://github.com/nostr-protocol/nips/blob/master/01.md diff --git a/docs/reference/nostr-java-api.md b/docs/reference/nostr-java-api.md index 573e37dd8..b8b6a6250 100644 --- a/docs/reference/nostr-java-api.md +++ b/docs/reference/nostr-java-api.md @@ -1,13 +1,15 @@ # Nostr Java API Reference -Navigation: [Docs index](../README.md) · [Getting started](../GETTING_STARTED.md) · [API how‑to](../howto/use-nostr-java-api.md) · [Streaming subscriptions](../howto/streaming-subscriptions.md) · [Custom events](../howto/custom-events.md) +Navigation: [Docs index](../README.md) · [Getting started](../GETTING_STARTED.md) · [API how-to](../howto/use-nostr-java-api.md) · [Streaming subscriptions](../howto/streaming-subscriptions.md) · [Custom events](../howto/custom-events.md) -This document provides an overview of the public API exposed by the `nostr-java` modules. It lists the major classes, configuration objects and their key method signatures, and shows brief examples of how to use them. Where applicable, links to related [Nostr Improvement Proposals (NIPs)](https://github.com/nostr-protocol/nips) are provided. +This document provides an overview of the public API exposed by the `nostr-java` modules. It lists the major classes, their key method signatures, and shows brief usage examples. -## Identity (`nostr-java-id`) +--- + +## Identity (`nostr-java-identity`) ### `Identity` -Represents a Nostr identity backed by a private key. It can derive a public key and sign `ISignable` objects. +Represents a Nostr identity backed by a private key. Derives the public key and signs `ISignable` objects. ```java public static Identity create(PrivateKey privateKey) @@ -19,134 +21,236 @@ public Signature sign(ISignable signable) **Usage:** ```java -Identity id = Identity.generateRandomIdentity(); -PublicKey pub = id.getPublicKey(); -Signature sig = id.sign(event); +Identity identity = Identity.generateRandomIdentity(); +PublicKey pub = identity.getPublicKey(); +identity.sign(event); ``` +--- + ## Event Model (`nostr-java-event`) -### Core Types -- `BaseMessage` – base class for all relay messages. -- `BaseEvent` – root class for Nostr events. -- `BaseTag` – helper for tag encoding and decoding. +### `GenericEvent` +The sole event class for all Nostr event kinds. Implements `ISignable`. + +```java +// Builder +public static GenericEventBuilder builder() + +// Fields +public String getId() +public PublicKey getPubKey() +public Long getCreatedAt() +public int getKind() +public List getTags() +public String getContent() +public Signature getSignature() + +// Kind classification +public boolean isReplaceable() +public boolean isEphemeral() +public boolean isAddressable() + +// Bech32 encoding +public String toBech32() +``` + +**Usage:** +```java +GenericEvent event = GenericEvent.builder() + .pubKey(identity.getPublicKey()) + .kind(Kinds.TEXT_NOTE) + .content("Hello Nostr!") + .tags(List.of(GenericTag.of("t", "nostr"))) + .build(); + +identity.sign(event); +``` + +### `GenericTag` +The sole tag class. A code and a list of string parameters. + +```java +// Factory methods +public static GenericTag of(String code, String... params) +public static GenericTag of(String code, List params) + +// Accessors +public String getCode() +public List getParams() +public List toArray() +``` + +**Usage:** +```java +GenericTag tag = GenericTag.of("e", "eventId123", "wss://relay.example.com", "reply"); +tag.getCode() // "e" +tag.getParams().get(0) // "eventId123" +tag.toArray() // ["e", "eventId123", "wss://relay.example.com", "reply"] +``` -### Predefined Events -The `nostr.event` package provides event implementations for many NIPs: +### `Kinds` +Static `int` constants for common event kinds plus range-check utilities. -| Class | NIP | -|-------|-----| -| `NIP01Event` | [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) – standard text notes. | -| `NIP04Event` | [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) – encrypted direct messages. | -| `NIP05Event` | [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md) – DNS identifiers. | -| `NIP09Event` | [NIP-09](https://github.com/nostr-protocol/nips/blob/master/09.md) – event deletion. | -| `NIP25Event` | [NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md) – reactions. | -| `NIP52Event` | [NIP-52](https://github.com/nostr-protocol/nips/blob/master/52.md) – calendar events. | -| `NIP99Event` | [NIP-99](https://github.com/nostr-protocol/nips/blob/master/99.md) – classified listings. | +```java +public static final int SET_METADATA = 0; +public static final int TEXT_NOTE = 1; +public static final int RECOMMEND_SERVER = 2; +public static final int CONTACT_LIST = 3; +public static final int ENCRYPTED_DIRECT_MESSAGE = 4; +public static final int DELETION = 5; +public static final int REPOST = 6; +public static final int REACTION = 7; +public static final int ZAP_REQUEST = 9734; +public static final int ZAP_RECEIPT = 9735; + +public static boolean isValid(int kind) +public static boolean isReplaceable(int kind) +public static boolean isEphemeral(int kind) +public static boolean isAddressable(int kind) +``` -### Filters -`Filters` and related `Filterable` implementations help build subscription requests. +### `EventFilter` +Builder-based composable filter for relay REQ messages. ```java -new Filters(Filterable... filterables) -List getFilterByType(String type) -void setLimit(Integer limit) +public static EventFilterBuilder builder() + +// Builder methods +.kinds(List kinds) +.authors(List authors) +.ids(List ids) +.since(long timestamp) +.until(long timestamp) +.limit(int limit) +.addTagFilter(String tagCode, List values) +.build() ``` **Usage:** ```java -Filters filters = new Filters(new AuthorFilter(pubKey)); -filters.setLimit(100); +EventFilter filter = EventFilter.builder() + .kinds(List.of(Kinds.TEXT_NOTE)) + .authors(List.of(pubKeyHex)) + .since(timestamp) + .limit(100) + .build(); ``` -## WebSocket Clients (`nostr-java-client`, `nostr-java-api`) +### `Filters` +Container for one or more `EventFilter` instances (OR logic for REQ messages). -### `WebSocketClientIF` -Abstraction over a WebSocket connection to a relay. +```java +public Filters(EventFilter... filters) +public Filters(Filterable... filterables) +``` + +### Messages + +| Class | Command | Purpose | +|-------|---------|---------| +| `EventMessage` | `EVENT` | Send or receive an event | +| `ReqMessage` | `REQ` | Subscribe to events matching filters | +| `CloseMessage` | `CLOSE` | Close a subscription | +| `OkMessage` | `OK` | Relay acknowledgment | +| `EoseMessage` | `EOSE` | End of stored events | +| `NoticeMessage` | `NOTICE` | Relay notice/error | ```java - List send(T eventMessage) throws IOException -List send(String json) throws IOException -AutoCloseable subscribe(String requestJson, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) throws IOException - AutoCloseable subscribe(T eventMessage, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) throws IOException -void close() throws IOException +// Encode a message +String json = new EventMessage(event).encode(); + +// Decode a message +BaseMessage msg = BaseMessage.read(json); ``` -### `StandardWebSocketClient` -Spring `TextWebSocketHandler` based implementation of `WebSocketClientIF`. +--- +## WebSocket Client (`nostr-java-client`) + +### `NostrRelayClient` +Spring `TextWebSocketHandler`-based WebSocket client with retry and Virtual Thread support. + +**Constructors:** +```java +public NostrRelayClient(String relayUri) +public NostrRelayClient(String relayUri, long awaitTimeoutMs) +``` + +**Blocking operations:** ```java -public StandardWebSocketClient(String relayUri) public List send(T eventMessage) throws IOException public List send(String json) throws IOException public AutoCloseable subscribe(String requestJson, Consumer messageListener, Consumer errorListener, Runnable closeListener) throws IOException +public AutoCloseable subscribe(T message, + Consumer messageListener, + Consumer errorListener, + Runnable closeListener) throws IOException public void close() throws IOException ``` -### `SpringWebSocketClient` -Wrapper that adds retry logic around a `WebSocketClientIF`. +**Async operations (Virtual Threads):** +```java +public static CompletableFuture connectAsync(String relayUri) +public static CompletableFuture connectAsync(String relayUri, long awaitTimeoutMs) +public CompletableFuture> sendAsync(String json) +public CompletableFuture> sendAsync(T eventMessage) +public CompletableFuture subscribeAsync(String requestJson, + Consumer messageListener, + Consumer errorListener, + Runnable closeListener) +``` +**Usage:** ```java -public List send(BaseMessage eventMessage) throws IOException -public List send(String json) throws IOException -public AutoCloseable subscribe(BaseMessage requestMessage, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) throws IOException -public AutoCloseable subscribe(String json, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) throws IOException -public List recover(IOException ex, String json) throws IOException -public void close() throws IOException +// Blocking +try (NostrRelayClient client = new NostrRelayClient("wss://relay.example.com")) { + List responses = client.send(new EventMessage(event)); +} + +// Async +NostrRelayClient.connectAsync("wss://relay.example.com") + .thenCompose(client -> client.sendAsync(new EventMessage(event))) + .thenAccept(responses -> System.out.println("Done: " + responses)); ``` -### `NostrSpringWebSocketClient` -High level client coordinating multiple relay connections and signing. +### `RelayTimeoutException` +Thrown when the relay does not respond within the configured timeout. Extends `IOException`. ```java -public NostrIF setRelays(Map relays) -public List sendEvent(IEvent event) -public List sendRequest(List filters, String subscriptionId) -public AutoCloseable subscribe(Filters filters, String subscriptionId, Consumer listener) -public AutoCloseable subscribe(Filters filters, - String subscriptionId, - Consumer listener, - Consumer errorListener) -public NostrIF sign(Identity identity, ISignable signable) -public boolean verify(GenericEvent event) -public Map getRelays() -public void close() +public String getRelayUri() +public long getTimeoutMs() ``` -See also the test guides for examples and behavioral expectations: +### `ConnectionState` +Enum tracking WebSocket connection state. -- API Client/Handler tests: `nostr-java-api/src/test/java/nostr/api/client/README.md` -- Client module (Spring WebSocket): `nostr-java-client/src/test/java/nostr/client/springwebsocket/README.md` +```java +CONNECTING, CONNECTED, RECONNECTING, CLOSED +``` -`subscribe` opens a dedicated WebSocket per relay, returns immediately, and streams raw relay -messages to the provided listener. The returned `AutoCloseable` sends a `CLOSE` command and releases -resources when invoked. Because callbacks execute on the WebSocket thread, delegate heavy -processing to another executor to avoid stalling inbound traffic. +### Configuration -- How‑to guide: [../howto/streaming-subscriptions.md](../howto/streaming-subscriptions.md) -- Example: [../../nostr-java-examples/src/main/java/nostr/examples/SpringSubscriptionExample.java](../../nostr-java-examples/src/main/java/nostr/examples/SpringSubscriptionExample.java) +| Property | Default | Description | +|----------|---------|-------------| +| `nostr.websocket.await-timeout-ms` | `60000` | Max time to await a relay response | +| `nostr.websocket.max-idle-timeout-ms` | `3600000` | Max idle timeout for WebSocket sessions | +| `nostr.websocket.max-text-message-buffer-size` | `1048576` | WebSocket text message buffer size | +| `nostr.websocket.max-binary-message-buffer-size` | `1048576` | WebSocket binary message buffer size | -### Configuration -- `RetryConfig` – enables Spring Retry support. -- `RelaysProperties` – maps relay names to URLs via configuration properties. -- `RelayConfig` – loads `relays.properties` and exposes a `Map` bean. Deprecated in 0.6.2 (for removal in 1.0.0); prefer `RelaysProperties`. +### Retry behavior -## Encryption and Cryptography +Send and subscribe operations are annotated with `@NostrRetryable`: +- Included exception: `IOException` +- Max attempts: `3` +- Backoff: initial `500ms`, multiplier `2.0` + +--- + +## Encryption (`nostr-java-identity`) ### `MessageCipher` Strategy interface for message encryption. @@ -157,11 +261,15 @@ String decrypt(String message) ``` Implementations: -- `MessageCipher04` – NIP-04 direct message encryption. -- `MessageCipher44` – NIP-44 payload encryption. +- `MessageCipher04` — NIP-04 direct message encryption (legacy). +- `MessageCipher44` — NIP-44 versioned encryption (recommended). + +--- + +## Cryptography (`nostr-java-core`) ### `Schnorr` -Utility for Schnorr signatures (BIP-340). +BIP-340 Schnorr signature utility. ```java static byte[] sign(byte[] msg, byte[] secKey, byte[] auxRand) @@ -171,17 +279,15 @@ static byte[] genPubKey(byte[] secKey) ``` ### `Bech32` -Utility for Bech32/Bech32m encoding used by [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md). +Bech32/Bech32m encoding for NIP-19. ```java static String toBech32(Bech32Prefix hrp, byte[] hexKey) static String fromBech32(String str) ``` -## Utilities (`nostr-java-util`) - ### `NostrUtil` -General helper functions. +General helper functions using `java.util.HexFormat`. ```java static String bytesToHex(byte[] bytes) @@ -190,35 +296,34 @@ static byte[] sha256(byte[] data) static byte[] createRandomByteArray(int len) ``` -### `NostrException` -Base checked exception for utility methods. +--- + +## Key Types (`nostr-java-event`) -## Examples +### `PublicKey` +Nostr public key value object with Bech32 encoding (`npub` prefix). -### Send a Text Note (NIP-01) ```java -Identity id = Identity.generateRandomIdentity(); -NIP01 nip01 = new NIP01(id).createTextNoteEvent("Hello Nostr"); -NostrIF client = NostrSpringWebSocketClient.getInstance(id) - .setRelays(Map.of("398ja","wss://relay.398ja.xyz")); -client.sendEvent(nip01.getEvent()); +public PublicKey(String hex) +public String toBech32() +public String toString() // hex representation ``` -### Encrypted Direct Message (NIP-04) +### `PrivateKey` +Nostr private key value object with Bech32 encoding (`nsec` prefix). + ```java -Identity alice = Identity.generateRandomIdentity(); -Identity bob = Identity.generateRandomIdentity(); -NIP04 dm = new NIP04(alice, bob.getPublicKey()) - .createDirectMessageEvent("secret"); -String plaintext = NIP04.decrypt(bob, dm.getEvent()); +public PrivateKey(String hex) +public String toBech32() ``` -### Subscription with Filters +### `Signature` +BIP-340 Schnorr signature value object. + ```java -Filters filters = new Filters(new AuthorFilter(pubKey)); -NostrIF client = NostrSpringWebSocketClient.getInstance(id); -List events = client.sendRequest(filters, "sub-id"); +public Signature(String hex) ``` --- -This reference is a starting point; consult the source for complete details and additional NIP helpers. + +This reference is a starting point; consult the source for complete details. diff --git a/nostr-java-api/pom.xml b/nostr-java-api/pom.xml deleted file mode 100644 index be0f6ccc3..000000000 --- a/nostr-java-api/pom.xml +++ /dev/null @@ -1,127 +0,0 @@ - - 4.0.0 - - - xyz.tcheeric - nostr-java - 1.3.0 - ../pom.xml - - - nostr-java-api - jar - nostr-java-api - - - - reposilite-releases - https://maven.398ja.xyz/releases - - - reposilite-snapshots - https://maven.398ja.xyz/snapshots - - - - - - org.apache.commons - commons-text - - - - ${project.groupId} - nostr-java-client - - - - ${project.groupId} - nostr-java-encryption - - - - ${project.groupId} - nostr-java-id - - - - ${project.groupId} - nostr-java-event - - - - ${project.groupId} - nostr-java-util - - - - ${project.groupId} - nostr-java-crypto - - - - org.springframework.boot - spring-boot-starter - - - - org.springframework.boot - spring-boot-starter-logging - - - - - com.fasterxml.jackson.core - jackson-databind - - - org.projectlombok - lombok - - provided - - - org.slf4j - slf4j-api - - - org.testcontainers - junit-jupiter - test - - - org.springframework.boot - spring-boot-starter-test - - test - - - ch.qos.logback - logback-classic - - - - - com.google.guava - guava - - test - - - org.junit.jupiter - junit-jupiter - test - - - org.junit.platform - junit-platform-launcher - test - - - com.github.valfirst - slf4j-test - 3.0.3 - test - - - diff --git a/nostr-java-api/src/main/java/nostr/api/EventNostr.java b/nostr-java-api/src/main/java/nostr/api/EventNostr.java deleted file mode 100644 index ec4a5947b..000000000 --- a/nostr-java-api/src/main/java/nostr/api/EventNostr.java +++ /dev/null @@ -1,126 +0,0 @@ -package nostr.api; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.Setter; -import nostr.base.PublicKey; -import nostr.event.BaseMessage; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.id.Identity; -import org.apache.commons.lang3.stream.Streams.FailableStream; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Base helper for building, signing, and sending Nostr events over WebSocket. - */ -@Getter -@NoArgsConstructor -public abstract class EventNostr extends NostrSpringWebSocketClient { - - @Setter private GenericEvent event; - - private PublicKey recipient; - - public EventNostr(@NonNull Identity sender) { - super(sender); - } - - /** - * Sign the currently built event with the configured sender. - * - * @return this instance for chaining - */ - public EventNostr sign() { - super.sign(getSender(), event); - return this; - } - - /** - * Send the current event to the configured relays and return the first response message. - */ - public U send() { - return this.send(getRelays()); - } - - /** - * Send the current event to the provided relays and return the first response message. - * - * @param relays relay map (name -> URI) - */ - public U send(Map relays) { - List messages = super.sendEvent(this.event, relays); - BaseMessageDecoder decoder = new BaseMessageDecoder<>(); - - return new FailableStream<>(messages.stream()) - .map(msg -> (U) decoder.decode(msg)).filter(Objects::nonNull).stream() - .findFirst() - .orElseThrow(() -> new RuntimeException("No message received")); - } - - /** - * Sign and send the current event using the configured relays. - */ - public U signAndSend() { - return this.signAndSend(getRelays()); - } - - /** - * Sign and send the current event using the provided relays. - * - * @param relays relay map (name -> URI) - */ - public U signAndSend(Map relays) { - return sign().send(relays); - } - - /** - * Set the sender identity used for signing events. - */ - public EventNostr setSender(@NonNull Identity sender) { - super.setSender(sender); - return this; - } - - /** - * Set the relays used when sending the current event. - */ - public EventNostr setRelays(@NonNull Map relays) { - super.setRelays(relays); - return this; - } - - /** - * Set the recipient public key (used by DMs or recipient-specific flows). - */ - public EventNostr setRecipient(@NonNull PublicKey recipient) { - this.recipient = recipient; - return this; - } - - /** - * Replace the current event object and refresh its derived fields. - * - * @param event the new event instance - */ - public void updateEvent(@NonNull GenericEvent event) { - this.setEvent(event); - this.event.update(); - } - - /** - * Add a tag to the current event. - * - * @param tag the tag to add - * @return this instance for chaining - */ - public EventNostr addTag(@NonNull BaseTag tag) { - getEvent().addTag(tag); - return this; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP01.java b/nostr-java-api/src/main/java/nostr/api/NIP01.java deleted file mode 100644 index cbef29ee9..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP01.java +++ /dev/null @@ -1,451 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.nip01.NIP01EventBuilder; -import nostr.api.nip01.NIP01MessageFactory; -import nostr.api.nip01.NIP01TagFactory; -import nostr.base.Marker; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.entities.UserProfile; -import nostr.event.filter.Filters; -import nostr.event.impl.GenericEvent; -import nostr.event.message.CloseMessage; -import nostr.event.message.EoseMessage; -import nostr.event.message.EventMessage; -import nostr.event.message.NoticeMessage; -import nostr.event.message.ReqMessage; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; - -import java.util.List; -import java.util.Optional; - -/** - * Facade for NIP-01 (Basic Protocol Flow) - the fundamental building blocks of Nostr. - * - *

NIP-01 defines the core protocol for creating, signing, and transmitting events over - * Nostr relays. This class provides a high-level API for working with basic event types, - * tags, and messages without needing to understand the underlying implementation details. - * - *

What is NIP-01? - *

    - *
  • Event Structure: Defines the JSON format for events (id, pubkey, created_at, - * kind, tags, content, sig)
  • - *
  • Event Kinds: Basic kinds like text notes (1), metadata (0), contacts (3)
  • - *
  • Event Types: Regular, replaceable, ephemeral, and addressable events
  • - *
  • Tags: Standard tags like 'e' (event reference), 'p' (public key), 'd' (identifier)
  • - *
  • Messages: Client-relay communication (EVENT, REQ, CLOSE, EOSE, NOTICE)
  • - *
- * - *

Design Pattern: This class uses the Facade Pattern to hide the complexity of: - *

    - *
  • {@link NIP01EventBuilder} - Event construction logic
  • - *
  • {@link NIP01TagFactory} - Tag creation logic
  • - *
  • {@link NIP01MessageFactory} - Message formatting logic
  • - *
- * - *

Usage Example: - *

{@code
- * // Create NIP01 instance with sender identity
- * Identity identity = new Identity(privateKey);
- * NIP01 nip01 = new NIP01(identity);
- *
- * // Create and send a simple text note
- * nip01.createTextNoteEvent("Hello Nostr!")
- *      .sign()
- *      .send(relayUri);
- *
- * // Create a text note with tags
- * List tags = List.of(
- *     NIP01.createEventTag("event_id_hex", Marker.REPLY),
- *     NIP01.createPubKeyTag(recipientPublicKey)
- * );
- * nip01.createTextNoteEvent(tags, "Hello @recipient!")
- *      .sign()
- *      .send(relayUri);
- *
- * // Create metadata event
- * UserProfile profile = UserProfile.builder()
- *     .name("Alice")
- *     .about("Nostr enthusiast")
- *     .picture("https://example.com/avatar.jpg")
- *     .build();
- * nip01.createMetadataEvent(profile)
- *      .sign()
- *      .send(relayUri);
- *
- * // Create static tags and messages (without sender)
- * BaseTag eventTag = NIP01.createEventTag("event_id");
- * BaseTag pubKeyTag = NIP01.createPubKeyTag(publicKey);
- * ReqMessage reqMsg = NIP01.createReqMessage("sub_id", List.of(filters));
- * }
- * - *

Event Types Supported: - *

    - *
  • Text Notes: {@link #createTextNoteEvent(String)} - Basic short-form content (kind 1)
  • - *
  • Metadata: {@link #createMetadataEvent(UserProfile)} - User profile data (kind 0)
  • - *
  • Replaceable: {@link #createReplaceableEvent(Integer, String)} - Latest replaces earlier
  • - *
  • Ephemeral: {@link #createEphemeralEvent(Integer, String)} - Not stored by relays
  • - *
  • Addressable: {@link #createAddressableEvent(Integer, String)} - Replaceable with identifier
  • - *
- * - *

Tag Types Supported: - *

    - *
  • Event tags (e): {@link #createEventTag(String)} - References to other events
  • - *
  • Public key tags (p): {@link #createPubKeyTag(PublicKey)} - References to users
  • - *
  • Identifier tags (d): {@link #createIdentifierTag(String)} - For addressable events
  • - *
  • Address tags (a): {@link #createAddressTag(Integer, PublicKey, String)} - Addressable event refs
  • - *
- * - *

Message Types Supported: - *

    - *
  • EVENT: {@link #createEventMessage(GenericEvent, String)} - Publish events
  • - *
  • REQ: {@link #createReqMessage(String, List)} - Subscribe to events
  • - *
  • CLOSE: {@link #createCloseMessage(String)} - Unsubscribe
  • - *
  • EOSE: {@link #createEoseMessage(String)} - End of stored events
  • - *
  • NOTICE: {@link #createNoticeMessage(String)} - Human-readable messages
  • - *
- * - *

Method Chaining: This class supports fluent API style: - *

{@code
- * nip01.createTextNoteEvent("Hello World")  // Create event
- *      .sign()                               // Sign with sender's private key
- *      .send(relayUri)                       // Send to relay
- *      .get();                               // Get response
- * }
- * - *

Sender Management: The sender identity can be set at construction or changed later: - *

{@code
- * NIP01 nip01 = new NIP01(identity);  // Set sender at construction
- * nip01.setSender(newIdentity);        // Change sender later
- * }
- * - *

Migration Note: Version 0.6.2 deprecated methods that accept Identity parameters - * in favor of using the configured sender. Those overloads have been removed in 1.0.0. - * - *

Thread Safety: This class is not thread-safe. Each thread should use its own instance. - * - * @see NIP01EventBuilder - * @see NIP01TagFactory - * @see NIP01MessageFactory - * @see NIP-01 Specification - * @since 0.1.0 - */ -public class NIP01 extends EventNostr { - - private final NIP01EventBuilder eventBuilder; - - public NIP01(Identity sender) { - super(sender); - this.eventBuilder = new NIP01EventBuilder(sender); - } - - @Override - public NIP01 setSender(@NonNull Identity sender) { - super.setSender(sender); - this.eventBuilder.updateDefaultSender(sender); - return this; - } - - /** - * Create a NIP01 text note event without tags. - * - * @param content the content of the note - * @return the text note without tags - */ - public NIP01 createTextNoteEvent(String content) { - this.updateEvent(eventBuilder.buildTextNote(content)); - return this; - } - - - - // Removed deprecated overload accepting Identity. Use instance sender instead. - - /** - * Create a NIP01 text note event addressed to specific recipients using the configured sender. - * - * @param content the content of the note - * @param recipients the list of {@code p} tags identifying recipients' public keys - * @return this instance for chaining - */ - public NIP01 createTextNoteEvent(String content, List recipients) { - this.updateEvent(eventBuilder.buildRecipientTextNote(content, recipients)); - return this; - } - - /** - * Create a NIP01 text note event with recipients. - * - * @param tags the tags - * @param content the content of the note - * @return a text note event - */ - public NIP01 createTextNoteEvent(@NonNull List tags, @NonNull String content) { - this.updateEvent(eventBuilder.buildTaggedTextNote(tags, content)); - return this; - } - - public NIP01 createMetadataEvent(@NonNull UserProfile profile) { - GenericEvent genericEvent = - Optional.ofNullable(getSender()) - .map(identity -> eventBuilder.buildMetadataEvent(identity, profile.toString())) - .orElse(eventBuilder.buildMetadataEvent(profile.toString())); - this.updateEvent(genericEvent); - return this; - } - - /** - * Create a replaceable event. - * - * @param kind the kind (10000 <= kind < 20000 || kind == 0 || kind == 3) - * @param content the content - */ - public NIP01 createReplaceableEvent(Integer kind, String content) { - this.updateEvent(eventBuilder.buildReplaceableEvent(kind, content)); - return this; - } - - /** - * Create a replaceable event. - * - * @param tags the note's tags - * @param kind the kind (10000 <= kind < 20000 || kind == 0 || kind == 3) - * @param content the note's content - */ - public NIP01 createReplaceableEvent(List tags, Integer kind, String content) { - this.updateEvent(eventBuilder.buildReplaceableEvent(tags, kind, content)); - return this; - } - - /** - * Create an ephemeral event. - * - * @param kind the kind (20000 <= n < 30000) - * @param tags the note's tags - * @param content the note's content - */ - public NIP01 createEphemeralEvent(List tags, Integer kind, String content) { - this.updateEvent(eventBuilder.buildEphemeralEvent(tags, kind, content)); - return this; - } - - /** - * Create an ephemeral event. - * - * @param kind the kind (20000 <= n < 30000) - * @param content the note's content - */ - public NIP01 createEphemeralEvent(Integer kind, String content) { - this.updateEvent(eventBuilder.buildEphemeralEvent(kind, content)); - return this; - } - - /** - * Create an addressable event (A-event as defined by NIP-33). - * - * @param kind the event kind (replaceable/addressable kinds per NIP-33) - * @param content the event's content/comment - * @return this instance for chaining - */ - public NIP01 createAddressableEvent(Integer kind, String content) { - this.updateEvent(eventBuilder.buildAddressableEvent(kind, content)); - return this; - } - - /** - * Create an addressable event (A-event as defined by NIP-33). - * - * @param tags additional tags to attach to the event (e.g., identifier/address tags) - * @param kind the event kind (replaceable/addressable kinds per NIP-33) - * @param content the event's content/comment - * @return this instance for chaining - */ - public NIP01 createAddressableEvent( - @NonNull List tags, @NonNull Integer kind, String content) { - this.updateEvent(eventBuilder.buildAddressableEvent(tags, kind, content)); - return this; - } - - /** - * Create a NIP01 event tag. - * - * @param relatedEventId the related event id - * @return an event tag with the id of the related event - */ - public static BaseTag createEventTag(@NonNull String relatedEventId) { - return NIP01TagFactory.eventTag(relatedEventId); - } - - /** - * Create a NIP01 event tag with additional recommended relay and marker. - * - * @param idEvent the related event id - * @param recommendedRelayUrl the recommended relay url - * @param marker the marker - * @return an event tag with the id of the related event and optional recommended relay and marker - */ - public static BaseTag createEventTag( - @NonNull String idEvent, String recommendedRelayUrl, Marker marker) { - return NIP01TagFactory.eventTag(idEvent, recommendedRelayUrl, marker); - } - - /** - * Create a NIP01 event tag with additional recommended relay and marker. - * - * @param idEvent the related event id - * @param marker the marker - * @return an event tag with the id of the related event and optional recommended relay and marker - */ - public static BaseTag createEventTag(@NonNull String idEvent, Marker marker) { - return NIP01TagFactory.eventTag(idEvent, marker); - } - - /** - * Create a NIP01 event tag with additional recommended relay and marker. - * - * @param idEvent the related event id - * @param recommendedRelay the recommended relay - * @param marker the marker - * @return an event tag with the id of the related event and optional recommended relay and marker - */ - public static BaseTag createEventTag( - @NonNull String idEvent, Relay recommendedRelay, Marker marker) { - return NIP01TagFactory.eventTag(idEvent, recommendedRelay, marker); - } - - /** - * Create a NIP01 pubkey tag. - * - * @param publicKey the associated public key - * @return a pubkey tag with the hex representation of the associated public key - */ - public static BaseTag createPubKeyTag(@NonNull PublicKey publicKey) { - return NIP01TagFactory.pubKeyTag(publicKey); - } - - /** - * Create a NIP01 pubkey tag with additional recommended relay and petname (as defined in NIP02). - * - * @param publicKey the associated public key - * @param mainRelayUrl the recommended relay - * @param petName the petname - * @return a pubkey tag with the hex representation of the associated public key and the optional - * recommended relay and petname - */ - public static BaseTag createPubKeyTag( - @NonNull PublicKey publicKey, String mainRelayUrl, String petName) { - return NIP01TagFactory.pubKeyTag(publicKey, mainRelayUrl, petName); - } - - /** - * Create a NIP01 pubkey tag with additional recommended relay and petname (as defined in NIP02). - * - * @param publicKey the associated public key - * @param mainRelayUrl the recommended relay - * @return a pubkey tag with the hex representation of the associated public key and the optional - * recommended relay and petname - */ - public static BaseTag createPubKeyTag(@NonNull PublicKey publicKey, String mainRelayUrl) { - return NIP01TagFactory.pubKeyTag(publicKey, mainRelayUrl); - } - - /** - * Create a NIP01 identifier tag ({@code d}-tag). - * - * @param id the identifier value for replaceable/addressable events (NIP-33) - * @return the created identifier tag - */ - public static BaseTag createIdentifierTag(@NonNull String id) { - return NIP01TagFactory.identifierTag(id); - } - - /** - * Create an address tag ({@code a}-tag) as defined in NIP-33. - * - * @param kind the target event kind (e.g., replaceable/addressable kind) - * @param publicKey the author public key of the addressed event - * @param idTag an optional {@code d}-tag (identifier) for the addressed event - * @param relay an optional recommended relay URL for the addressed event - * @return the created address tag - */ - public static BaseTag createAddressTag( - @NonNull Integer kind, @NonNull PublicKey publicKey, BaseTag idTag, Relay relay) { - return NIP01TagFactory.addressTag(kind, publicKey, idTag, relay); - } - - public static BaseTag createAddressTag( - @NonNull Integer kind, @NonNull PublicKey publicKey, String id, Relay relay) { - return NIP01TagFactory.addressTag(kind, publicKey, id, relay); - } - - /** - * Create an address tag ({@code a}-tag) referencing an addressable event (NIP-33). - * - * @param kind the event kind - * @param publicKey the author public key of the addressed event - * @param id the identifier ({@code d}-tag value) - * @return the created address tag - */ - public static BaseTag createAddressTag( - @NonNull Integer kind, @NonNull PublicKey publicKey, String id) { - return NIP01TagFactory.addressTag(kind, publicKey, id); - } - - /** - * Create an event message to send events requested by clients. - * - * @param event the related event - * @param subscriptionId the related subscription id - * @return an event message - */ - public static EventMessage createEventMessage(@NonNull GenericEvent event, String subscriptionId) { - return NIP01MessageFactory.eventMessage(event, subscriptionId); - } - - /** - * Create a REQ message to request events and subscribe to new updates. - * - * @param subscriptionId the subscription id - * @param filtersList the filters list - * @return a REQ message - */ - public static ReqMessage createReqMessage( - @NonNull String subscriptionId, @NonNull List filtersList) { - return NIP01MessageFactory.reqMessage(subscriptionId, filtersList); - } - - /** - * Create a CLOSE message to stop previous subscriptions. - * - * @param subscriptionId the subscription id - * @return a CLOSE message - */ - public static CloseMessage createCloseMessage(@NonNull String subscriptionId) { - return NIP01MessageFactory.closeMessage(subscriptionId); - } - - /** - * Create an EOSE message to indicate the end of stored events and the beginning of events newly - * received in real-time. - * - * @param subscriptionId the subscription id - * @return an EOSE message - */ - public static EoseMessage createEoseMessage(@NonNull String subscriptionId) { - return NIP01MessageFactory.eoseMessage(subscriptionId); - } - - /** - * Create a NOTICE message to send human-readable error messages or other things to clients. - * - * @param message the human-readable message to send to the client - * @return a NOTICE message - */ - public static NoticeMessage createNoticeMessage(@NonNull String message) { - return NIP01MessageFactory.noticeMessage(message); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP02.java b/nostr-java-api/src/main/java/nostr/api/NIP02.java deleted file mode 100644 index 0be351ca5..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP02.java +++ /dev/null @@ -1,58 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -import java.util.List; - -/** - * NIP-02 helpers (Contact List). Create and manage kind 3 contact lists and p-tags. - * Spec: NIP-02 - */ -public class NIP02 extends EventNostr { - - public NIP02(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create a contact list event (kind 3) as defined by NIP-02. - * - * @param pubKeyTags the list of {@code p} tags representing contacts and optional relay/petname - * @return this instance for chaining - */ - @SuppressWarnings("rawtypes") - public NIP02 createContactListEvent(List pubKeyTags) { - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.CONTACT_LIST.getValue(), pubKeyTags, "").create(); - updateEvent(genericEvent); - return this; - } - - /** - * Add a pubkey tag to the contact list event - * - * @param tag the pubkey tag - */ - public NIP02 addContactTag(@NonNull BaseTag tag) { - if (!(tag instanceof nostr.event.tag.PubKeyTag)) { - throw new IllegalArgumentException("Tag must be a pubkey tag"); - } - getEvent().addTag(tag); - return this; - } - - /** - * Add a pubkey tag to the contact list event - * - * @param publicKey the public key to add to the contact list - */ - public NIP02 addContactTag(@NonNull PublicKey publicKey) { - return addContactTag(NIP01.createPubKeyTag(publicKey)); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP03.java b/nostr-java-api/src/main/java/nostr/api/NIP03.java deleted file mode 100644 index 84855299e..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP03.java +++ /dev/null @@ -1,37 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -/** - * NIP-03 helpers (OpenTimestamps Attestations). Create OTS attestation events. - * Spec: NIP-03 - */ -public class NIP03 extends EventNostr { - - public NIP03(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create a NIP03 OTS event - * - * @param referencedEvent the referenced event - * @param ots the full content of an .ots file containing at least one Bitcoin attestation - * @param alt the note's content - * @return an OTS event - */ - public NIP03 createOtsEvent( - @NonNull GenericEvent referencedEvent, @NonNull String ots, @NonNull String alt) { - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.OTS_EVENT.getValue(), ots).create(); - genericEvent.addTag(NIP31.createAltTag(alt)); - genericEvent.addTag(NIP01.createEventTag(referencedEvent.getId())); - this.updateEvent(genericEvent); - - return this; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP04.java b/nostr-java-api/src/main/java/nostr/api/NIP04.java deleted file mode 100644 index 5e01ca083..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP04.java +++ /dev/null @@ -1,403 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.encryption.MessageCipher; -import nostr.encryption.MessageCipher04; -import nostr.event.BaseTag; -import nostr.event.filter.Filterable; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; - -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Optional; - -/** - * NIP-04: Encrypted Direct Messages. - * - *

This class provides utilities for creating, encrypting, and decrypting private direct messages - * (DMs) on the Nostr protocol. NIP-04 uses AES-256-CBC encryption with a shared secret derived from - * ECDH (Elliptic Curve Diffie-Hellman) key agreement. - * - *

What is NIP-04?

- * - *

NIP-04 defines encrypted direct messages as kind-4 events where: - *

    - *
  • The content is encrypted using AES-256-CBC
  • - *
  • The encryption key is derived from ECDH between sender and recipient
  • - *
  • A 'p' tag indicates the recipient's public key
  • - *
  • The encrypted content format is: base64(ciphertext)?iv=base64(initialization_vector)
  • - *
- * - *

Security Note

- * - *

NIP-04 is deprecated for new applications. Use NIP-44 instead, which provides: - *

    - *
  • Better encryption scheme (XChaCha20-Poly1305)
  • - *
  • Authenticated encryption (AEAD)
  • - *
  • Protection against padding oracle attacks
  • - *
  • No metadata leakage through message length
  • - *
- * - *

NIP-04 is maintained for backward compatibility with existing clients and messages. - * - *

Usage Examples

- * - *

Example 1: Send an Encrypted DM

- *
{@code
- * Identity sender = new Identity("nsec1...");
- * PublicKey recipient = new PublicKey("npub1...");
- *
- * NIP04 nip04 = new NIP04(sender, recipient);
- * nip04.createDirectMessageEvent("Hello! This is a private message.")
- *      .sign()
- *      .send(relays);
- * }
- * - *

Example 2: Decrypt a Received DM (as recipient)

- *
{@code
- * Identity myIdentity = new Identity("nsec1...");
- * GenericEvent dmEvent = ... // received from relay (kind 4)
- *
- * String plaintext = NIP04.decrypt(myIdentity, dmEvent);
- * System.out.println("Received: " + plaintext);
- * }
- * - *

Example 3: Decrypt Your Own Sent DM

- *
{@code
- * Identity myIdentity = new Identity("nsec1...");
- * GenericEvent myDmEvent = ... // a DM I sent (kind 4)
- *
- * // Works for both sender and recipient
- * String plaintext = NIP04.decrypt(myIdentity, myDmEvent);
- * System.out.println("I sent: " + plaintext);
- * }
- * - *

Example 4: Standalone Encrypt/Decrypt

- *
{@code
- * Identity sender = new Identity("nsec1...");
- * PublicKey recipient = new PublicKey("npub1...");
- *
- * // Encrypt a message
- * String encrypted = NIP04.encrypt(sender, "Secret message", recipient);
- *
- * // Decrypt it (either party can decrypt with their private key + other's public key)
- * String decrypted = NIP04.decrypt(sender, encrypted, recipient);
- * }
- * - *

Design Pattern

- * - *

This class follows the Facade Pattern, providing a simplified interface for: - *

    - *
  • Event creation (delegates to {@code GenericEventFactory})
  • - *
  • Encryption (delegates to {@code MessageCipher04})
  • - *
  • Decryption (delegates to {@code MessageCipher04})
  • - *
  • Event signing and sending (inherited from {@code EventNostr})
  • - *
- * - *

How Encryption Works

- * - *
    - *
  1. Key Agreement: ECDH produces a shared secret from sender's private key + recipient's public key
  2. - *
  3. IV Generation: A random 16-byte initialization vector is generated
  4. - *
  5. Encryption: AES-256-CBC encrypts the plaintext message
  6. - *
  7. Format: Output is base64(ciphertext)?iv=base64(iv)
  8. - *
- * - *

Known Limitations

- * - *
    - *
  • No authentication: Vulnerable to tampering (use NIP-44 for AEAD)
  • - *
  • Padding oracle risk: CBC mode can leak info through padding errors
  • - *
  • Metadata leakage: Message length is visible (NIP-44 pads to fixed sizes)
  • - *
  • Replay attacks: No nonce/counter mechanism
  • - *
- * - *

Thread Safety

- * - *

This class is not thread-safe for instance methods. Each thread should create - * its own {@code NIP04} instance. The static {@code encrypt()} and {@code decrypt()} methods are - * thread-safe. - * - * @see NIP-04 Specification - * @see NIP44 - * @see nostr.encryption.MessageCipher04 - * @since 0.1.0 - */ -@Slf4j -public class NIP04 extends EventNostr { - /** - * Construct a NIP-04 helper for encrypting/sending DMs. - * - * @param sender the sender identity used for signing and encryption - * @param recipient the recipient public key - */ - public NIP04(@NonNull Identity sender, @NonNull PublicKey recipient) { - setSender(sender); - setRecipient(recipient); - } - - /** - * Create a NIP-04 encrypted direct message event (kind 4). - * - *

This method: - *

    - *
  1. Encrypts the plaintext content using AES-256-CBC
  2. - *
  3. Adds a 'p' tag with the recipient's public key
  4. - *
  5. Creates a kind-4 event with the encrypted content
  6. - *
  7. Stores the event in this instance for signing/sending
  8. - *
- * - *

The event is NOT signed or sent automatically. Chain with {@code .sign()} and - * {@code .send(relays)} to complete the operation. - * - *

Example: - *

{@code
-   * NIP04 nip04 = new NIP04(senderIdentity, recipientPubKey);
-   * nip04.createDirectMessageEvent("Hello, this is private!")
-   *      .sign()
-   *      .send(relays);
-   * }
- * - * @param content the plaintext message to encrypt and send - * @return this instance for method chaining - */ - @SuppressWarnings({"rawtypes","unchecked"}) - public NIP04 createDirectMessageEvent(@NonNull String content) { - log.debug("Creating direct message event"); - var encryptedContent = encrypt(getSender(), content, getRecipient()); - List tags = List.of(new PubKeyTag(getRecipient())); - - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), Kind.ENCRYPTED_DIRECT_MESSAGE.getValue(), tags, encryptedContent) - .create(); - this.updateEvent(genericEvent); - - return this; - } - - /** - * Encrypt the content of the current event (must be a kind-4 event). - * - *

This method encrypts the plaintext content stored in the current event using NIP-04 - * encryption. It extracts the recipient from the 'p' tag and uses AES-256-CBC encryption. - * - *

Note: This is only needed if you manually created an event. The - * {@link #createDirectMessageEvent(String)} method already encrypts the content automatically. - * - * @return this instance for method chaining - * @throws IllegalArgumentException if the event is not kind 4 - * @throws NoSuchElementException if no 'p' tag is found in the event - */ - public NIP04 encrypt() { - encryptDirectMessage(getSender(), getEvent()); - return this; - } - - /** - * Encrypt a plaintext message using NIP-04 encryption (AES-256-CBC + ECDH). - * - *

This is a standalone utility method for encrypting messages without creating a full event. - * The encryption process: - *

    - *
  1. Derives a shared secret using ECDH (sender's private key + recipient's public key)
  2. - *
  3. Generates a random 16-byte initialization vector (IV)
  4. - *
  5. Encrypts the message using AES-256-CBC
  6. - *
  7. Returns: base64(ciphertext)?iv=base64(iv)
  8. - *
- * - *

Example: - *

{@code
-   * Identity alice = new Identity("nsec1...");
-   * PublicKey bob = new PublicKey("npub1...");
-   *
-   * String encrypted = NIP04.encrypt(alice, "Hello Bob!", bob);
-   * // Returns something like: "SGVsbG8gQm9iIQ==?iv=randomBase64IV=="
-   * }
- * - * @param senderId the sender's identity (contains private key for ECDH) - * @param message the plaintext message to encrypt - * @param recipient the recipient's public key - * @return the encrypted message in NIP-04 format: base64(ciphertext)?iv=base64(iv) - */ - public static String encrypt( - @NonNull Identity senderId, @NonNull String message, @NonNull PublicKey recipient) { - log.debug("Encrypting message from {} to {}", senderId.getPublicKey(), recipient); - MessageCipher cipher = - new MessageCipher04(senderId.getPrivateKey().getRawData(), recipient.getRawData()); - return cipher.encrypt(message); - } - - /** - * Decrypt an encrypted message using NIP-04 decryption (AES-256-CBC + ECDH). - * - *

This is a standalone utility method for decrypting NIP-04 encrypted messages. Either party - * (sender or recipient) can decrypt the message by providing their own private key and the other - * party's public key. - * - *

The decryption process: - *

    - *
  1. Parses the encrypted format: base64(ciphertext)?iv=base64(iv)
  2. - *
  3. Derives the same shared secret using ECDH
  4. - *
  5. Decrypts using AES-256-CBC with the extracted IV
  6. - *
  7. Returns the plaintext message
  8. - *
- * - *

Example: - *

{@code
-   * Identity bob = new Identity("nsec1...");
-   * PublicKey alice = new PublicKey("npub1...");
-   *
-   * String encrypted = "SGVsbG8gQm9iIQ==?iv=randomBase64IV==";
-   * String plaintext = NIP04.decrypt(bob, encrypted, alice);
-   * // Returns: "Hello Bob!"
-   * }
- * - * @param identity the identity of the party decrypting (sender or recipient) - * @param encryptedMessage the encrypted message in NIP-04 format - * @param recipient the public key of the other party (if you're the sender, this is the recipient; if you're the recipient, this is the sender) - * @return the decrypted plaintext message - */ - public static String decrypt( - @NonNull Identity identity, @NonNull String encryptedMessage, @NonNull PublicKey recipient) { - log.debug("Decrypting message for {}", identity.getPublicKey()); - MessageCipher cipher = - new MessageCipher04(identity.getPrivateKey().getRawData(), recipient.getRawData()); - return cipher.decrypt(encryptedMessage); - } - - private static void encryptDirectMessage( - @NonNull Identity senderId, @NonNull GenericEvent directMessageEvent) { - - if (directMessageEvent.getKind() != Kind.ENCRYPTED_DIRECT_MESSAGE.getValue()) { - throw new IllegalArgumentException("Event is not an encrypted direct message"); - } - - GenericTag recipient = - directMessageEvent.getTags().stream() - .filter(t -> t.getCode().equalsIgnoreCase("p")) - .map(tag -> (GenericTag) tag) - .findFirst() - .orElseThrow(() -> new NoSuchElementException("No matching p-tag found.")); - - PubKeyTag pubKeyTag = PubKeyTag.updateFields(recipient); - PublicKey rcptPublicKey = pubKeyTag.getPublicKey(); - MessageCipher cipher = - new MessageCipher04(senderId.getPrivateKey().getRawData(), rcptPublicKey.getRawData()); - var encryptedContent = cipher.encrypt(directMessageEvent.getContent()); - directMessageEvent.setContent(encryptedContent); - } - - /** - * Decrypt an encrypted direct message event (kind 4). - * - *

This method automatically determines whether the provided identity is the sender or recipient - * of the message, and decrypts accordingly. Both parties can decrypt the same message. - * - *

The method: - *

    - *
  1. Validates the event is kind 4 (encrypted DM)
  2. - *
  3. Extracts the 'p' tag to identify the recipient
  4. - *
  5. Determines if the identity is the sender or recipient
  6. - *
  7. Uses the appropriate keys for ECDH decryption
  8. - *
  9. Returns the plaintext content
  10. - *
- * - *

Example (as recipient): - *

{@code
-   * Identity myIdentity = new Identity("nsec1...");
-   * GenericEvent dmEvent = ... // received from relay
-   *
-   * String message = NIP04.decrypt(myIdentity, dmEvent);
-   * System.out.println("Received: " + message);
-   * }
- * - *

Example (as sender, reading your own DM): - *

{@code
-   * Identity myIdentity = new Identity("nsec1...");
-   * GenericEvent myDmEvent = ... // a DM I sent
-   *
-   * String message = NIP04.decrypt(myIdentity, myDmEvent);
-   * System.out.println("I sent: " + message);
-   * }
- * - * @param rcptId the identity attempting to decrypt (must be either sender or recipient) - * @param event the encrypted direct message event (must be kind 4) - * @return the decrypted plaintext message - * @throws IllegalArgumentException if the event is not kind 4 - * @throws NoSuchElementException if no 'p' tag is found in the event - * @throws RuntimeException if the identity is neither the sender nor the recipient - */ - public static String decrypt(@NonNull Identity rcptId, @NonNull GenericEvent event) { - - if (event.getKind() != Kind.ENCRYPTED_DIRECT_MESSAGE.getValue()) { - throw new IllegalArgumentException("Event is not an encrypted direct message"); - } - - PubKeyTag pTag = - Filterable.getTypeSpecificTags(PubKeyTag.class, event).stream() - .findFirst() - .or(() -> findGenericPubKeyTag(event)) - .orElseThrow(() -> new NoSuchElementException("No matching p-tag found.")); - - boolean rcptFlag = amITheRecipient(rcptId, event, pTag); - - if (!rcptFlag) { // I am the message sender - log.debug("Decrypting own sent message"); - MessageCipher cipher = - new MessageCipher04( - rcptId.getPrivateKey().getRawData(), pTag.getPublicKey().getRawData()); - return cipher.decrypt(event.getContent()); - } - - // I am the message recipient - var sender = event.getPubKey(); - log.debug("Decrypting message from {}", sender); - MessageCipher cipher = - new MessageCipher04(rcptId.getPrivateKey().getRawData(), sender.getRawData()); - return cipher.decrypt(event.getContent()); - } - - private static Optional findGenericPubKeyTag(GenericEvent event) { - return event.getTags().stream() - .filter(tag -> "p".equalsIgnoreCase(tag.getCode())) - .map(NIP04::toPubKeyTag) - .findFirst(); - } - - private static PubKeyTag toPubKeyTag(BaseTag tag) { - if (tag instanceof PubKeyTag pubKeyTag) { - return pubKeyTag; - } - - if (tag instanceof GenericTag genericTag) { - return PubKeyTag.updateFields(genericTag); - } - - throw new IllegalArgumentException( - "Unsupported tag type for p-tag conversion: " + tag.getClass().getName()); - } - - private static boolean amITheRecipient( - @NonNull Identity recipient, - @NonNull GenericEvent event, - @NonNull PubKeyTag resolvedPubKeyTag) { - if (Objects.equals(recipient.getPublicKey(), resolvedPubKeyTag.getPublicKey())) { - return true; - } - - if (Objects.equals(recipient.getPublicKey(), event.getPubKey())) { - return false; - } - - throw new RuntimeException("Unrelated event"); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP05.java b/nostr-java-api/src/main/java/nostr/api/NIP05.java deleted file mode 100644 index a1f9fdf56..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP05.java +++ /dev/null @@ -1,58 +0,0 @@ -package nostr.api; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.NonNull; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.event.entities.UserProfile; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.EventEncodingException; -import nostr.id.Identity; -import nostr.util.validator.Nip05Validator; - -import java.util.ArrayList; - -import static nostr.base.json.EventJsonMapper.mapper; -import static nostr.util.NostrUtil.escapeJsonString; - -/** - * NIP-05 helpers (DNS-based verification). Create internet identifier metadata events. - * Spec: NIP-05 - */ -public class NIP05 extends EventNostr { - - public NIP05(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create an Internet Identifier Metadata (IIM) Event - * - * @param profile the associate user profile - * @return the IIM event - */ - @SuppressWarnings({"rawtypes","unchecked"}) - public NIP05 createInternetIdentifierMetadataEvent(@NonNull UserProfile profile) { - String content = getContent(profile); - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), Kind.SET_METADATA.getValue(), new ArrayList<>(), content) - .create(); - this.updateEvent(genericEvent); - return this; - } - - private String getContent(UserProfile profile) { - try { - String jsonString = - mapper().writeValueAsString( - Nip05Validator.builder() - .nip05(profile.getNip05()) - .publicKey(profile.getPublicKey().toString()) - .build()); - return escapeJsonString(jsonString); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to encode NIP-05 profile content", ex); - } - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP09.java b/nostr-java-api/src/main/java/nostr/api/NIP09.java deleted file mode 100644 index 4657afeaa..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP09.java +++ /dev/null @@ -1,74 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.event.BaseTag; -import nostr.event.Deleteable; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.id.Identity; - -import java.util.ArrayList; -import java.util.List; - -/** - * NIP-09 helpers (Event Deletion). Build deletion events targeting events or addresses. - * Spec: NIP-09 - */ -public class NIP09 extends EventNostr { - - public NIP09(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create a NIP09 Deletion Event - * - * @param deleteables an array of event or address tags to be deleted - * @return this instance for chaining - */ - public NIP09 createDeletionEvent(@NonNull Deleteable... deleteables) { - return this.createDeletionEvent(List.of(deleteables)); - } - - /** - * Create a NIP09 Deletion Event - * - * @param deleteables list of event or address tags to be deleted - * @return this instance for chaining - */ - public NIP09 createDeletionEvent(@NonNull List deleteables) { - List tags = getTags(deleteables); - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.DELETION.getValue(), tags, "").create(); - this.updateEvent(genericEvent); - - return this; - } - - private List getTags(List deleteables) { - List tags = new ArrayList<>(); - - for (Deleteable d : deleteables) { - if (d instanceof GenericEvent event) { - // Event IDs - tags.add(new EventTag(event.getId())); - // Address tags contained in the event - event.getTags().stream() - .filter(tag -> tag instanceof AddressTag) - .map(AddressTag.class::cast) - .forEach( - tag -> { - tags.add(tag); - tags.add(NIP25.createKindTag(tag.getKind())); - }); - } - // Always include kind tag for each deleteable - tags.add(NIP25.createKindTag(d.getKind())); - } - - return tags; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP12.java b/nostr-java-api/src/main/java/nostr/api/NIP12.java deleted file mode 100644 index d3adf5f6d..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP12.java +++ /dev/null @@ -1,43 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.config.Constants; -import nostr.event.BaseTag; - -import java.net.URL; -import java.util.List; - -/** - * NIP-12 helpers (Generic Tag Queries). Convenience creators for hashtag, reference and geohash tags. - * Spec: NIP-12 - */ -public class NIP12 { - - /** - * Create a hashtag tag - * - * @param hashtag the hashtag - */ - public static BaseTag createHashtagTag(@NonNull String hashtag) { - return new BaseTagFactory(Constants.Tag.HASHTAG_CODE, List.of(hashtag)).create(); - } - - /** - * Create an URL tag - * - * @param url the reference - */ - public static BaseTag createReferenceTag(@NonNull URL url) { - return new BaseTagFactory(Constants.Tag.REFERENCE_CODE, List.of(url.toString())).create(); - } - - /** - * Create a Geo tag - * - * @param location the geohash - */ - public static BaseTag createGeohashTag(@NonNull String location) { - return new BaseTagFactory(Constants.Tag.GEOHASH_CODE, List.of(location)).create(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP14.java b/nostr-java-api/src/main/java/nostr/api/NIP14.java deleted file mode 100644 index 98a32cd9a..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP14.java +++ /dev/null @@ -1,25 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.config.Constants; -import nostr.event.BaseTag; - -import java.util.List; - -/** - * NIP-14 helpers (Subject tag in text notes). Create subject tags for threads. - * Spec: NIP-14 - */ -public class NIP14 { - - /** - * Create a subject tag - * - * @param subject the subject - * @return the created subject tag - */ - public static BaseTag createSubjectTag(@NonNull String subject) { - return new BaseTagFactory(Constants.Tag.SUBJECT_CODE, List.of(subject)).create(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP15.java b/nostr-java-api/src/main/java/nostr/api/NIP15.java deleted file mode 100644 index 60bf2ab74..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP15.java +++ /dev/null @@ -1,98 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.event.entities.CustomerOrder; -import nostr.event.entities.PaymentRequest; -import nostr.event.entities.Product; -import nostr.event.entities.Stall; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -import java.util.List; - -/** - * NIP-15 helpers (Endorsements/Marketplace). Build stall/product metadata and encrypted order flows. - * Spec: NIP-15 - */ -public class NIP15 extends EventNostr { - - public NIP15(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create a merchant request payment event (encrypted DM per NIP-04/NIP-15 flow). - * - * @param paymentRequest the payment request payload (bolt11/details) - * @param customerOrder the referenced customer order containing buyer contact - * @return this instance for chaining - */ - public NIP15 createMerchantRequestPaymentEvent( - @NonNull PaymentRequest paymentRequest, @NonNull CustomerOrder customerOrder) { - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), Kind.ENCRYPTED_DIRECT_MESSAGE.getValue(), paymentRequest.value()) - .create(); - genericEvent.addTag(NIP01.createPubKeyTag(customerOrder.getContact().getPublicKey())); - this.updateEvent(genericEvent); - return this; - } - - /** - * Create a customer order event (encrypted DM per NIP-04/NIP-15 flow). - * - * @param customerOrder the order details including buyer contact - * @return this instance for chaining - */ - public NIP15 createCustomerOrderEvent(@NonNull CustomerOrder customerOrder) { - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), Kind.ENCRYPTED_DIRECT_MESSAGE.getValue(), customerOrder.value()) - .create(); - genericEvent.addTag(NIP01.createPubKeyTag(customerOrder.getContact().getPublicKey())); - this.updateEvent(genericEvent); - - return this; - } - - /** - * Create or update a stall (kind 30017 per NIP-15). - * - * @param stall the stall definition - * @return this instance for chaining - */ - public NIP15 createCreateOrUpdateStallEvent(@NonNull Stall stall) { - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.STALL_CREATE_OR_UPDATE.getValue(), stall.value()).create(); - genericEvent.addTag(NIP01.createIdentifierTag(stall.getId())); - this.updateEvent(genericEvent); - - return this; - } - - /** - * Create or update a product (kind 30018 per NIP-15). - * - * @param product the product definition - * @param categories optional list of hashtags/categories - * @return this instance for chaining - */ - public NIP15 createCreateOrUpdateProductEvent(@NonNull Product product, List categories) { - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.PRODUCT_CREATE_OR_UPDATE.getValue(), product.value()).create(); - genericEvent.addTag(NIP01.createIdentifierTag(product.getId())); - - if (categories != null && !categories.isEmpty()) { - categories.forEach( - category -> { - genericEvent.addTag(NIP12.createHashtagTag(category)); - }); - } - - this.updateEvent(genericEvent); - - return this; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP20.java b/nostr-java-api/src/main/java/nostr/api/NIP20.java deleted file mode 100644 index bbca69eee..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP20.java +++ /dev/null @@ -1,25 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.event.impl.GenericEvent; -import nostr.event.message.OkMessage; - -/** - * NIP-20 helpers (OK message). Build OK messages indicating relay acceptance/rejection. - * Spec: NIP-20 - */ -public class NIP20 { - - /** - * Create an OK message providing information about if an event was accepted or rejected. - * - * @param event the related event - * @param flag true if the relay accepted the event; false otherwise - * @param message additional information as to why the command succeeded or failed - * @return the OK message - */ - public static OkMessage createOkMessage( - @NonNull GenericEvent event, boolean flag, String message) { - return new OkMessage(event.getId(), flag, message); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP23.java b/nostr-java-api/src/main/java/nostr/api/NIP23.java deleted file mode 100644 index 8f31decc9..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP23.java +++ /dev/null @@ -1,139 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -import java.net.URL; - -/** - * NIP-23 helpers (Long-form content). Build long-form notes and related tags. - * Spec: NIP-23 - */ -public class NIP23 extends EventNostr { - - public NIP23(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create a Long-form Content event with tags - * - * @param content a text in Markdown syntax - */ - public NIP23 creatLongFormTextNoteEvent(@NonNull String content) { - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.LONG_FORM_TEXT_NOTE.getValue(), content).create(); - this.updateEvent(genericEvent); - return this; - } - - /** - * Create a Long-form Draft event (kind 30023) that is not intended for indexing. - * - * @param content a text in Markdown syntax for the draft - * @return this instance for chaining - */ - NIP23 createLongFormDraftEvent(@NonNull String content) { - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.LONG_FORM_DRAFT.getValue(), content).create(); - this.updateEvent(genericEvent); - return this; - } - - /** - * Add a title tag to the long-form content event. - * - * @param title the article title - * @return this instance for chaining - */ - public NIP23 addTitleTag(@NonNull String title) { - getEvent().addTag(createTitleTag(title)); - return this; - } - - /** - * Add an image tag to the long-form content event. - * - * @param url URL of the image to be shown with the title - * @return this instance for chaining - */ - public NIP23 addImageTag(@NonNull URL url) { - getEvent().addTag(createImageTag(url)); - return this; - } - - /** - * Add a summary tag to the long-form content event. - * - * @param summary the article summary - * @return this instance for chaining - */ - public NIP23 addSummaryTag(@NonNull String summary) { - getEvent().addTag(createSummaryTag(summary)); - return this; - } - - /** - * Add a published_at tag to the long-form content event. - * - * @param date timestamp in unix seconds (stringified) when the article was first published - * @return this instance for chaining - */ - public NIP23 addPublishedAtTag(@NonNull Long date) { - getEvent().addTag(createPublishedAtTag(date)); - return this; - } - - /** - * Create a title tag - * - * @param title the article title - */ - public static BaseTag createTitleTag(@NonNull String title) { - return new BaseTagFactory("title", title).create(); - } - - /** - * Create an image tag - * - * @param url a URL pointing to an image to be shown along with the title - */ - public static BaseTag createImageTag(@NonNull URL url) { - return new BaseTagFactory(Constants.Tag.IMAGE_CODE, url.toString()).create(); - } - - /** - * Create an image tag - * - * @param url a URL pointing to an image to be shown along with the title - * @param size the size of the image - */ - public static BaseTag createImageTag(@NonNull URL url, String size) { - return new BaseTagFactory(Constants.Tag.IMAGE_CODE, url.toString(), size).create(); - } - - /** - * Create a summary tag - * - * @param summary the article summary - */ - public static BaseTag createSummaryTag(@NonNull String summary) { - return new BaseTagFactory(Constants.Tag.SUMMARY_CODE, summary).create(); - } - - /** - * Create a published_at tag - * - * @param date the timestamp in unix seconds (stringified) of the first time the article was - * published - */ - public static BaseTag createPublishedAtTag(@NonNull Long date) { - return new BaseTagFactory(Constants.Tag.PUBLISHED_AT_CODE, date.toString()).create(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP25.java b/nostr-java-api/src/main/java/nostr/api/NIP25.java deleted file mode 100644 index a4f530de4..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP25.java +++ /dev/null @@ -1,175 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.base.Relay; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.entities.Reaction; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.EmojiTag; -import nostr.event.tag.EventTag; -import nostr.id.Identity; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; - -/** - * NIP-25 helpers (Reactions). Build reaction events and custom emoji tags. - * Spec: NIP-25 - */ -public class NIP25 extends EventNostr { - - public NIP25(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create a Reaction event - * - * @param event the related event to react to - * @param reaction the reaction to use (e.g., 👍/👎 or custom emoji) - * @param relay optional recommended relay for the referenced event - */ - public NIP25 createReactionEvent( - @NonNull GenericEvent event, @NonNull Reaction reaction, Relay relay) { - return this.createReactionEvent(event, reaction.getEmoji(), relay); - } - - /** - * Create a NIP25 Reaction event to react to a specific event - * - * @param event the related event to react to - * @param content MAY be an emoji - * @param relay optional recommended relay for the referenced event - */ - public NIP25 createReactionEvent( - @NonNull GenericEvent event, @NonNull String content, Relay relay) { - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.REACTION.getValue(), content).create(); - - // Addressable event? - if (event.isAddressable()) { - genericEvent.addTag( - NIP01.createAddressTag(event.getKind(), event.getPubKey(), event.getId())); - genericEvent.addTag(NIP25.createKindTag(event.getKind())); - } else { - genericEvent.addTag( - NIP01.createEventTag(event.getId(), relay != null ? relay.toString() : null, null)); - genericEvent.addTag(NIP01.createPubKeyTag(event.getPubKey())); - } - this.updateEvent(genericEvent); - return this; - } - - /** - * Create a reaction-to-website event (kind 17) reacting to a URL. - * - * @param url the target website URL to react to - * @param reaction the reaction to use (emoji) - * @return this instance for chaining - */ - public NIP25 createReactionToWebsiteEvent(@NonNull URL url, @NonNull Reaction reaction) { - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), Kind.REACTION_TO_WEBSITE.getValue(), reaction.getEmoji()) - .create(); - genericEvent.addTag(NIP12.createReferenceTag(url)); - this.updateEvent(genericEvent); - return this; - } - - /** - * Create a NIP25 Reaction event to react to a specific event - * - * @param eventTag the e-tag referencing the related event to react to - * @param emojiTag MUST be an costum emoji (NIP30) - */ - public NIP25 createReactionEvent(@NonNull BaseTag eventTag, @NonNull BaseTag emojiTag) { - - // 1. Validation - if (!(emojiTag instanceof EmojiTag)) { - throw new IllegalArgumentException("The tag is not a custom emoji tag"); - } - - if (!(eventTag instanceof EventTag)) { - throw new IllegalArgumentException("The tag is not an event tag"); - } - - String shortCode = ((EmojiTag) emojiTag).getShortcode(); - var content = String.format(":%s:", shortCode); - - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.REACTION.getValue(), content).create(); - genericEvent.addTag(emojiTag); - genericEvent.addTag(eventTag); - - this.updateEvent(genericEvent); - return this; - } - - /** - * Create the kind tag - * - * @param kind the kind - */ - public static BaseTag createKindTag(@NonNull Integer kind) { - return new BaseTagFactory(Constants.Tag.KIND_CODE, kind.toString()).create(); - } - - public static BaseTag createCustomEmojiTag(@NonNull String shortcode, @NonNull URL url) { - return new BaseTagFactory(Constants.Tag.EMOJI_CODE, shortcode, url.toString()).create(); - } - - public static BaseTag createCustomEmojiTag(@NonNull String shortcode, @NonNull String url) { - try { - return createCustomEmojiTag(shortcode, URI.create(url).toURL()); - } catch (MalformedURLException ex) { - throw new IllegalArgumentException("Invalid custom emoji URL: " + url, ex); - } - } - - /** - * Send a like reaction to an event. - * - * @param event the event to like - * @param relay optional recommended relay for the referenced event - */ - public void like(@NonNull GenericEvent event, Relay relay) { - react(event, Reaction.LIKE.getEmoji(), relay); - } - - public void like(@NonNull GenericEvent event) { - react(event, Reaction.LIKE.getEmoji(), null); - } - - /** - * Send a dislike reaction to an event. - * - * @param event the event to dislike - * @param relay optional recommended relay for the referenced event - */ - public void dislike(@NonNull GenericEvent event, Relay relay) { - react(event, Reaction.DISLIKE.getEmoji(), relay); - } - - public void dislike(@NonNull GenericEvent event) { - react(event, Reaction.DISLIKE.getEmoji(), null); - } - - /** - * React to an event with the provided content. - * - * @param event the event being reacted to - * @param reaction the reaction content (emoji or text) - * @param relay optional recommended relay for the referenced event - */ - public void react(@NonNull GenericEvent event, @NonNull String reaction, Relay relay) { - GenericEvent e = createReactionEvent(event, reaction, relay).getEvent(); - this.updateEvent(e); - this.sign().send(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP28.java b/nostr-java-api/src/main/java/nostr/api/NIP28.java deleted file mode 100644 index c3c03b70a..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP28.java +++ /dev/null @@ -1,229 +0,0 @@ -package nostr.api; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.base.Marker; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.base.json.EventJsonMapper; -import nostr.event.entities.ChannelProfile; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; -import org.apache.commons.text.StringEscapeUtils; - -import java.util.List; - -import static nostr.api.NIP12.createHashtagTag; - -/** - * NIP-28 helpers (Public chat). Build channel create/metadata/message and moderation events. - * Spec: NIP-28 - */ -public class NIP28 extends EventNostr { - - public NIP28(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create a KIND-40 public chat channel - * - * @param profile the channel metadata - */ - public NIP28 createChannelCreateEvent(@NonNull ChannelProfile profile) { - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), - Kind.CHANNEL_CREATE.getValue(), - StringEscapeUtils.escapeJson(profile.toString())) - .create(); - this.updateEvent(genericEvent); - return this; - } - - /** - * Create a KIND-42 channel message - * - * @param channelCreateEvent KIND-40 channel create event - * @param messageReplyTo the reply tag. If present, it must be a reply to a message, else it is a - * root message - * @param recommendedRelayRoot in the scenario of a root message, the recommended relay for the - * root message - * @param recommendedRelayReply in the scenario of a reply message, the recommended relay for the - * reply message - * @param content the message - */ - public NIP28 createChannelMessageEvent( - @NonNull GenericEvent channelCreateEvent, - GenericEvent messageReplyTo, - Relay recommendedRelayRoot, - Relay recommendedRelayReply, - @NonNull String content) { - - // 1. Validation - if (channelCreateEvent.getKind() != Kind.CHANNEL_CREATE.getValue()) { - throw new IllegalArgumentException("The event is not a channel creation event"); - } - - // 2. Create the event - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.CHANNEL_MESSAGE.getValue(), content).create(); - - // 3. Add the tags - genericEvent.addTag( - NIP01.createEventTag(channelCreateEvent.getId(), recommendedRelayRoot, Marker.ROOT)); - if (messageReplyTo != null) { - genericEvent.addTag( - NIP01.createEventTag(messageReplyTo.getId(), recommendedRelayReply, Marker.REPLY)); - genericEvent.addTag(NIP01.createPubKeyTag(messageReplyTo.getPubKey())); - } - - // 4. Update the event - this.updateEvent(genericEvent); - - return this; - } - - /** - * Create a KIND-42 channel root message - * - * @param channelCreateEvent KIND-40 channel create event - * @param content the message - */ - public NIP28 createChannelMessageEvent( - @NonNull GenericEvent channelCreateEvent, - @NonNull Relay recommendedRelayRoot, - @NonNull String content) { - - return createChannelMessageEvent(channelCreateEvent, null, recommendedRelayRoot, null, content); - } - - /** - * Create a KIND-42 channel message reply - * - * @param channelCreateEvent KIND-40 channel create event - * @param eventTagReplyTo the reply tag with the root marker - * @param content the message - */ - public NIP28 createChannelMessageEvent( - @NonNull GenericEvent channelCreateEvent, - @NonNull GenericEvent eventTagReplyTo, - @NonNull String content) { - - return createChannelMessageEvent(channelCreateEvent, eventTagReplyTo, null, null, content); - } - - /** - * Create a KIND-41 channel metadata event - * - * @param profile the channel metadata - */ - public NIP28 updateChannelMetadataEvent( - @NonNull GenericEvent channelCreateEvent, @NonNull ChannelProfile profile, Relay relay) { - return this.updateChannelMetadataEvent(channelCreateEvent, profile, null, relay); - } - - /** - * Create a KIND-41 channel metadata event - * - * @param channelCreateEvent KIND-40 channel create event - * @param categories the list of categories - * @param profile the channel metadata - * @param relay the recommended root relay - */ - public NIP28 updateChannelMetadataEvent( - @NonNull GenericEvent channelCreateEvent, - @NonNull ChannelProfile profile, - List categories, - Relay relay) { - - // 1. Validation - if (channelCreateEvent.getKind() != Kind.CHANNEL_CREATE.getValue()) { - throw new IllegalArgumentException("The event is not a channel creation event"); - } - - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), - Kind.CHANNEL_METADATA.getValue(), - StringEscapeUtils.escapeJson(profile.toString())) - .create(); - genericEvent.addTag(NIP01.createEventTag(channelCreateEvent.getId(), relay, Marker.ROOT)); - if (categories != null) { - categories.stream() - .filter(category -> category != null && !category.isEmpty()) - .forEach( - category -> { - genericEvent.addTag(createHashtagTag(category)); - }); - } - updateEvent(genericEvent); - return this; - } - - /** - * Create a KIND-43 hide message event - * - * @param channelMessageEvent NIP-42 event to hide - * @param reason optional reason for the action - */ - public NIP28 createHideMessageEvent(@NonNull GenericEvent channelMessageEvent, String reason) { - - if (channelMessageEvent.getKind() != Kind.CHANNEL_MESSAGE.getValue()) { - throw new IllegalArgumentException("The event is not a channel message event"); - } - - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), - Kind.HIDE_MESSAGE.getValue(), - Reason.fromString(reason).toString()) - .create(); - genericEvent.addTag(NIP01.createEventTag(channelMessageEvent.getId())); - updateEvent(genericEvent); - return this; - } - - /** - * Create a KIND-44 mute user event - * - * @param mutedUser the user to mute. Their messages will no longer be visible - * @param reason optional reason for the action - */ - public NIP28 createMuteUserEvent(@NonNull PublicKey mutedUser, String reason) { - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), Kind.MUTE_USER.getValue(), Reason.fromString(reason).toString()) - .create(); - genericEvent.addTag(NIP01.createPubKeyTag(mutedUser)); - updateEvent(genericEvent); - return this; - } - - @NoArgsConstructor - @AllArgsConstructor - @EqualsAndHashCode - private static class Reason { - - @JsonProperty("reason") - private String value; - - public String toString() { - try { - return EventJsonMapper.mapper().writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - public static Reason fromString(String reason) { - return new Reason(reason); - } - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP30.java b/nostr-java-api/src/main/java/nostr/api/NIP30.java deleted file mode 100644 index 1b948394e..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP30.java +++ /dev/null @@ -1,24 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.config.Constants; -import nostr.event.BaseTag; - -/** - * NIP-30 helpers (Custom emoji). Create emoji tags with shortcode and image URL. - * Spec: NIP-30 - */ -public class NIP30 { - - /** - * Create a custom emoji tag as defined by NIP-30. - * - * @param shortcode the emoji shortcode (e.g., "party_parrot") - * @param imageUrl the URL pointing to the emoji image asset - * @return the created emoji tag - */ - public static BaseTag createEmojiTag(@NonNull String shortcode, @NonNull String imageUrl) { - return new BaseTagFactory(Constants.Tag.EMOJI_CODE, shortcode, imageUrl).create(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP31.java b/nostr-java-api/src/main/java/nostr/api/NIP31.java deleted file mode 100644 index 782fc83c1..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP31.java +++ /dev/null @@ -1,23 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.config.Constants; -import nostr.event.BaseTag; - -/** - * NIP-31 helpers (Alt tag). Create alt tags describing event context/purpose. - * Spec: NIP-31 - */ -public class NIP31 { - - /** - * Create an alt tag describing the purpose or context of an event (NIP-31). - * - * @param alt the human-friendly alternative description - * @return the created alt tag - */ - public static BaseTag createAltTag(@NonNull String alt) { - return new BaseTagFactory(Constants.Tag.ALT_CODE, alt).create(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP32.java b/nostr-java-api/src/main/java/nostr/api/NIP32.java deleted file mode 100644 index 6163aa6f2..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP32.java +++ /dev/null @@ -1,34 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.config.Constants; -import nostr.event.BaseTag; - -/** - * NIP-32 helpers (Labeling). Create namespace and label tags. - * Spec: NIP-32 - */ -public class NIP32 { - - /** - * Create a namespace tag for labels (NIP-32). - * - * @param namespace the label namespace - * @return the created namespace tag - */ - public static BaseTag createNameSpaceTag(@NonNull String namespace) { - return new BaseTagFactory(Constants.Tag.NAMESPACE_CODE, namespace).create(); - } - - /** - * Create a label tag within the provided namespace (NIP-32). - * - * @param label the label value - * @param namespace the label's namespace - * @return the created label tag - */ - public static BaseTag createLabelTag(@NonNull String label, @NonNull String namespace) { - return new BaseTagFactory(Constants.Tag.LABEL_CODE, label, namespace).create(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP40.java b/nostr-java-api/src/main/java/nostr/api/NIP40.java deleted file mode 100644 index 35fb8df32..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP40.java +++ /dev/null @@ -1,23 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.config.Constants; -import nostr.event.BaseTag; - -/** - * NIP-40 helpers (Expiration). Create expiration tags for events. - * Spec: NIP-40 - */ -public class NIP40 { - - /** - * Create an expiration tag (NIP-40) to indicate when an event should be considered expired. - * - * @param expiration unix timestamp (seconds) when the event expires - * @return the created expiration tag - */ - public static BaseTag createExpirationTag(@NonNull Integer expiration) { - return new BaseTagFactory(Constants.Tag.EXPIRATION_CODE, expiration.toString()).create(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP42.java b/nostr-java-api/src/main/java/nostr/api/NIP42.java deleted file mode 100644 index 980e47ed6..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP42.java +++ /dev/null @@ -1,98 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Command; -import nostr.base.ElementAttribute; -import nostr.base.Kind; -import nostr.base.Relay; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.impl.CanonicalAuthenticationEvent; -import nostr.event.impl.GenericEvent; -import nostr.event.message.CanonicalAuthenticationMessage; -import nostr.event.message.GenericMessage; - -import java.util.ArrayList; -import java.util.List; - -/** - * NIP-42 helpers (Authentication). Build auth events and AUTH messages. - * Spec: NIP-42 - */ -public class NIP42 extends EventNostr { - - /** - * Create a canonical authentication event (NIP-42). - * - * @param challenge the challenge string received from the relay - * @param relay the relay to which the client authenticates - * @return this instance for chaining - */ - public NIP42 createCanonicalAuthenticationEvent(@NonNull String challenge, @NonNull Relay relay) { - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.CLIENT_AUTH.getValue(), "").create(); - this.updateEvent(genericEvent); - this.addChallengeTag(challenge); - this.addRelayTag(relay); - - return this; - } - - public NIP42 addRelayTag(@NonNull Relay relay) { - var tag = createRelayTag(relay); - getEvent().addTag(tag); - return this; - } - - public NIP42 addChallengeTag(@NonNull String challenge) { - var tag = createChallengeTag(challenge); - getEvent().addTag(tag); - return this; - } - - /** - * Create a relay tag referencing the relay being authenticated. - * - * @param relay the relay - * @return the created relay tag - */ - public static BaseTag createRelayTag(@NonNull Relay relay) { - return new BaseTagFactory(Constants.Tag.RELAY_CODE, relay.getUri()).create(); - } - - /** - * Create a challenge tag holding the relay-provided token. - * - * @param challenge the relay-provided challenge string - * @return the created challenge tag - */ - public static BaseTag createChallengeTag(@NonNull String challenge) { - return new BaseTagFactory(Constants.Tag.CHALLENGE_CODE, challenge).create(); - } - - /** - * Create a client authentication message for the provided authentication event. - * - * @param event the canonical authentication event (signed) - * @return the AUTH message to send to the relay - */ - public static CanonicalAuthenticationMessage createClientAuthenticationMessage( - @NonNull CanonicalAuthenticationEvent event) { - return new CanonicalAuthenticationMessage(event); - } - - /** - * Create a relay AUTH message requesting client authentication. - * - * @param challenge the relay-provided challenge string - * @return the AUTH message - */ - public static GenericMessage createRelayAuthenticationMessage(@NonNull String challenge) { - final List attributes = new ArrayList<>(); - final var attr = new ElementAttribute("challenge", challenge); - attributes.add(attr); - return new GenericMessage(Command.AUTH.name(), attributes); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP44.java b/nostr-java-api/src/main/java/nostr/api/NIP44.java deleted file mode 100644 index 91616d13b..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP44.java +++ /dev/null @@ -1,351 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import nostr.base.PublicKey; -import nostr.encryption.MessageCipher; -import nostr.encryption.MessageCipher44; -import nostr.event.filter.Filterable; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; - -import java.util.NoSuchElementException; -import java.util.Objects; - -/** - * NIP-44: Encrypted Payloads (Versioned Encrypted Messages). - * - *

This class provides utilities for encrypting and decrypting messages using NIP-44, which is - * the recommended encryption standard for Nostr. NIP-44 uses XChaCha20-Poly1305 - * authenticated encryption (AEAD) with padding to prevent metadata leakage. - * - *

What is NIP-44?

- * - *

NIP-44 is the successor to NIP-04 and provides: - *

    - *
  • XChaCha20-Poly1305 AEAD: Authenticated encryption prevents tampering
  • - *
  • Padding: Messages are padded to standard sizes to hide true length
  • - *
  • Versioning: Version byte (0x02) allows future algorithm upgrades
  • - *
  • HMAC-SHA256 for key derivation: Safer than raw ECDH
  • - *
  • Protection against metadata leakage: Padding obscures message size
  • - *
- * - *

NIP-44 vs NIP-04

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
FeatureNIP-04 (Legacy)NIP-44 (Recommended)
EncryptionAES-256-CBCXChaCha20-Poly1305
AuthenticationNone (vulnerable to tampering)AEAD (authenticated)
PaddingNone (message length visible)Power-of-2 padding (hides length)
Key DerivationRaw ECDH shared secretHMAC-SHA256(ECDH)
VersioningNo version byteVersion byte (0x02)
Security⚠️ Deprecated✅ Production-ready
- * - *

When to Use NIP-44

- * - *

Use NIP-44 for: - *

    - *
  • New applications: Always prefer NIP-44 over NIP-04
  • - *
  • Private DMs: Kind 4 events with encrypted content
  • - *
  • Encrypted content fields: Any event that needs encrypted data
  • - *
  • Group messaging: When combined with multi-recipient protocols
  • - *
- * - *

Use NIP-04 only for: - *

    - *
  • Backward compatibility with legacy clients
  • - *
  • Reading existing NIP-04 encrypted messages
  • - *
- * - *

Usage Examples

- * - *

Example 1: Encrypt a Message

- *
{@code
- * Identity alice = new Identity("nsec1...");
- * PublicKey bob = new PublicKey("npub1...");
- *
- * String encrypted = NIP44.encrypt(alice, "Hello Bob!", bob);
- * // Returns a versioned encrypted payload (starts with 0x02)
- * }
- * - *

Example 2: Decrypt a Message

- *
{@code
- * Identity bob = new Identity("nsec1...");
- * PublicKey alice = new PublicKey("npub1...");
- * String encrypted = "..."; // received encrypted message
- *
- * String plaintext = NIP44.decrypt(bob, encrypted, alice);
- * System.out.println(plaintext); // "Hello Bob!"
- * }
- * - *

Example 3: Decrypt an Encrypted DM Event (Kind 4)

- *
{@code
- * Identity myIdentity = new Identity("nsec1...");
- * GenericEvent dmEvent = ... // received kind-4 event with NIP-44 encryption
- *
- * String plaintext = NIP44.decrypt(myIdentity, dmEvent);
- * // Works whether you're the sender or recipient
- * }
- * - *

Example 4: Create and Send an Encrypted DM (Manual)

- *
{@code
- * Identity sender = new Identity("nsec1...");
- * PublicKey recipient = new PublicKey("npub1...");
- *
- * // Encrypt the message
- * String encrypted = NIP44.encrypt(sender, "Secret message", recipient);
- *
- * // Create a kind-4 event
- * GenericEvent dm = new GenericEvent(sender.getPublicKey(), Kind.ENCRYPTED_DIRECT_MESSAGE);
- * dm.setContent(encrypted);
- * dm.addTag(new PubKeyTag(recipient));
- *
- * // Sign and send
- * sender.sign(dm);
- * client.send(dm, relays);
- * }
- * - *

Encryption Format

- * - *

NIP-44 ciphertext structure (base64-encoded): - *

- * [version (1 byte)][nonce (32 bytes)][ciphertext (variable)][MAC (16 bytes)]
- * 
- * - *
    - *
  • Version: 0x02 (current version)
  • - *
  • Nonce: 32-byte random value (XChaCha20 nonce)
  • - *
  • Ciphertext: Encrypted + padded message
  • - *
  • MAC: 16-byte Poly1305 authentication tag
  • - *
- * - *

Padding Scheme

- * - *

Messages are padded to the next power-of-2 size (up to 64KB), hiding the true message length: - *

    - *
  • 0-32 bytes → padded to 32 bytes
  • - *
  • 33-64 bytes → padded to 64 bytes
  • - *
  • 65-128 bytes → padded to 128 bytes
  • - *
  • ... and so on up to 65536 bytes
  • - *
- * - *

Security Properties

- * - *
    - *
  • Confidentiality: XChaCha20 encryption
  • - *
  • Authenticity: Poly1305 MAC prevents tampering
  • - *
  • Forward secrecy: No (static key pairs)
  • - *
  • Metadata protection: Padding hides message length
  • - *
  • Replay protection: No (application-level responsibility)
  • - *
- * - *

Thread Safety

- * - *

All static methods in this class are thread-safe. - * - *

Design Pattern

- * - *

This class follows the Utility Pattern, providing static helper methods for: - *

    - *
  • Message encryption (delegates to {@link MessageCipher44})
  • - *
  • Message decryption (delegates to {@link MessageCipher44})
  • - *
  • Event-based decryption (extracts keys from event tags)
  • - *
- * - * @see NIP-44 Specification - * @see NIP04 - * @see nostr.encryption.MessageCipher44 - * @since 0.5.0 - */ -@Slf4j -public class NIP44 extends EventNostr { - - /** - * Encrypt a plaintext message using NIP-44 encryption (XChaCha20-Poly1305 AEAD). - * - *

This method performs NIP-44 encryption: - *

    - *
  1. Derives a shared secret using ECDH (sender's private key + recipient's public key)
  2. - *
  3. Derives an encryption key using HMAC-SHA256
  4. - *
  5. Pads the message to the next power-of-2 size (32, 64, 128, ..., 65536 bytes)
  6. - *
  7. Generates a random 32-byte nonce
  8. - *
  9. Encrypts with XChaCha20-Poly1305 AEAD
  10. - *
  11. Returns: base64([version][nonce][ciphertext][MAC])
  12. - *
- * - *

Security: This method provides both confidentiality (encryption) and - * authenticity (MAC). Tampering with the ciphertext will be detected during decryption. - * - *

Example: - *

{@code
-   * Identity alice = new Identity("nsec1...");
-   * PublicKey bob = new PublicKey("npub1...");
-   *
-   * String encrypted = NIP44.encrypt(alice, "Hello Bob!", bob);
-   * // Returns base64-encoded versioned encrypted payload
-   * }
- * - * @param sender the identity of the sender (must contain private key for ECDH) - * @param message the plaintext message to encrypt - * @param recipient the recipient's public key - * @return the encrypted message in NIP-44 format (base64-encoded) - */ - public static String encrypt( - @NonNull Identity sender, @NonNull String message, @NonNull PublicKey recipient) { - MessageCipher cipher = - new MessageCipher44(sender.getPrivateKey().getRawData(), recipient.getRawData()); - return cipher.encrypt(message); - } - - /** - * Decrypt a NIP-44 encrypted message using XChaCha20-Poly1305 AEAD. - * - *

This method performs NIP-44 decryption: - *

    - *
  1. Derives the same shared secret using ECDH
  2. - *
  3. Derives the decryption key using HMAC-SHA256
  4. - *
  5. Parses the encrypted format: [version][nonce][ciphertext][MAC]
  6. - *
  7. Verifies the Poly1305 MAC (throws if tampered)
  8. - *
  9. Decrypts using XChaCha20
  10. - *
  11. Removes padding and returns the plaintext
  12. - *
- * - *

Either party (sender or recipient) can decrypt the message by providing their own private - * key and the other party's public key. - * - *

Security: If the MAC verification fails (message was tampered with), - * decryption will fail with an exception. - * - *

Example: - *

{@code
-   * Identity bob = new Identity("nsec1...");
-   * PublicKey alice = new PublicKey("npub1...");
-   * String encrypted = "..."; // received NIP-44 encrypted message
-   *
-   * String plaintext = NIP44.decrypt(bob, encrypted, alice);
-   * System.out.println(plaintext); // "Hello Bob!"
-   * }
- * - * @param identity the identity performing decryption (sender or recipient) - * @param encrypteEPessage the encrypted message in NIP-44 format (base64-encoded) - * @param recipient the public key of the other party (counterparty) - * @return the decrypted plaintext message - * @throws RuntimeException if MAC verification fails or decryption fails - */ - public static String decrypt( - @NonNull Identity identity, @NonNull String encrypteEPessage, @NonNull PublicKey recipient) { - MessageCipher cipher = - new MessageCipher44(identity.getPrivateKey().getRawData(), recipient.getRawData()); - return cipher.decrypt(encrypteEPessage); - } - - /** - * Decrypt a NIP-44 encrypted direct message event (kind 4 or other encrypted events). - * - *

This method automatically determines whether the provided identity is the sender or recipient - * of the message, extracts the counterparty's public key from the event, and decrypts accordingly. - * - *

The method: - *

    - *
  1. Extracts the 'p' tag to identify the recipient/counterparty
  2. - *
  3. Determines if the identity is the sender or recipient
  4. - *
  5. Uses the appropriate keys for ECDH decryption
  6. - *
  7. Verifies the Poly1305 MAC
  8. - *
  9. Returns the plaintext content
  10. - *
- * - *

Example (as recipient): - *

{@code
-   * Identity myIdentity = new Identity("nsec1...");
-   * GenericEvent dmEvent = ... // received from relay (kind 4 with NIP-44 encryption)
-   *
-   * String message = NIP44.decrypt(myIdentity, dmEvent);
-   * System.out.println("Received: " + message);
-   * }
- * - *

Example (as sender, reading your own DM): - *

{@code
-   * Identity myIdentity = new Identity("nsec1...");
-   * GenericEvent myDmEvent = ... // a DM I sent with NIP-44
-   *
-   * String message = NIP44.decrypt(myIdentity, myDmEvent);
-   * System.out.println("I sent: " + message);
-   * }
- * - * @param recipient the identity attempting to decrypt (must be either sender or recipient) - * @param event the encrypted event (typically kind 4, but can be any event with encrypted content) - * @return the decrypted plaintext content - * @throws NoSuchElementException if no 'p' tag is found in the event - * @throws RuntimeException if the identity is neither the sender nor the recipient, or if MAC verification fails - */ - public static String decrypt(@NonNull Identity recipient, @NonNull GenericEvent event) { - boolean rcptFlag = amITheRecipient(recipient, event); - - if (!rcptFlag) { // I am the message sender - MessageCipher cipher = - new MessageCipher44( - recipient.getPrivateKey().getRawData(), - Filterable.getTypeSpecificTags(PubKeyTag.class, event).stream() - .findFirst() - .orElseThrow(() -> new NoSuchElementException("No matching p-tag found.")) - .getPublicKey() - .getRawData()); - return cipher.decrypt(event.getContent()); - } - - // I am the message recipient - var sender = event.getPubKey(); - log.debug("Decrypting message for {}", sender); - MessageCipher cipher = - new MessageCipher44(recipient.getPrivateKey().getRawData(), sender.getRawData()); - return cipher.decrypt(event.getContent()); - } - - private static boolean amITheRecipient(@NonNull Identity recipient, @NonNull GenericEvent event) { - // Use helper to fetch the p-tag without manual casts - PubKeyTag pTag = - Filterable.getTypeSpecificTags(PubKeyTag.class, event).stream() - .findFirst() - .orElseThrow(() -> new NoSuchElementException("No matching p-tag found.")); - - if (Objects.equals(recipient.getPublicKey(), pTag.getPublicKey())) { - return true; - } - - if (Objects.equals(recipient.getPublicKey(), event.getPubKey())) { - return false; - } - - throw new RuntimeException("Unrelated event"); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP46.java b/nostr-java-api/src/main/java/nostr/api/NIP46.java deleted file mode 100644 index 5d25d5656..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP46.java +++ /dev/null @@ -1,180 +0,0 @@ -package nostr.api; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -import java.io.Serializable; -import java.util.LinkedHashSet; -import java.util.Set; - -import static nostr.base.json.EventJsonMapper.mapper; - -/** - * NIP-46 helpers (Nostr Connect). Build app requests and signer responses. - * Spec: NIP-46 - */ -@Slf4j -public final class NIP46 extends EventNostr { - - public NIP46(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create an app request for the signer - * - * @param request the request payload (RPC-like) serialized to JSON - * @param signer the target signer public key - * @return this instance for chaining - */ - public NIP46 createRequestEvent(@NonNull NIP46.Request request, @NonNull PublicKey signer) { - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), - Kind.NOSTR_CONNECT.getValue(), - NIP44.encrypt(getSender(), request.toString(), signer)) - .create(); - genericEvent.addTag(NIP01.createPubKeyTag(signer)); - this.updateEvent(genericEvent); - return this; - } - - /** - * Create a signer response for the app. - * - * @param response the response payload serialized to JSON - * @param app the target app public key - * @return this instance for chaining - */ - public NIP46 createResponseEvent(@NonNull NIP46.Response response, @NonNull PublicKey app) { - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), - Kind.NOSTR_CONNECT.getValue(), - NIP44.encrypt(getSender(), response.toString(), app)) - .create(); - genericEvent.addTag(NIP01.createPubKeyTag(app)); - this.updateEvent(genericEvent); - return this; - } - - @Data - @AllArgsConstructor - @NoArgsConstructor - @Slf4j - public static final class Request implements Serializable { - private String id; - private String method; - // @JsonIgnore - private final Set params = new LinkedHashSet<>(); - - public Request(String id, String method, Set params) { - this.id = id; - this.method = method; - if (params != null) { - this.params.addAll(params); - } - } - - /** - * Add a parameter to the request payload preserving insertion order. - * - * @param param the parameter value - */ - public void addParam(String param) { - this.params.add(param); - } - - /** - * Number of parameters currently present. - */ - @JsonIgnore - public int getParamCount() { - return this.params.size(); - } - - /** - * Tests whether the given parameter exists. - */ - @JsonIgnore - public boolean containsParam(String param) { - return this.params.contains(param); - } - - /** - * Serialize this request to JSON. - * - * @return the JSON representation of this request - */ - public String toString() { - try { - return mapper().writeValueAsString(this); - } catch (JsonProcessingException ex) { - log.warn("Error converting request to JSON: {}", ex.getMessage()); - return "{}"; // Return an empty JSON object as a fallback - } - } - - /** - * Deserialize a JSON string into a Request. - * - * @param jsonString the JSON string - * @return the parsed Request instance - */ - public static Request fromString(@NonNull String jsonString) { - try { - return mapper().readValue(jsonString, Request.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - } - - @Data - @AllArgsConstructor - @NoArgsConstructor - @Slf4j - public static final class Response implements Serializable { - private String id; - private String error; - private String result; - - /** - * Serialize this response to JSON. - * - * @return the JSON representation of this response - */ - public String toString() { - try { - return mapper().writeValueAsString(this); - } catch (JsonProcessingException ex) { - log.warn("Error converting response to JSON: {}", ex.getMessage()); - return "{}"; // Return an empty JSON object as a fallback - } - } - - /** - * Deserialize a JSON string into a Response. - * - * @param jsonString the JSON string - * @return the parsed Response instance - */ - public static Response fromString(@NonNull String jsonString) { - try { - return mapper().readValue(jsonString, Response.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP52.java b/nostr-java-api/src/main/java/nostr/api/NIP52.java deleted file mode 100644 index 9a453ab9e..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP52.java +++ /dev/null @@ -1,233 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.entities.CalendarContent; -import nostr.event.entities.CalendarRsvpContent; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.EventTag; -import nostr.event.tag.GeohashTag; -import nostr.id.Identity; -import org.apache.commons.lang3.stream.Streams; - -import java.net.URI; -import java.util.List; -import java.util.Optional; - -import static nostr.api.NIP01.createIdentifierTag; -import static nostr.api.NIP23.createImageTag; -import static nostr.api.NIP23.createSummaryTag; -import static nostr.api.NIP23.createTitleTag; -import static nostr.api.NIP99.createLocationTag; -import static nostr.api.NIP99.createStatusTag; - -/** - * NIP-52 helpers (Calendar Events). Build time/date-based calendar events and RSVP. - * Spec: NIP-52 - */ -public class NIP52 extends EventNostr { - public NIP52(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create a time-based calendar event (kind 31922) with provided tags and content. - * - * @param baseTags additional tags to include (e.g., location, labels) - * @param content optional human-readable content/notes - * @param calendarContent the structured calendar content (identifier, title, start, etc.) - * @return this instance for chaining - */ - @SuppressWarnings({"rawtypes","unchecked"}) - public NIP52 createCalendarTimeBasedEvent( - @NonNull List baseTags, - @NonNull String content, - @NonNull CalendarContent calendarContent) { - - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), Kind.CALENDAR_TIME_BASED_EVENT.getValue(), baseTags, content) - .create(); - - genericEvent.addTag(calendarContent.getIdentifierTag()); - genericEvent.addTag(createTitleTag(calendarContent.getTitle())); - genericEvent.addTag(createStartTag(calendarContent.getStart())); - - Optional geohashTag = calendarContent.getGeohashTag(); - geohashTag.ifPresent(genericEvent::addTag); - calendarContent.getEnd().ifPresent(aLong -> genericEvent.addTag(createEndTag(aLong))); - calendarContent.getStartTzid().ifPresent(s -> genericEvent.addTag(createStartTzidTag(s))); - calendarContent.getEndTzid().ifPresent(s -> genericEvent.addTag(createEndTzidTag(s))); - calendarContent.getSummary().ifPresent(s -> genericEvent.addTag(createSummaryTag(s))); - - calendarContent - .getImage() - .ifPresent( - s -> - genericEvent.addTag( - createImageTag( - Streams.failableStream(URI.create(s)).map(URI::toURL).stream() - .findFirst() - .orElseThrow()))); - - calendarContent.getParticipantPubKeyTags().forEach(genericEvent::addTag); - calendarContent.getLocation().ifPresent(s -> genericEvent.addTag(createLocationTag(s))); - calendarContent.getHashtagTags().forEach(genericEvent::addTag); - calendarContent.getReferenceTags().forEach(genericEvent::addTag); - calendarContent.getLabelTags().forEach(genericEvent::addTag); - - this.updateEvent(genericEvent); - - return this; - } - - @SuppressWarnings({"rawtypes","unchecked"}) - public NIP52 createCalendarRsvpEvent( - @NonNull String content, @NonNull CalendarRsvpContent calendarRsvpContent) { - - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.CALENDAR_RSVP_EVENT.getValue(), content).create(); - - // mandatory tags - genericEvent.addTag(calendarRsvpContent.getIdentifierTag()); - genericEvent.addTag(calendarRsvpContent.getAddressTag()); - genericEvent.addTag(createStatusTag(calendarRsvpContent.getStatus())); - - // optional tags - calendarRsvpContent.getAuthorPubKeyTag().ifPresent(genericEvent::addTag); - calendarRsvpContent.getEventTag().ifPresent(genericEvent::addTag); - calendarRsvpContent.getFbTag().ifPresent(genericEvent::addTag); - - this.updateEvent(genericEvent); - - return this; - } - - /** - * Create a date-based (all-day) calendar event using calendar content fields. - * - * @param content optional human-readable content/notes - * @param calendarContent the structured calendar content (identifier, title, dates) - * @return this instance for chaining - */ - @SuppressWarnings({"rawtypes","unchecked"}) - public NIP52 createDateBasedCalendarEvent( - @NonNull String content, @NonNull CalendarContent calendarContent) { - - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.CALENDAR_DATE_BASED_EVENT.getValue(), content) - .create(); - - // mandatory tags - genericEvent.addTag(calendarContent.getIdentifierTag()); - genericEvent.addTag(createTitleTag(calendarContent.getTitle())); - genericEvent.addTag(createStartTag(calendarContent.getStart())); - - // optional tags - calendarContent.getGeohashTag().ifPresent(genericEvent::addTag); - calendarContent.getEnd().ifPresent(s -> genericEvent.addTag(createEndTag(s))); - calendarContent.getStartTzid().ifPresent(s -> genericEvent.addTag(createStartTzidTag(s))); - calendarContent.getEndTzid().ifPresent(s -> genericEvent.addTag(createEndTzidTag(s))); - calendarContent.getSummary().ifPresent(s -> genericEvent.addTag(createSummaryTag(s))); - - this.updateEvent(genericEvent); - return this; - } - - public NIP52 addIdentifierTag(@NonNull String identifier) { - addTag(createIdentifierTag(identifier)); - return this; - } - - /** - * Add a title tag to the current calendar event. - * - * @param title the event title - * @return this instance for chaining - */ - public NIP52 addTitleTag(@NonNull String title) { - addTag(createTitleTag(title)); - return this; - } - - /** - * Add a start timestamp to the current calendar event. - * - * @param start unix timestamp (seconds) - * @return this instance for chaining - */ - public NIP52 addStartTag(@NonNull Long start) { - addTag(createStartTag(start)); - return this; - } - - /** - * Add an end timestamp to the current calendar event. - * - * @param end unix timestamp (seconds) - * @return this instance for chaining - */ - public NIP52 addEndTag(@NonNull Long end) { - addTag(createEndTag(end)); - return this; - } - - public NIP52 addEventTag(@NonNull EventTag eventTag) { - addTag(eventTag); - return this; - } - - /** - * Create a {@code start} tag specifying the start timestamp. - * - * @param start unix timestamp (seconds) - * @return the created tag - */ - public static BaseTag createStartTag(@NonNull Long start) { - return new BaseTagFactory(Constants.Tag.START_CODE, start.toString()).create(); - } - - /** - * Create an {@code end} tag specifying the end timestamp. - * - * @param end unix timestamp (seconds) - * @return the created tag - */ - public static BaseTag createEndTag(@NonNull Long end) { - return new BaseTagFactory(Constants.Tag.END_CODE, end.toString()).create(); - } - - /** - * Create a {@code start_tzid} tag specifying timezone ID for start. - * - * @param startTzid IANA timezone identifier for the start - * @return the created tag - */ - public static BaseTag createStartTzidTag(@NonNull String startTzid) { - return new BaseTagFactory(Constants.Tag.START_TZID_CODE, startTzid).create(); - } - - /** - * Create an {@code end_tzid} tag specifying timezone ID for end. - * - * @param endTzid IANA timezone identifier for the end - * @return the created tag - */ - public static BaseTag createEndTzidTag(@NonNull String endTzid) { - return new BaseTagFactory(Constants.Tag.END_TZID_CODE, endTzid).create(); - } - - /** - * Create a {@code fb} (free-busy) tag describing availability. - * - * @param fb the free-busy value (e.g., free/busy/tentative) - * @return the created tag - */ - public static BaseTag createFreeBusyTag(@NonNull String fb) { - return new BaseTagFactory(Constants.Tag.FREE_BUSY_CODE, fb).create(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP57.java b/nostr-java-api/src/main/java/nostr/api/NIP57.java deleted file mode 100644 index 6ecc592c2..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP57.java +++ /dev/null @@ -1,423 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.nip01.NIP01TagFactory; -import nostr.api.nip57.NIP57TagFactory; -import nostr.api.nip57.NIP57ZapReceiptBuilder; -import nostr.api.nip57.NIP57ZapRequestBuilder; -import nostr.api.nip57.ZapRequestParameters; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.entities.ZapRequest; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.EventTag; -import nostr.event.tag.RelaysTag; -import nostr.id.Identity; - -import java.util.List; - -/** - * NIP-57: Lightning Zaps. - * - *

This class provides utilities for creating and managing Lightning Network zaps on Nostr. Zaps - * are a standardized way to send Bitcoin payments (via Lightning Network) to Nostr users, content, - * or events, with the payment being publicly recorded on Nostr relays. - * - *

What are Zaps?

- * - *

Zaps enable Bitcoin micropayments on Nostr: - *

    - *
  • Zap Request (kind 9734): A request to send sats to a user or event
  • - *
  • Zap Receipt (kind 9735): Public proof that a payment was completed
  • - *
  • Lightning Integration: Uses LNURL and Lightning invoices (bolt11)
  • - *
  • Public Attribution: Zaps are publicly visible on relays (unlike tips)
  • - *
- * - *

How Zaps Work

- * - *
    - *
  1. User creates a zap request (kind 9734) specifying amount and recipient
  2. - *
  3. Request is sent to an LNURL server (specified in recipient's NIP-05 profile)
  4. - *
  5. LNURL server returns a Lightning invoice (bolt11)
  6. - *
  7. User pays the invoice via their Lightning wallet
  8. - *
  9. LNURL server publishes a zap receipt (kind 9735) to Nostr relays
  10. - *
  11. Receipt is visible to everyone as proof of payment
  12. - *
- * - *

Zap Types

- * - *
    - *
  • Public Zaps: Sender is visible (default)
  • - *
  • Private Zaps: Sender is anonymous (requires NIP-04 encryption)
  • - *
  • Profile Zaps: Zap a user's profile
  • - *
  • Event Zaps: Zap a specific note or event
  • - *
  • Anonymous Zaps: No sender attribution
  • - *
- * - *

Usage Examples

- * - *

Example 1: Create a Zap Request (Profile Zap)

- *
{@code
- * Identity sender = new Identity("nsec1...");
- * PublicKey recipient = new PublicKey("npub1...");
- *
- * NIP57 nip57 = new NIP57(sender);
- * nip57.createZapRequestEvent(
- *         1000L,                              // amount in millisatoshis
- *         "lnurl1...",                        // LNURL from recipient's profile
- *         List.of("wss://relay.damus.io"),   // relays to publish receipt
- *         "Great content! ⚡",                // optional comment
- *         recipient                           // recipient public key
- *     )
- *     .sign()
- *     .send(relays); // sends to LNURL server (not relays)
- * }
- * - *

Example 2: Create a Zap Request (Event Zap)

- *
{@code
- * Identity sender = new Identity("nsec1...");
- * GenericEvent noteToZap = ... // the note you want to zap
- * PublicKey author = noteToZap.getPubKey();
- *
- * NIP57 nip57 = new NIP57(sender);
- * nip57.createZapRequestEvent(
- *         5000L,                              // 5000 millisats
- *         "lnurl1...",                        // author's LNURL
- *         List.of("wss://relay.damus.io"),
- *         "Amazing post! 🔥",
- *         author,
- *         noteToZap,                          // the event being zapped
- *         null                                // no address tag (for kind 1 events)
- *     )
- *     .sign()
- *     .send(relays);
- * }
- * - *

Example 3: Create a Zap Request with Parameter Object

- *
{@code
- * Identity sender = new Identity("nsec1...");
- * PublicKey recipient = new PublicKey("npub1...");
- *
- * ZapRequestParameters params = ZapRequestParameters.builder()
- *     .amount(1000L)
- *     .lnUrl("lnurl1...")
- *     .relays(List.of(new Relay("wss://relay.damus.io")))
- *     .content("Thanks for the content!")
- *     .recipientPubKey(recipient)
- *     .build();
- *
- * NIP57 nip57 = new NIP57(sender);
- * nip57.createZapRequestEvent(params)
- *     .sign()
- *     .send(relays);
- * }
- * - *

Example 4: Create a Zap Receipt (LNURL server use case)

- *
{@code
- * // This is typically done by the LNURL server after payment is confirmed
- * Identity lnurlServer = new Identity("nsec_of_lnurl_server...");
- * GenericEvent zapRequest = ... // the original zap request
- * String bolt11 = "lnbc..."; // the paid Lightning invoice
- * String preimage = "..."; // payment preimage (proof of payment)
- * PublicKey recipient = zapRequest.getPubKey();
- *
- * NIP57 nip57 = new NIP57(lnurlServer);
- * nip57.createZapReceiptEvent(zapRequest, bolt11, preimage, recipient)
- *     .sign()
- *     .send(relays); // publishes receipt to Nostr
- * }
- * - *

Design Pattern

- * - *

This class follows the Facade Pattern combined with Builder Pattern: - *

    - *
  • Facade: Simplifies zap request/receipt creation
  • - *
  • Builder: {@link NIP57ZapRequestBuilder} and {@link NIP57ZapReceiptBuilder} handle construction
  • - *
  • Parameter Object: {@link ZapRequestParameters} groups related parameters
  • - *
  • Method Chaining: Fluent API for sign() and send()
  • - *
- * - *

Key Concepts

- * - *

Amount (millisatoshis)

- *

Amounts are specified in millisatoshis (msat = 1/1000 of a satoshi = 1/100,000,000,000 BTC). - *

    - *
  • 1 satoshi = 1,000 millisatoshis
  • - *
  • Example: 1000 msat = 1 sat ≈ $0.0006 USD (at $60k BTC)
  • - *
- * - *

LNURL

- *

Lightning URL (LNURL) is a protocol for Lightning payments. The recipient's LNURL is typically - * found in their NIP-05 profile metadata. The LNURL server generates invoices and publishes receipts. - * - *

Bolt11

- *

Bolt11 is the Lightning invoice format. It's a bech32-encoded payment request that includes: - *

    - *
  • Payment amount
  • - *
  • Payment hash
  • - *
  • Expiration time
  • - *
  • Routing hints
  • - *
- * - *

Event Tags

- * - *

Zap requests (kind 9734) include: - *

    - *
  • relays tag: Where the zap receipt should be published
  • - *
  • amount tag: Payment amount in millisatoshis
  • - *
  • lnurl tag: LNURL of the recipient
  • - *
  • p tag: Recipient's public key (optional for event zaps)
  • - *
  • e tag: Event ID being zapped (optional)
  • - *
  • a tag: Address tag for replaceable/parameterized events (optional)
  • - *
- * - *

Zap receipts (kind 9735) include: - *

    - *
  • bolt11 tag: The Lightning invoice that was paid
  • - *
  • preimage tag: Payment preimage (proof of payment)
  • - *
  • description tag: JSON-encoded zap request event
  • - *
  • p tag: Recipient's public key
  • - *
  • e tag: Original event ID (if event zap)
  • - *
- * - *

Thread Safety

- * - *

This class is not thread-safe for instance methods. Each thread should create - * its own {@code NIP57} instance. - * - * @see NIP-57 Specification - * @see NIP57ZapRequestBuilder - * @see NIP57ZapReceiptBuilder - * @see ZapRequestParameters - * @since 0.3.0 - */ -public class NIP57 extends EventNostr { - - private final NIP57ZapRequestBuilder zapRequestBuilder; - private final NIP57ZapReceiptBuilder zapReceiptBuilder; - - public NIP57(@NonNull Identity sender) { - super(sender); - this.zapRequestBuilder = new NIP57ZapRequestBuilder(sender); - this.zapReceiptBuilder = new NIP57ZapReceiptBuilder(sender); - } - - @Override - public NIP57 setSender(@NonNull Identity sender) { - super.setSender(sender); - this.zapRequestBuilder.updateDefaultSender(sender); - this.zapReceiptBuilder.updateDefaultSender(sender); - return this; - } - - /** - * Create a zap request event (kind 9734) using a structured request. - */ - public NIP57 createZapRequestEvent( - @NonNull ZapRequest zapRequest, - @NonNull String content, - PublicKey recipientPubKey, - GenericEvent zappedEvent, - BaseTag addressTag) { - this.updateEvent( - zapRequestBuilder.buildFromZapRequest( - resolveSender(), zapRequest, content, recipientPubKey, zappedEvent, addressTag)); - return this; - } - - /** - * Create a zap request event (kind 9734) using a parameter object. - */ - public NIP57 createZapRequestEvent(@NonNull ZapRequestParameters parameters) { - this.updateEvent(zapRequestBuilder.build(parameters)); - return this; - } - - /** - * Create a zap request event (kind 9734) using explicit parameters and a relays tag. - */ - public NIP57 createZapRequestEvent( - @NonNull Long amount, - @NonNull String lnUrl, - @NonNull BaseTag relaysTags, - @NonNull String content, - PublicKey recipientPubKey, - GenericEvent zappedEvent, - BaseTag addressTag) { - return createZapRequestEvent( - ZapRequestParameters.builder() - .amount(amount) - .lnUrl(lnUrl) - .relaysTag(requireRelaysTag(relaysTags)) - .content(content) - .recipientPubKey(recipientPubKey) - .zappedEvent(zappedEvent) - .addressTag(addressTag) - .build()); - } - - /** - * Create a zap request event (kind 9734) using explicit parameters and a list of relays. - */ - public NIP57 createZapRequestEvent( - @NonNull Long amount, - @NonNull String lnUrl, - @NonNull List relays, - @NonNull String content, - PublicKey recipientPubKey, - GenericEvent zappedEvent, - BaseTag addressTag) { - return createZapRequestEvent( - ZapRequestParameters.builder() - .amount(amount) - .lnUrl(lnUrl) - .relays(relays) - .content(content) - .recipientPubKey(recipientPubKey) - .zappedEvent(zappedEvent) - .addressTag(addressTag) - .build()); - } - - /** - * Create a zap request event (kind 9734) using explicit parameters and a list of relay URLs. - */ - public NIP57 createZapRequestEvent( - @NonNull Long amount, - @NonNull String lnUrl, - @NonNull List relays, - @NonNull String content, - PublicKey recipientPubKey) { - return createZapRequestEvent( - ZapRequestParameters.builder() - .amount(amount) - .lnUrl(lnUrl) - .relays(relays.stream().map(Relay::new).toList()) - .content(content) - .recipientPubKey(recipientPubKey) - .build()); - } - - /** - * Create a zap receipt event (kind 9735) acknowledging a zap payment. - */ - public NIP57 createZapReceiptEvent( - @NonNull GenericEvent zapRequestEvent, - @NonNull String bolt11, - @NonNull String preimage, - @NonNull PublicKey zapRecipient) { - this.updateEvent(zapReceiptBuilder.build(zapRequestEvent, bolt11, preimage, zapRecipient)); - return this; - } - - public NIP57 addLnurlTag(@NonNull String lnurl) { - getEvent().addTag(NIP57TagFactory.lnurl(lnurl)); - return this; - } - - public NIP57 addEventTag(@NonNull EventTag tag) { - getEvent().addTag(tag); - return this; - } - - public NIP57 addBolt11Tag(@NonNull String bolt11) { - getEvent().addTag(NIP57TagFactory.bolt11(bolt11)); - return this; - } - - public NIP57 addPreImageTag(@NonNull String preimage) { - getEvent().addTag(NIP57TagFactory.preimage(preimage)); - return this; - } - - public NIP57 addDescriptionTag(@NonNull String description) { - getEvent().addTag(NIP57TagFactory.description(description)); - return this; - } - - public NIP57 addAmountTag(@NonNull Integer amount) { - getEvent().addTag(NIP57TagFactory.amount(amount)); - return this; - } - - public NIP57 addRecipientTag(@NonNull PublicKey recipient) { - getEvent().addTag(NIP01TagFactory.pubKeyTag(recipient)); - return this; - } - - public NIP57 addZapTag(@NonNull PublicKey receiver, @NonNull List relays, Integer weight) { - getEvent().addTag(NIP57TagFactory.zap(receiver, relays, weight)); - return this; - } - - public NIP57 addZapTag(@NonNull PublicKey receiver, @NonNull List relays) { - getEvent().addTag(NIP57TagFactory.zap(receiver, relays)); - return this; - } - - public NIP57 addRelaysTag(@NonNull RelaysTag relaysTag) { - getEvent().addTag(relaysTag); - return this; - } - - public NIP57 addRelaysList(@NonNull List relays) { - return addRelaysTag(new RelaysTag(relays)); - } - - public NIP57 addRelays(@NonNull List relays) { - return addRelaysList(relays.stream().map(Relay::new).toList()); - } - - public NIP57 addRelays(@NonNull String... relays) { - return addRelays(List.of(relays)); - } - - public static BaseTag createLnurlTag(@NonNull String lnurl) { - return NIP57TagFactory.lnurl(lnurl); - } - - public static BaseTag createBolt11Tag(@NonNull String bolt11) { - return NIP57TagFactory.bolt11(bolt11); - } - - public static BaseTag createPreImageTag(@NonNull String preimage) { - return NIP57TagFactory.preimage(preimage); - } - - public static BaseTag createDescriptionTag(@NonNull String description) { - return NIP57TagFactory.description(description); - } - - public static BaseTag createAmountTag(@NonNull Number amount) { - return NIP57TagFactory.amount(amount); - } - - public static BaseTag createZapSenderPubKeyTag(@NonNull PublicKey publicKey) { - return NIP57TagFactory.zapSender(publicKey); - } - - public static BaseTag createZapTag( - @NonNull PublicKey receiver, @NonNull List relays, Integer weight) { - return NIP57TagFactory.zap(receiver, relays, weight); - } - - public static BaseTag createZapTag(@NonNull PublicKey receiver, @NonNull List relays) { - return NIP57TagFactory.zap(receiver, relays); - } - - private RelaysTag requireRelaysTag(BaseTag tag) { - if (tag instanceof RelaysTag relaysTag) { - return relaysTag; - } - throw new IllegalArgumentException("tag must be of type RelaysTag"); - } - - private Identity resolveSender() { - Identity sender = getSender(); - if (sender == null) { - throw new IllegalStateException("Sender identity is required for zap operations"); - } - return sender; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP60.java b/nostr-java-api/src/main/java/nostr/api/NIP60.java deleted file mode 100644 index 6ea224c3d..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP60.java +++ /dev/null @@ -1,484 +0,0 @@ -package nostr.api; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.base.Relay; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.entities.Amount; -import nostr.event.entities.CashuMint; -import nostr.event.entities.CashuQuote; -import nostr.event.entities.CashuToken; -import nostr.event.entities.CashuWallet; -import nostr.event.entities.SpendingHistory; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseTagEncoder; -import nostr.event.json.codec.EventEncodingException; -import nostr.id.Identity; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import static nostr.base.json.EventJsonMapper.mapper; - -/** - * NIP-60: Cashu Wallet over Nostr. - * - *

This class provides utilities for managing Cashu wallets on Nostr. Cashu is an ecash system - * for Bitcoin that enables private, custodial Bitcoin wallets. NIP-60 defines how to store and - * manage Cashu tokens, wallet metadata, transaction history, and quotes on Nostr relays. - * - *

What is Cashu?

- * - *

Cashu is a Chaumian ecash system for Bitcoin: - *

    - *
  • Ecash tokens: Bearer instruments backed by Bitcoin (like digital cash)
  • - *
  • Blind signatures: Mint can't link tokens to users (privacy)
  • - *
  • Custodial: Tokens are backed by Bitcoin held by the mint
  • - *
  • Transferable: Tokens can be sent peer-to-peer offline
  • - *
  • Lightweight: No blockchain, instant transactions
  • - *
- * - *

What is NIP-60?

- * - *

NIP-60 defines how to store Cashu wallet data on Nostr: - *

    - *
  • Wallet events (kind 37375): Wallet configuration and mint URLs
  • - *
  • Token events (kind 7375): Unspent Cashu tokens (proofs)
  • - *
  • History events (kind 7376): Transaction history
  • - *
  • Quote events (kind 7377): Reserved tokens for redemption
  • - *
- * - *

Benefits of storing Cashu on Nostr: - *

    - *
  • Backup: Tokens are backed up to relays (recover lost wallet)
  • - *
  • Sync: Multiple devices can access the same wallet
  • - *
  • Privacy: Events can be encrypted with NIP-04 or NIP-44
  • - *
  • Portable: Move wallets between clients
  • - *
- * - *

Event Kinds

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
KindNameDescription
37375Wallet EventWallet metadata: name, mints, relays, supported units
7375Token EventUnspent Cashu tokens (proofs) linked to a wallet
7376History EventTransaction history: send, receive, swap
7377Quote EventReserved tokens for Lightning redemption
- * - *

Usage Examples

- * - *

Example 1: Create a Wallet Event

- *
{@code
- * Identity walletOwner = new Identity("nsec1...");
- *
- * CashuWallet wallet = CashuWallet.builder()
- *     .name("My Cashu Wallet")
- *     .mints(List.of(
- *         new CashuMint("https://mint.minibits.cash/Bitcoin", List.of("sat", "msat"))
- *     ))
- *     .relays(List.of(new Relay("wss://relay.damus.io")))
- *     .unit("sat")
- *     .build();
- *
- * NIP60 nip60 = new NIP60(walletOwner);
- * nip60.createWalletEvent(wallet)
- *      .sign()
- *      .send(relays);
- * }
- * - *

Example 2: Create a Token Event (Store Unspent Tokens)

- *
{@code
- * Identity walletOwner = new Identity("nsec1...");
- * CashuWallet wallet = ... // existing wallet
- *
- * CashuToken token = CashuToken.builder()
- *     .mint("https://mint.minibits.cash/Bitcoin")
- *     .proofs(List.of(...)) // list of proofs from the mint
- *     .build();
- *
- * NIP60 nip60 = new NIP60(walletOwner);
- * nip60.createTokenEvent(token, wallet)
- *      .sign()
- *      .send(relays); // backup tokens to relays
- * }
- * - *

Example 3: Create a Spending History Event

- *
{@code
- * Identity walletOwner = new Identity("nsec1...");
- * CashuWallet wallet = ... // existing wallet
- *
- * SpendingHistory history = SpendingHistory.builder()
- *     .direction("out") // "in" or "out"
- *     .amount(new Amount(1000, "sat"))
- *     .timestamp(System.currentTimeMillis() / 1000)
- *     .description("Paid for coffee")
- *     .build();
- *
- * NIP60 nip60 = new NIP60(walletOwner);
- * nip60.createSpendingHistoryEvent(history, wallet)
- *      .sign()
- *      .send(relays);
- * }
- * - *

Example 4: Create a Redemption Quote Event

- *
{@code
- * Identity walletOwner = new Identity("nsec1...");
- *
- * CashuQuote quote = CashuQuote.builder()
- *     .quoteId("quote_abc123")
- *     .amount(new Amount(5000, "sat"))
- *     .mint("https://mint.minibits.cash/Bitcoin")
- *     .request("lnbc5000n...") // Lightning invoice
- *     .state("pending") // pending, paid, unpaid
- *     .build();
- *
- * NIP60 nip60 = new NIP60(walletOwner);
- * nip60.createRedemptionQuoteEvent(quote)
- *      .sign()
- *      .send(relays);
- * }
- * - *

Key Concepts

- * - *

Cashu Proofs

- *

Cashu proofs are the actual tokens. They are JSON objects containing: - *

    - *
  • id: Keyset ID (identifies the mint's keys)
  • - *
  • amount: Token denomination (e.g., 1, 2, 4, 8, 16... sats)
  • - *
  • secret: Random secret (proves ownership)
  • - *
  • C: Blinded signature from the mint
  • - *
- * - *

Mints

- *

Mints are custodians that issue Cashu tokens. Each mint: - *

    - *
  • Holds Bitcoin reserves backing the tokens
  • - *
  • Signs tokens with blind signatures
  • - *
  • Redeems tokens for Bitcoin (Lightning)
  • - *
  • Can support multiple units (sat, msat, USD, EUR, etc.)
  • - *
- * - *

Wallet Tags

- *

Wallet events use a 'd' tag to identify the wallet (like an address). Token, history, and - * quote events reference this 'd' tag to associate data with a specific wallet. - * - *

Security Considerations

- * - *
    - *
  • Encrypt events: Use NIP-04 or NIP-44 to encrypt token events (proofs are bearer instruments!)
  • - *
  • Relay trust: Relays can see encrypted data but not decrypt it
  • - *
  • Mint trust: Mints are custodial - they hold your Bitcoin
  • - *
  • Backup regularly: Sync tokens to relays to prevent loss
  • - *
  • Spent tokens: Delete spent token events to avoid confusion
  • - *
- * - *

Design Pattern

- * - *

This class follows the Facade Pattern: - *

    - *
  • Simplifies creation of NIP-60 events (wallet, token, history, quote)
  • - *
  • Delegates to {@link GenericEventFactory} for event construction
  • - *
  • Uses entity classes ({@link CashuWallet}, {@link CashuToken}, {@link SpendingHistory}, {@link CashuQuote})
  • - *
  • Provides static helper methods for tag creation
  • - *
- * - *

Thread Safety

- * - *

This class is not thread-safe for instance methods. Each thread should create - * its own {@code NIP60} instance. Static methods are thread-safe. - * - * @see NIP-60 Specification - * @see Cashu Documentation - * @see CashuWallet - * @see CashuToken - * @see SpendingHistory - * @see CashuQuote - * @since 0.6.0 - */ -public class NIP60 extends EventNostr { - - public NIP60(@NonNull Identity sender) { - setSender(sender); - } - - public NIP60 createWalletEvent(@NonNull CashuWallet wallet) { - GenericEvent walletEvent = - new GenericEventFactory( - getSender(), - Kind.WALLET.getValue(), - getWalletEventTags(wallet), - getWalletEventContent(wallet)) - .create(); - updateEvent(walletEvent); - return this; - } - - public NIP60 createTokenEvent(@NonNull CashuToken token, @NonNull CashuWallet wallet) { - GenericEvent tokenEvent = - new GenericEventFactory( - getSender(), - Kind.WALLET_UNSPENT_PROOF.getValue(), - getTokenEventTags(wallet), - getTokenEventContent(token)) - .create(); - updateEvent(tokenEvent); - return this; - } - - public NIP60 createSpendingHistoryEvent( - @NonNull SpendingHistory spendingHistory, @NonNull CashuWallet wallet) { - GenericEvent spendingHistoryEvent = - new GenericEventFactory( - getSender(), - Kind.WALLET_TX_HISTORY.getValue(), - getSpendingHistoryEventTags(wallet), - getSpendingHistoryEventContent(spendingHistory)) - .create(); - updateEvent(spendingHistoryEvent); - return this; - } - - public NIP60 createRedemptionQuoteEvent(@NonNull CashuQuote quote) { - GenericEvent redemptionQuoteEvent = - new GenericEventFactory( - getSender(), - Kind.RESERVED_CASHU_WALLET_TOKENS.getValue(), - getRedemptionQuoteEventTags(quote), - getRedemptionQuoteEventContent(quote)) - .create(); - updateEvent(redemptionQuoteEvent); - return this; - } - - /** - * Create a mint tag for a Cashu mint reference. - * - * @param mint the Cashu mint (contains URL and supported units) - * @return the created mint tag - */ - public static BaseTag createMintTag(@NonNull CashuMint mint) { - return createMintTag( - mint.getUrl(), mint.getUnits() != null ? mint.getUnits().toArray(new String[0]) : null); - } - - /** - * Create a mint tag for a Cashu mint reference. - * - * @param mintUrl the mint base URL - * @return the created mint tag - */ - public static BaseTag createMintTag(@NonNull String mintUrl) { - return createMintTag(mintUrl, (String[]) null); - } - - /** - * Create a mint tag for a Cashu mint reference with supported units. - * - * @param mintUrl the mint base URL - * @param units optional list of supported unit codes - * @return the created mint tag - */ - public static BaseTag createMintTag(@NonNull String mintUrl, String... units) { - List params = new ArrayList<>(); - params.add(mintUrl); - if (units != null && units.length > 0) { - params.addAll(Arrays.asList(units)); - } - return new BaseTagFactory(Constants.Tag.MINT_CODE, params.toArray(new String[0])).create(); - } - - /** - * Create a unit tag for Cashu amounts. - * - * @param unit the currency/unit code (e.g., sat, usd) - * @return the created unit tag - */ - public static BaseTag createUnitTag(@NonNull String unit) { - return new BaseTagFactory(Constants.Tag.UNIT_CODE, unit).create(); - } - - /** - * Create a wallet private key tag. - * - * @param privKey the wallet private key - * @return the created tag - */ - public static BaseTag createPrivKeyTag(@NonNull String privKey) { - return new BaseTagFactory(Constants.Tag.PRIVKEY_CODE, privKey).create(); - } - - /** - * Create a balance tag for a given unit. - * - * @param balance the wallet balance value - * @param unit the currency/unit code - * @return the created balance tag - */ - public static BaseTag createBalanceTag(@NonNull Integer balance, String unit) { - return new BaseTagFactory(Constants.Tag.BALANCE_CODE, balance.toString(), unit).create(); - } - - /** - * Create a direction tag for spending history entries. - * - * @param direction the spending direction (incoming/outgoing) - * @return the created direction tag - */ - public static BaseTag createDirectionTag(@NonNull SpendingHistory.Direction direction) { - return new BaseTagFactory(Constants.Tag.DIRECTION_CODE, direction.getValue()).create(); - } - - public static BaseTag createAmountTag(@NonNull Amount amount) { - return new BaseTagFactory( - Constants.Tag.AMOUNT_CODE, amount.getAmount().toString(), amount.getUnit()) - .create(); - } - - public static BaseTag createExpirationTag(@NonNull Long expiration) { - return new BaseTagFactory(Constants.Tag.EXPIRATION_CODE, expiration.toString()).create(); - } - - private String getWalletEventContent(@NonNull CashuWallet wallet) { - List tags = new ArrayList<>(); - Map> relayMap = wallet.getRelays(); - Set unitSet = relayMap.keySet(); - unitSet.forEach(u -> tags.add(NIP60.createBalanceTag(wallet.getBalance(), u))); - tags.add(NIP60.createPrivKeyTag(wallet.getPrivateKey())); - - try { - return NIP44.encrypt( - getSender(), mapper().writeValueAsString(tags), getSender().getPublicKey()); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to encode wallet content", ex); - } - } - - private String getTokenEventContent(@NonNull CashuToken token) { - try { - return NIP44.encrypt( - getSender(), mapper().writeValueAsString(token), getSender().getPublicKey()); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to encode token content", ex); - } - } - - private String getRedemptionQuoteEventContent(@NonNull CashuQuote quote) { - return NIP44.encrypt(getSender(), quote.getId(), getSender().getPublicKey()); - } - - private String getSpendingHistoryEventContent(@NonNull SpendingHistory spendingHistory) { - List tags = new ArrayList<>(); - tags.add(NIP60.createDirectionTag(spendingHistory.getDirection())); - tags.add(NIP60.createAmountTag(spendingHistory.getAmount())); - tags.addAll(spendingHistory.getEventTags()); - - return NIP44.encrypt(getSender(), getContent(tags), getSender().getPublicKey()); - } - - /** - * Encodes a list of tags to JSON array format. - * - *

Note: This could be extracted to a GenericTagListEncoder class if this pattern - * is used in multiple places. For now, it's kept here as it's NIP-60 specific. - */ - private String getContent(@NonNull List tags) { - return "[" - + tags.stream() - .map(tag -> new BaseTagEncoder(tag).encode()) - .collect(Collectors.joining(",")) - + "]"; - } - - private List getWalletEventTags(@NonNull CashuWallet wallet) { - List tags = new ArrayList<>(); - - Map> relayMap = wallet.getRelays(); - Set unitSet = relayMap.keySet(); - unitSet.forEach( - u -> { - tags.add(NIP60.createUnitTag(u)); - tags.add(NIP60.createBalanceTag(wallet.getBalance(), u)); - }); - - tags.add(NIP01.createIdentifierTag(wallet.getId())); - tags.add(NIP57.createDescriptionTag(wallet.getDescription())); - tags.add(NIP60.createPrivKeyTag(wallet.getPrivateKey())); - - if (wallet.getMints() != null) { - wallet.getMints().forEach(mint -> tags.add(NIP60.createMintTag(mint))); - } - - Map> relays = wallet.getRelays(); - relays - .keySet() - .forEach( - unit -> { - Set relaySet = wallet.getRelays(unit); - relaySet.forEach( - relay -> { - tags.add(NIP42.createRelayTag(relay)); - }); - }); - - return tags; - } - - private List getTokenEventTags(@NonNull CashuWallet wallet) { - List tags = new ArrayList<>(); - - tags.add( - NIP01.createAddressTag( - Kind.WALLET.getValue(), - getSender().getPublicKey(), - NIP01.createIdentifierTag(wallet.getId()), - null)); - - return tags; - } - - private List getSpendingHistoryEventTags(@NonNull CashuWallet wallet) { - return getTokenEventTags(wallet); - } - - private List getRedemptionQuoteEventTags(@NonNull CashuQuote quote) { - List tags = new ArrayList<>(); - tags.add(NIP60.createExpirationTag(quote.getExpiration())); - tags.add(NIP60.createMintTag(quote.getMint())); - tags.add( - NIP01.createAddressTag( - Kind.WALLET.getValue(), - getSender().getPublicKey(), - NIP01.createIdentifierTag(quote.getWallet().getId()), - null)); - return tags; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP61.java b/nostr-java-api/src/main/java/nostr/api/NIP61.java deleted file mode 100644 index 6b74b8a4e..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP61.java +++ /dev/null @@ -1,158 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.entities.CashuMint; -import nostr.event.entities.CashuProof; -import nostr.event.entities.NutZap; -import nostr.event.entities.NutZapInformation; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.EventTag; -import nostr.id.Identity; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.util.List; - -/** - * NIP-61 helpers (Cashu Nutzap). Build informational and payment events for Cashu zaps. - * Spec: NIP-61 - */ -public class NIP61 extends EventNostr { - - public NIP61(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create a Nutzap informational event (kind 7375) from a structured payload. - * - * @param nutZapInformation structured information including p2pk pubkey, relays and mints - * @return this instance for chaining - */ - public NIP61 createNutzapInformationalEvent(@NonNull NutZapInformation nutZapInformation) { - return createNutzapInformationalEvent( - List.of(nutZapInformation.getP2pkPubkey()), - nutZapInformation.getRelays(), - nutZapInformation.getMints()); - } - - /** - * Create a Nutzap informational event (kind 7375). - * - * @param p2pkPubkey list of p2pk pubkeys supported - * @param relays list of recommended relays - * @param mints list of Cashu mints - * @return this instance for chaining - */ - public NIP61 createNutzapInformationalEvent( - @NonNull List p2pkPubkey, - @NonNull List relays, - @NonNull List mints) { - - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.NUTZAP_INFORMATIONAL.getValue()).create(); - - relays.forEach(relay -> genericEvent.addTag(NIP42.createRelayTag(relay))); - mints.forEach(mint -> genericEvent.addTag(NIP60.createMintTag(mint))); - p2pkPubkey.forEach(pubkey -> genericEvent.addTag(NIP61.createP2pkTag(pubkey))); - - updateEvent(genericEvent); - - return this; - } - - /** - * Create a Nutzap event (kind 7374) from a structured payload. - * - * @param nutZap the structured Nutzap containing proofs, mint and optional target event - * @param content optional human-readable content - * @return this instance for chaining - */ - public NIP61 createNutzapEvent(@NonNull NutZap nutZap, @NonNull String content) { - try { - return createNutzapEvent( - nutZap.getProofs(), - URI.create(nutZap.getMint().getUrl()).toURL(), - nutZap.getNutZappedEvent(), - nutZap.getRecipient(), - content); - } catch (MalformedURLException ex) { - throw new IllegalArgumentException( - "Invalid mint URL for Nutzap event: " + nutZap.getMint().getUrl(), ex); - } - } - - /** - * Create a Nutzap event (kind 7374). - * - * @param proofs list of Cashu proofs - * @param url the mint URL - * @param nutzappedEventTag optional event being zapped (e-tag) - * @param recipient the recipient public key (p-tag) - * @param content optional human-readable content - * @return this instance for chaining - */ - public NIP61 createNutzapEvent( - List proofs, - @NonNull URL url, - EventTag nutzappedEventTag, - @NonNull PublicKey recipient, - @NonNull String content) { - - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.NUTZAP.getValue(), content).create(); - - proofs.forEach(proof -> genericEvent.addTag(NIP61.createProofTag(proof))); - - if (nutzappedEventTag != null) { - genericEvent.addTag(nutzappedEventTag); - } - genericEvent.addTag(NIP61.createUrlTag(url.toString())); - genericEvent.addTag(NIP01.createPubKeyTag(recipient)); - - updateEvent(genericEvent); - - return this; - } - - - - /** - * Create a {@code p2pk} tag. - * - * @param pubkey the p2pk pubkey string - * @return the created tag - */ - public static BaseTag createP2pkTag(@NonNull String pubkey) { - return new BaseTagFactory(Constants.Tag.P2PKH_CODE, pubkey).create(); - } - - /** - * Create a {@code url} tag. - * - * @param url the URL string - * @return the created tag - */ - public static BaseTag createUrlTag(@NonNull String url) { - return new BaseTagFactory(Constants.Tag.URL_CODE, url).create(); - } - - /** - * Create a {@code proof} tag from a Cashu proof. - * - * @param proof the Cashu proof - * @return the created tag - */ - public static BaseTag createProofTag(@NonNull CashuProof proof) { - return new BaseTagFactory(Constants.Tag.PROOF_CODE, proof.toString().replace("\"", "\\\"")) - .create(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP65.java b/nostr-java-api/src/main/java/nostr/api/NIP65.java deleted file mode 100644 index db2bbb99e..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP65.java +++ /dev/null @@ -1,136 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.base.Marker; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * NIP-65 helpers (Relay List Metadata). Build relay list events and r-tags. - * Spec: NIP-65 - */ -public class NIP65 extends EventNostr { - - public NIP65(@NonNull Identity sender) { - setSender(sender); - } - - /** - * Create a relay list metadata event (kind 10002) with a set of relay URLs. - * - * @param relayList the list of relays to include - * @return this instance for chaining - */ - @SuppressWarnings({"rawtypes","unchecked"}) - public NIP65 createRelayListMetadataEvent(@NonNull List relayList) { - List relayUrlTags = relayList.stream().map(relay -> createRelayUrlTag(relay)).toList(); - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), Kind.RELAY_LIST_METADATA.getValue(), relayUrlTags, "") - .create(); - this.updateEvent(genericEvent); - return this; - } - - /** - * Create a relay list metadata event (kind 10002) with a permission marker. - * - * @param relayList the list of relays to include - * @param permission the marker indicating read/write preference - * @return this instance for chaining - */ - @SuppressWarnings({"rawtypes","unchecked"}) - public NIP65 createRelayListMetadataEvent( - @NonNull List relayList, @NonNull Marker permission) { - List relayUrlTags = - relayList.stream().map(relay -> createRelayUrlTag(relay, permission)).toList(); - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), Kind.RELAY_LIST_METADATA.getValue(), relayUrlTags, "") - .create(); - this.updateEvent(genericEvent); - return this; - } - - /** - * Create a relay list metadata event (kind 10002) from a map of relays to markers. - * - * @param relayMarkerMap map from relay to permission marker - * @return this instance for chaining - */ - @SuppressWarnings({"rawtypes","unchecked"}) - public NIP65 createRelayListMetadataEvent(@NonNull Map relayMarkerMap) { - List relayUrlTags = new ArrayList<>(); - for (Map.Entry entry : relayMarkerMap.entrySet()) { - relayUrlTags.add(createRelayUrlTag(entry.getKey(), entry.getValue())); - } - GenericEvent genericEvent = - new GenericEventFactory( - getSender(), Kind.RELAY_LIST_METADATA.getValue(), relayUrlTags, "") - .create(); - this.updateEvent(genericEvent); - return this; - } - - /** - * Add a relay URL tag with permission marker to the current event. - * - * @param url the relay URL - * @param permission the marker indicating read/write preference - * @return this instance for chaining - */ - public NIP65 addRelayUrlTag(@NonNull String url, @NonNull Marker permission) { - this.getEvent().addTag(createRelayUrlTag(url, permission)); - return this; - } - - /** - * Create an {@code r} tag for a relay URL. - * - * @param relay the relay - * @return the created tag - */ - public static BaseTag createRelayUrlTag(@NonNull Relay relay) { - return BaseTag.create("r", relay.getUri()); - } - - /** - * Create an {@code r} tag for a relay URL. - * - * @param url the relay URL - * @return the created tag - */ - public static BaseTag createRelayUrlTag(@NonNull String url) { - return BaseTag.create("r", url); - } - - /** - * Create an {@code r} tag with a permission marker. - * - * @param url the relay URL - * @param permission the marker indicating read/write preference - * @return the created tag - */ - public static BaseTag createRelayUrlTag(@NonNull String url, @NonNull Marker permission) { - return BaseTag.create("r", url, permission.getValue()); - } - - /** - * Create an {@code r} tag for a relay with a permission marker. - * - * @param relay the relay instance - * @param permission the marker indicating read/write preference - * @return the created tag - */ - public static BaseTag createRelayUrlTag(@NonNull Relay relay, @NonNull Marker permission) { - return BaseTag.create("r", relay.getUri(), permission.getValue()); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NIP99.java b/nostr-java-api/src/main/java/nostr/api/NIP99.java deleted file mode 100644 index f68d59f08..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NIP99.java +++ /dev/null @@ -1,130 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.entities.ClassifiedListing; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -import java.net.URL; -import java.util.List; - -import static nostr.api.NIP12.createGeohashTag; -import static nostr.api.NIP12.createHashtagTag; -import static nostr.api.NIP23.createImageTag; -import static nostr.api.NIP23.createPublishedAtTag; -import static nostr.api.NIP23.createSummaryTag; -import static nostr.api.NIP23.createTitleTag; - -/** - * NIP-99 helpers (Classified Listings). Build classified listing events and tags. - * Spec: NIP-99 - */ -public class NIP99 extends EventNostr { - - public NIP99(@NonNull Identity sender) { - setSender(sender); - } - - @SuppressWarnings({"rawtypes","unchecked"}) - public NIP99 createClassifiedListingEvent( - @NonNull List baseTags, - String content, - @NonNull ClassifiedListing classifiedListing) { - GenericEvent genericEvent = - new GenericEventFactory(getSender(), Kind.CLASSIFIED_LISTING.getValue(), baseTags, content) - .create(); - - genericEvent.addTag(createTitleTag(classifiedListing.getTitle())); - genericEvent.addTag(createSummaryTag(classifiedListing.getSummary())); - - if (classifiedListing.getPublishedAt() != null) { - genericEvent.addTag(createPublishedAtTag(classifiedListing.getPublishedAt())); - } - - if (classifiedListing.getLocation() != null) { - genericEvent.addTag(createLocationTag(classifiedListing.getLocation())); - } - - genericEvent.addTag(classifiedListing.getPriceTag()); - - updateEvent(genericEvent); - - return this; - } - - public static BaseTag createLocationTag(@NonNull String location) { - return new BaseTagFactory(Constants.Tag.LOCATION_CODE, location).create(); - } - - public static BaseTag createPriceTag(@NonNull String price, @NonNull String currency) { - return new BaseTagFactory(Constants.Tag.PRICE_CODE, price, currency, null).create(); - } - - public static BaseTag createPriceTag( - @NonNull String price, @NonNull String currency, String frequency) { - return new BaseTagFactory(Constants.Tag.PRICE_CODE, price, currency, frequency).create(); - } - - public static BaseTag createStatusTag(@NonNull String status) { - return new BaseTagFactory(Constants.Tag.STATUS_CODE, status).create(); - } - - public NIP99 addHashtagTag(@NonNull String hashtag) { - getEvent().addTag(createHashtagTag(hashtag)); - return this; - } - - public NIP99 addLocationTag(@NonNull String location) { - getEvent().addTag(createLocationTag(location)); - return this; - } - - public NIP99 addGeohashTag(@NonNull String geohash) { - getEvent().addTag(createGeohashTag(geohash)); - return this; - } - - public NIP99 addPriceTag(@NonNull String price, @NonNull String currency, String frequency) { - getEvent().addTag(createPriceTag(price, currency, frequency)); - return this; - } - - public NIP99 addPriceTag(@NonNull String price, @NonNull String currency) { - return addPriceTag(price, currency, null); - } - - public NIP99 addTitleTag(@NonNull String title) { - getEvent().addTag(createTitleTag(title)); - return this; - } - - public NIP99 addSummaryTag(@NonNull String summary) { - getEvent().addTag(createSummaryTag(summary)); - return this; - } - - public NIP99 addPublishedAtTag(@NonNull Long date) { - getEvent().addTag(createPublishedAtTag(date)); - return this; - } - - public NIP99 addImageTag(@NonNull URL url, String size) { - getEvent().addTag(createImageTag(url, size)); - return this; - } - - public NIP99 addStatusTag(@NonNull String status) { - getEvent().addTag(createStatusTag(status)); - return this; - } - - public NIP99 addTag(@NonNull BaseTag tag) { - getEvent().addTag(tag); - return this; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NostrIF.java b/nostr-java-api/src/main/java/nostr/api/NostrIF.java deleted file mode 100644 index e2ef733de..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NostrIF.java +++ /dev/null @@ -1,160 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.base.IEvent; -import nostr.base.ISignable; -import nostr.event.filter.Filters; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; - -/** - * Core client interface for sending Nostr events and REQ messages to relays, signing and verifying - * events, and managing sender/relay configuration. - */ -public interface NostrIF { - /** - * Set the sender identity used to sign events. - * - * @param sender the identity - * @return this instance for chaining - */ - NostrIF setSender(@NonNull Identity sender); - - /** - * Configure relays for sending and requesting events. - * - * @param relays a map from relay name to relay URI - * @return this instance for chaining - */ - NostrIF setRelays(@NonNull Map relays); - - /** - * Send a single event to the configured relays. - * - * @param event the event to send - * @return a list of relay responses (raw JSON messages) - */ - List sendEvent(@NonNull IEvent event); - - /** - * Send a single event to the provided relays. - * - * @param event the event to send - * @param relays relay map (name -> URI) to use for this send - * @return a list of relay responses (raw JSON messages) - */ - List sendEvent(@NonNull IEvent event, Map relays); - - /** - * Send a REQ request with a single filter to the configured relays. - * - * @param filters the filter - * @param subscriptionId the subscription identifier - * @return a list of relay responses (raw JSON messages) - */ - List sendRequest(@NonNull Filters filters, @NonNull String subscriptionId); - - /** - * Send a REQ request with a single filter to provided relays. - * - * @param filters the filter - * @param subscriptionId the subscription identifier - * @param relays relay map (name -> URI) - * @return a list of relay responses (raw JSON messages) - */ - List sendRequest( - @NonNull Filters filters, @NonNull String subscriptionId, Map relays); - - /** - * Send a REQ request with multiple filters to the configured relays. - * - * @param filtersList filters to apply - * @param subscriptionId the subscription identifier - * @return a list of relay responses (raw JSON messages) - */ - List sendRequest(@NonNull List filtersList, @NonNull String subscriptionId); - - /** - * Send a REQ request with multiple filters to provided relays. - * - * @param filtersList filters to apply - * @param subscriptionId the subscription identifier - * @param relays relay map (name -> URI) - * @return a list of relay responses (raw JSON messages) - */ - List sendRequest( - @NonNull List filtersList, - @NonNull String subscriptionId, - Map relays); - - /** - * Subscribe to a stream of events for the given filter on configured relays. - * - * @param filters the filter describing events to stream - * @param subscriptionId identifier for the subscription - * @param listener consumer invoked for each raw relay message - * @return a handle that cancels the subscription when closed - */ - AutoCloseable subscribe( - @NonNull Filters filters, - @NonNull String subscriptionId, - @NonNull Consumer listener); - - /** - * Subscribe to a stream of events with custom error handling. - * - * @param filters the filter describing events to stream - * @param subscriptionId identifier for the subscription - * @param listener consumer invoked for each raw relay message - * @param errorListener optional consumer invoked when a transport error occurs - * @return a handle that cancels the subscription when closed - */ - AutoCloseable subscribe( - @NonNull Filters filters, - @NonNull String subscriptionId, - @NonNull Consumer listener, - Consumer errorListener); - - /** - * Sign a signable object with the provided identity. - * - * @param identity the identity providing the private key - * @param signable the object to sign - * @return this instance for chaining - */ - NostrIF sign(@NonNull Identity identity, @NonNull ISignable signable); - - /** - * Verify the Schnorr signature of a GenericEvent. - * - * @param event the event to verify - * @return true if signature is valid - */ - boolean verify(@NonNull GenericEvent event); - - /** - * Get the configured sender identity. - * - * @return the sender identity - */ - Identity getSender(); - - /** - * Get the configured relays map. - * - * @return relay map (name -> URI) - */ - Map getRelays(); - - /** - * Close all underlying WebSocket clients. - * - * @throws IOException if an I/O error occurs - */ - void close() throws IOException; -} diff --git a/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java b/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java deleted file mode 100644 index 133315968..000000000 --- a/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java +++ /dev/null @@ -1,276 +0,0 @@ -package nostr.api; - -import lombok.Getter; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import nostr.api.client.NostrEventDispatcher; -import nostr.api.client.NostrRelayRegistry; -import nostr.api.client.NostrRequestDispatcher; -import nostr.api.client.NostrSubscriptionManager; -import nostr.api.client.WebSocketClientHandlerFactory; -import nostr.api.service.NoteService; -import nostr.api.service.impl.DefaultNoteService; -import nostr.base.IEvent; -import nostr.base.ISignable; -import nostr.base.RelayUri; -import nostr.base.SubscriptionId; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.SpringWebSocketClientFactory; -import nostr.event.filter.Filters; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; - -/** - * Default Nostr client using Spring WebSocket clients to send events and requests to relays. - */ -@Slf4j -public class NostrSpringWebSocketClient implements NostrIF { - - private final NostrRelayRegistry relayRegistry; - private final NostrEventDispatcher eventDispatcher; - private final NostrRequestDispatcher requestDispatcher; - private final NostrSubscriptionManager subscriptionManager; - private final WebSocketClientFactory clientFactory; - private final NoteService noteService; - - @Getter private Identity sender; - - public NostrSpringWebSocketClient() { - this(null, new DefaultNoteService(), new SpringWebSocketClientFactory()); - } - - /** - * Construct a client with a single relay configured. - */ - public NostrSpringWebSocketClient(String relayName, String relayUri) { - this(); - setRelays(Map.of(relayName, relayUri)); - } - - /** - * Construct a client with a custom note service implementation. - */ - public NostrSpringWebSocketClient(@NonNull NoteService noteService) { - this(null, noteService, new SpringWebSocketClientFactory()); - } - - /** - * Construct a client with a sender identity and a custom note service. - */ - public NostrSpringWebSocketClient(@NonNull Identity sender, @NonNull NoteService noteService) { - this(sender, noteService, new SpringWebSocketClientFactory()); - } - - public NostrSpringWebSocketClient( - Identity sender, - @NonNull NoteService noteService, - @NonNull WebSocketClientFactory clientFactory) { - this.sender = sender; - this.noteService = noteService; - this.clientFactory = clientFactory; - this.relayRegistry = new NostrRelayRegistry(buildFactory()); - this.eventDispatcher = new NostrEventDispatcher(this.noteService, this.relayRegistry); - this.requestDispatcher = new NostrRequestDispatcher(this.relayRegistry); - this.subscriptionManager = new NostrSubscriptionManager(this.relayRegistry); - } - - /** - * Construct a client with a sender identity. - */ - public NostrSpringWebSocketClient(@NonNull Identity sender) { - this(sender, new DefaultNoteService()); - } - - /** - * Get a singleton instance of the client without a preconfigured sender. - */ - private static final class InstanceHolder { - private static final NostrSpringWebSocketClient INSTANCE = new NostrSpringWebSocketClient(); - - private InstanceHolder() {} - } - - /** - * Get a lazily initialized singleton instance of the client without a preconfigured sender. - */ - public static NostrIF getInstance() { - return InstanceHolder.INSTANCE; - } - - /** - * Get a lazily initialized singleton instance of the client, configuring the sender if unset. - */ - public static NostrIF getInstance(@NonNull Identity sender) { - NostrSpringWebSocketClient instance = InstanceHolder.INSTANCE; - if (instance.getSender() == null) { - synchronized (instance) { - if (instance.getSender() == null) { - instance.setSender(sender); - } - } - } - return instance; - } - - @Override - public NostrIF setSender(@NonNull Identity sender) { - this.sender = sender; - return this; - } - - @Override - public NostrIF setRelays(@NonNull Map relays) { - relayRegistry.registerRelays(relays); - return this; - } - - @Override - public List sendEvent(@NonNull IEvent event) { - return eventDispatcher.send(event); - } - - @Override - public List sendEvent(@NonNull IEvent event, Map relays) { - setRelays(relays); - return sendEvent(event); - } - - @Override - public List sendRequest(@NonNull Filters filters, @NonNull String subscriptionId) { - return requestDispatcher.sendRequest(filters, SubscriptionId.of(subscriptionId)); - } - - @Override - public List sendRequest( - @NonNull Filters filters, @NonNull String subscriptionId, Map relays) { - setRelays(relays); - return sendRequest(filters, subscriptionId); - } - - @Override - public List sendRequest( - @NonNull List filtersList, @NonNull String subscriptionId, Map relays) { - setRelays(relays); - return sendRequest(filtersList, subscriptionId); - } - - @Override - public List sendRequest(@NonNull List filtersList, @NonNull String subscriptionId) { - return requestDispatcher.sendRequest(filtersList, subscriptionId); - } - - public static List sendRequest( - @NonNull SpringWebSocketClient client, - @NonNull Filters filters, - @NonNull String subscriptionId) - throws IOException { - return NostrRequestDispatcher.sendRequest(client, filters, subscriptionId); - } - - @Override - public AutoCloseable subscribe( - @NonNull Filters filters, - @NonNull String subscriptionId, - @NonNull Consumer listener) { - return subscribe(filters, subscriptionId, listener, null); - } - - @Override - public AutoCloseable subscribe( - @NonNull Filters filters, - @NonNull String subscriptionId, - @NonNull Consumer listener, - Consumer errorListener) { - SubscriptionId id = SubscriptionId.of(subscriptionId); - Consumer safeError = - errorListener != null - ? errorListener - : throwable -> - log.warn( - "Subscription error for {} on relays {}", - id.value(), - relayRegistry.getClientMap().keySet(), - throwable); - - return subscriptionManager.subscribe(filters, id.value(), listener, safeError); - } - - @Override - public NostrIF sign(@NonNull Identity identity, @NonNull ISignable signable) { - identity.sign(signable); - return this; - } - - @Override - public boolean verify(@NonNull GenericEvent event) { - return eventDispatcher.verify(event); - } - - @Override - public Map getRelays() { - return relayRegistry.snapshotRelays(); - } - - /** - * Returns a map of relay name to the last send failure Throwable, if available. - * - *

When using {@link DefaultNoteService}, failures encountered during the last send on this - * thread are recorded for diagnostics. For other NoteService implementations, this returns an - * empty map. - */ - public Map getLastSendFailures() { - if (this.noteService instanceof DefaultNoteService d) { - return d.getLastFailures(); - } - return new HashMap<>(); - } - - /** - * Returns structured failure details when using {@link DefaultNoteService}. - * - * @see DefaultNoteService#getLastFailureDetails() - */ - public Map getLastSendFailureDetails() { - if (this.noteService instanceof DefaultNoteService d) { - return d.getLastFailureDetails(); - } - return new HashMap<>(); - } - - /** - * Registers a failure listener when using {@link DefaultNoteService}. No‑op otherwise. - * - *

The listener receives a relay‑name → exception map after each call to - * {@link #sendEvent(nostr.base.IEvent)}. - * - * @param listener consumer of last failures (may be {@code null} to clear) - * @return this client for chaining - */ - public NostrSpringWebSocketClient onSendFailures(java.util.function.Consumer> listener) { - if (this.noteService instanceof DefaultNoteService d) { - d.setFailureListener(listener); - } - return this; - } - - public void close() throws IOException { - relayRegistry.closeAll(); - } - - protected WebSocketClientHandler newWebSocketClientHandler(String relayName, RelayUri relayUri) - throws ExecutionException, InterruptedException { - return new WebSocketClientHandler(relayName, relayUri, clientFactory); - } - - private WebSocketClientHandlerFactory buildFactory() { - return this::newWebSocketClientHandler; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/WebSocketClientHandler.java b/nostr-java-api/src/main/java/nostr/api/WebSocketClientHandler.java deleted file mode 100644 index 368a19a63..000000000 --- a/nostr-java-api/src/main/java/nostr/api/WebSocketClientHandler.java +++ /dev/null @@ -1,283 +0,0 @@ -package nostr.api; - -import lombok.Getter; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import nostr.base.IEvent; -import nostr.base.RelayUri; -import nostr.base.SubscriptionId; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.SpringWebSocketClientFactory; -import nostr.event.filter.Filters; -import nostr.event.impl.GenericEvent; -import nostr.event.message.CloseMessage; -import nostr.event.message.EventMessage; -import nostr.event.message.ReqMessage; - -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; -import java.util.function.Function; - -/** - * Internal helper managing a relay connection and per-subscription request clients. - */ -@Slf4j -public class WebSocketClientHandler { - private final SpringWebSocketClient eventClient; - private final Map requestClientMap = - new ConcurrentHashMap<>(); - private final Function requestClientFactory; - private final WebSocketClientFactory clientFactory; - - @Getter private final String relayName; - @Getter private final RelayUri relayUri; - - /** - * Create a handler for a specific relay. - * - * @param relayName human-friendly relay name - * @param relayUri relay WebSocket URI - */ - protected WebSocketClientHandler(@NonNull String relayName, @NonNull String relayUri) - throws ExecutionException, InterruptedException { - this(relayName, new RelayUri(relayUri), new SpringWebSocketClientFactory()); - } - - protected WebSocketClientHandler( - @NonNull String relayName, - @NonNull RelayUri relayUri, - @NonNull WebSocketClientFactory clientFactory) - throws ExecutionException, InterruptedException { - this( - relayName, - relayUri, - new SpringWebSocketClient(clientFactory.create(relayUri), relayUri.toString()), - null, - null, - clientFactory); - } - - public WebSocketClientHandler( - @NonNull String relayName, - @NonNull RelayUri relayUri, - @NonNull SpringWebSocketClient eventClient, - Map requestClients, - Function requestClientFactory, - @NonNull WebSocketClientFactory clientFactory) { - this.relayName = relayName; - this.relayUri = relayUri; - this.eventClient = eventClient; - this.clientFactory = clientFactory; - this.requestClientFactory = - requestClientFactory != null ? requestClientFactory : key -> createRequestClient(); - if (requestClients != null) { - this.requestClientMap.putAll(requestClients); - } - } - - /** - * Send an event message to the relay using the main client. - * - * @param event the event to send - * @return relay responses (raw JSON messages) - */ - public List sendEvent(@NonNull IEvent event) { - ((GenericEvent) event).validate(); - try { - return eventClient.send(new EventMessage(event)).stream().toList(); - } catch (IOException e) { - throw new RuntimeException("Failed to send event", e); - } - } - - /** - * Send a REQ message on a per-subscription client associated with this handler. - * - * @param filters the filter - * @param subscriptionId the subscription identifier - * @return relay responses (raw JSON messages) - */ - public List sendRequest( - @NonNull Filters filters, @NonNull SubscriptionId subscriptionId) { - try { - @SuppressWarnings("resource") - SpringWebSocketClient client = getOrCreateRequestClient(subscriptionId); - return client.send(new ReqMessage(subscriptionId.value(), filters)); - } catch (IOException e) { - throw new RuntimeException("Failed to send request", e); - } - } - - public AutoCloseable subscribe( - @NonNull Filters filters, - @NonNull String subscriptionId, - @NonNull Consumer listener, - Consumer errorListener) { - SubscriptionId id = SubscriptionId.of(subscriptionId); - @SuppressWarnings("resource") - SpringWebSocketClient client = getOrCreateRequestClient(id); - Consumer safeError = resolveErrorListener(id, errorListener); - AutoCloseable delegate = openSubscription(client, filters, id, listener, safeError); - - return new SubscriptionHandle(id, client, delegate, safeError); - } - - private Consumer resolveErrorListener( - SubscriptionId subscriptionId, Consumer errorListener) { - if (errorListener != null) { - return errorListener; - } - return throwable -> - log.warn( - "Subscription error on relay {} for {}", relayName, subscriptionId.value(), throwable); - } - - private AutoCloseable openSubscription( - SpringWebSocketClient client, - Filters filters, - SubscriptionId subscriptionId, - Consumer listener, - Consumer errorListener) { - try { - return client.subscribe( - new ReqMessage(subscriptionId.value(), filters), - listener, - errorListener, - () -> - errorListener.accept( - new IOException( - "Subscription closed by relay %s for id %s" - .formatted(relayName, subscriptionId.value())))); - } catch (IOException e) { - errorListener.accept(e); - throw new RuntimeException("Failed to establish subscription", e); - } - } - - private final class SubscriptionHandle implements AutoCloseable { - private final SubscriptionId subscriptionId; - private final SpringWebSocketClient client; - private final AutoCloseable delegate; - private final Consumer errorListener; - - private SubscriptionHandle( - SubscriptionId subscriptionId, - SpringWebSocketClient client, - AutoCloseable delegate, - Consumer errorListener) { - this.subscriptionId = subscriptionId; - this.client = client; - this.delegate = delegate; - this.errorListener = errorListener; - } - - @Override - public void close() throws IOException { - CloseAccumulator accumulator = new CloseAccumulator(errorListener); - AutoCloseable closeFrameHandle = openCloseFrame(subscriptionId, accumulator); - closeQuietly(closeFrameHandle, accumulator); - closeQuietly(delegate, accumulator); - closeQuietly(client, accumulator); - - requestClientMap.remove(subscriptionId); - accumulator.rethrowIfNecessary(); - } - - private AutoCloseable openCloseFrame( - SubscriptionId subscriptionId, CloseAccumulator accumulator) { - try { - return client.subscribe( - new CloseMessage(subscriptionId.value()), - message -> {}, - errorListener, - null); - } catch (IOException e) { - accumulator.record(e); - return null; - } - } - } - - private void closeQuietly(AutoCloseable closeable, CloseAccumulator accumulator) { - if (closeable == null) { - return; - } - try { - closeable.close(); - } catch (IOException e) { - accumulator.record(e); - } catch (Exception e) { - accumulator.record(e); - } - } - - private static final class CloseAccumulator { - private final Consumer errorListener; - private IOException ioFailure; - private Exception nonIoFailure; - - private CloseAccumulator(Consumer errorListener) { - this.errorListener = errorListener; - } - - private void record(IOException exception) { - errorListener.accept(exception); - if (ioFailure == null) { - ioFailure = exception; - } - } - - private void record(Exception exception) { - errorListener.accept(exception); - if (nonIoFailure == null) { - nonIoFailure = exception; - } - } - - private void rethrowIfNecessary() throws IOException { - if (ioFailure != null) { - throw ioFailure; - } - if (nonIoFailure != null) { - throw new IOException("Failed to close subscription cleanly", nonIoFailure); - } - } - } - - /** - * Close the event client and any per-subscription request clients. - */ - public void close() throws IOException { - eventClient.close(); - for (SpringWebSocketClient client : requestClientMap.values()) { - client.close(); - } - } - - protected SpringWebSocketClient getOrCreateRequestClient(SubscriptionId subscriptionId) { - try { - return requestClientMap.computeIfAbsent(subscriptionId, requestClientFactory); - } catch (RuntimeException e) { - if (e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - throw e; - } - } - - private SpringWebSocketClient createRequestClient() { - try { - return new SpringWebSocketClient(clientFactory.create(relayUri), relayUri.toString()); - } catch (ExecutionException e) { - throw new RuntimeException("Failed to initialize request client", e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted while initializing request client", e); - } - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/client/NostrEventDispatcher.java b/nostr-java-api/src/main/java/nostr/api/client/NostrEventDispatcher.java deleted file mode 100644 index b09f69355..000000000 --- a/nostr-java-api/src/main/java/nostr/api/client/NostrEventDispatcher.java +++ /dev/null @@ -1,77 +0,0 @@ -package nostr.api.client; - -import lombok.NonNull; -import nostr.api.service.NoteService; -import nostr.base.IEvent; -import nostr.crypto.schnorr.Schnorr; -import nostr.crypto.schnorr.SchnorrException; -import nostr.event.impl.GenericEvent; -import nostr.util.NostrUtil; - -import java.security.NoSuchAlgorithmException; -import java.util.List; - -/** - * Handles event verification and dispatching to relays. - * - *

Performs BIP-340 Schnorr signature verification before forwarding events to all configured - * relays. - * - * @see nostr.crypto.schnorr.Schnorr - * @see NIP-01 - */ -public final class NostrEventDispatcher { - - private final NoteService noteService; - private final NostrRelayRegistry relayRegistry; - - /** - * Create a dispatcher that uses the provided services to verify and distribute events. - * - * @param noteService service responsible for communicating with relays - * @param relayRegistry registry that tracks the connected relay handlers - */ - public NostrEventDispatcher(NoteService noteService, NostrRelayRegistry relayRegistry) { - this.noteService = noteService; - this.relayRegistry = relayRegistry; - } - - /** - * Verify the supplied event and forward it to all configured relays. - * - * @param event event to send - * @return responses returned by relays - * @throws IllegalStateException if verification fails - */ - public List send(@NonNull IEvent event) { - if (event instanceof GenericEvent genericEvent) { - if (!verify(genericEvent)) { - throw new IllegalStateException("Event verification failed"); - } - } - return noteService.send(event, relayRegistry.getClientMap()); - } - - /** - * Verify the Schnorr signature of the provided event. - * - * @param event event to verify - * @return {@code true} if the signature is valid - * @throws IllegalStateException if the event is unsigned or verification cannot complete - */ - public boolean verify(@NonNull GenericEvent event) { - if (!event.isSigned()) { - throw new IllegalStateException("The event is not signed"); - } - try { - return Schnorr.verify( - NostrUtil.sha256(event.getSerializedEventCache()), - event.getPubKey().getRawData(), - event.getSignature().getRawData()); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("SHA-256 algorithm not available", e); - } catch (SchnorrException e) { - throw new IllegalStateException("Failed to verify Schnorr signature", e); - } - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/client/NostrRelayRegistry.java b/nostr-java-api/src/main/java/nostr/api/client/NostrRelayRegistry.java deleted file mode 100644 index 2376d7259..000000000 --- a/nostr-java-api/src/main/java/nostr/api/client/NostrRelayRegistry.java +++ /dev/null @@ -1,126 +0,0 @@ -package nostr.api.client; - -import nostr.api.WebSocketClientHandler; -import nostr.base.RelayUri; -import nostr.base.SubscriptionId; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; - -/** - * Manages the lifecycle of {@link WebSocketClientHandler} instances keyed by relay name. - */ -public class NostrRelayRegistry { - - private final Map clientMap = new ConcurrentHashMap<>(); - private final WebSocketClientHandlerFactory factory; - - /** - * Create a registry backed by the supplied handler factory. - * - * @param factory factory used to lazily create relay handlers - */ - public NostrRelayRegistry(WebSocketClientHandlerFactory factory) { - this.factory = factory; - } - - /** - * Expose the internal handler map for read-only scenarios. - * - * @return relay name to handler map - */ - public Map getClientMap() { - return clientMap; - } - - /** - * Ensure handlers exist for the provided relay definitions. - * - * @param relays mapping of relay names to relay URIs - */ - public void registerRelays(Map relays) { - for (Entry relayEntry : relays.entrySet()) { - clientMap.computeIfAbsent( - relayEntry.getKey(), - key -> createHandler(key, new RelayUri(relayEntry.getValue()))); - } - } - - /** - * Take a snapshot of the currently registered relay URIs. - * - * @return immutable copy of relay name to URI mappings - */ - public Map snapshotRelays() { - return clientMap.values().stream() - .collect( - Collectors.toMap( - WebSocketClientHandler::getRelayName, - handler -> handler.getRelayUri().toString(), - (prev, next) -> next, - HashMap::new)); - } - - /** - * Return handlers that correspond to base relay connections (non request-scoped). - * - * @return list of base handlers - */ - public List baseHandlers() { - return clientMap.entrySet().stream() - .filter(entry -> !entry.getKey().contains(":")) - .map(Entry::getValue) - .toList(); - } - - /** - * Retrieve handlers dedicated to the provided subscription identifier. - * - * @param subscriptionId subscription identifier suffix - * @return list of handlers for the subscription - */ - public List requestHandlers(SubscriptionId subscriptionId) { - return clientMap.entrySet().stream() - .filter(entry -> entry.getKey().endsWith(":" + subscriptionId.value())) - .map(Entry::getValue) - .toList(); - } - - /** - * Create request-scoped handlers for each base relay if they do not already exist. - * - * @param subscriptionId subscription identifier used to scope handlers - */ - public void ensureRequestClients(SubscriptionId subscriptionId) { - for (WebSocketClientHandler baseHandler : baseHandlers()) { - clientMap.computeIfAbsent( - baseHandler.getRelayName() + ":" + subscriptionId.value(), - key -> createHandler(key, baseHandler.getRelayUri())); - } - } - - /** - * Close all handlers currently registered with the registry. - * - * @throws IOException if closing any handler fails - */ - public void closeAll() throws IOException { - for (WebSocketClientHandler client : clientMap.values()) { - client.close(); - } - } - - private WebSocketClientHandler createHandler(String relayName, RelayUri relayUri) { - try { - return factory.create(relayName, relayUri); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("Failed to initialize WebSocket client handler", e); - } - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/client/NostrRequestDispatcher.java b/nostr-java-api/src/main/java/nostr/api/client/NostrRequestDispatcher.java deleted file mode 100644 index 01032ae50..000000000 --- a/nostr-java-api/src/main/java/nostr/api/client/NostrRequestDispatcher.java +++ /dev/null @@ -1,82 +0,0 @@ -package nostr.api.client; - -import lombok.NonNull; -import nostr.base.SubscriptionId; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.event.filter.Filters; -import nostr.event.message.ReqMessage; - -import java.io.IOException; -import java.util.List; - -/** - * Coordinates REQ message dispatch across registered relay clients. - * - *

REQ is the standard subscribe request defined by - * NIP-01. - */ -public final class NostrRequestDispatcher { - - private final NostrRelayRegistry relayRegistry; - - /** - * Create a dispatcher that leverages the registry to route REQ commands. - * - * @param relayRegistry registry that owns relay handlers - */ - public NostrRequestDispatcher(NostrRelayRegistry relayRegistry) { - this.relayRegistry = relayRegistry; - } - - /** - * Send a REQ message using the provided filters across all registered relays. - * - * @param filters filters describing the subscription - * @param subscriptionId subscription identifier applied to handlers - * @return list of relay responses - */ - public List sendRequest(@NonNull Filters filters, @NonNull String subscriptionId) { - return sendRequest(filters, SubscriptionId.of(subscriptionId)); - } - - public List sendRequest(@NonNull Filters filters, @NonNull SubscriptionId subscriptionId) { - relayRegistry.ensureRequestClients(subscriptionId); - return relayRegistry.requestHandlers(subscriptionId).stream() - .map(handler -> handler.sendRequest(filters, subscriptionId)) - .flatMap(List::stream) - .toList(); - } - - /** - * Send REQ messages for multiple filter sets under the same subscription identifier. - * - * @param filtersList list of filter definitions to send - * @param subscriptionId subscription identifier applied to handlers - * @return distinct collection of relay responses - */ - public List sendRequest(@NonNull List filtersList, @NonNull String subscriptionId) { - SubscriptionId id = SubscriptionId.of(subscriptionId); - return filtersList.stream() - .map(filters -> sendRequest(filters, id)) - .flatMap(List::stream) - .distinct() - .toList(); - } - - /** - * Convenience helper for issuing a REQ message via a specific client instance. - * - * @param client relay client used to send the REQ - * @param filters filters describing the subscription - * @param subscriptionId subscription identifier applied to the message - * @return list of responses returned by the relay - * @throws IOException if sending fails - */ - public static List sendRequest( - @NonNull SpringWebSocketClient client, - @NonNull Filters filters, - @NonNull String subscriptionId) - throws IOException { - return client.send(new ReqMessage(subscriptionId, filters)); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/client/NostrSubscriptionManager.java b/nostr-java-api/src/main/java/nostr/api/client/NostrSubscriptionManager.java deleted file mode 100644 index 941039790..000000000 --- a/nostr-java-api/src/main/java/nostr/api/client/NostrSubscriptionManager.java +++ /dev/null @@ -1,92 +0,0 @@ -package nostr.api.client; - -import lombok.NonNull; -import nostr.base.SubscriptionId; -import nostr.event.filter.Filters; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -/** - * Manages subscription lifecycles across multiple relay handlers. - */ -public final class NostrSubscriptionManager { - - private final NostrRelayRegistry relayRegistry; - - /** - * Create a manager backed by the provided relay registry. - * - * @param relayRegistry registry used to look up relay handlers - */ - public NostrSubscriptionManager(NostrRelayRegistry relayRegistry) { - this.relayRegistry = relayRegistry; - } - - /** - * Subscribe to the provided filters across all base relay handlers. - * - * @param filters subscription filters to apply - * @param subscriptionId identifier shared across relay subscriptions - * @param listener callback invoked for each event payload - * @param errorConsumer callback invoked when an error occurs - * @return a handle that closes all subscriptions when invoked - */ - public AutoCloseable subscribe( - @NonNull Filters filters, - @NonNull String subscriptionId, - @NonNull Consumer listener, - @NonNull Consumer errorConsumer) { - SubscriptionId id = SubscriptionId.of(subscriptionId); - List handles = new ArrayList<>(); - try { - for (var handler : relayRegistry.baseHandlers()) { - AutoCloseable handle = handler.subscribe(filters, id.value(), listener, errorConsumer); - handles.add(handle); - } - } catch (RuntimeException e) { - closeQuietly(handles, errorConsumer); - throw e; - } - - return () -> closeHandles(handles, errorConsumer); - } - - private void closeHandles(List handles, Consumer errorConsumer) - throws IOException { - IOException ioFailure = null; - Exception nonIoFailure = null; - for (AutoCloseable handle : handles) { - try { - handle.close(); - } catch (IOException e) { - errorConsumer.accept(e); - if (ioFailure == null) { - ioFailure = e; - } - } catch (Exception e) { - errorConsumer.accept(e); - nonIoFailure = e; - } - } - - if (ioFailure != null) { - throw ioFailure; - } - if (nonIoFailure != null) { - throw new IOException("Failed to close subscription", nonIoFailure); - } - } - - private void closeQuietly(List handles, Consumer errorConsumer) { - for (AutoCloseable handle : handles) { - try { - handle.close(); - } catch (Exception closeEx) { - errorConsumer.accept(closeEx); - } - } - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/client/WebSocketClientHandlerFactory.java b/nostr-java-api/src/main/java/nostr/api/client/WebSocketClientHandlerFactory.java deleted file mode 100644 index b7dd6d194..000000000 --- a/nostr-java-api/src/main/java/nostr/api/client/WebSocketClientHandlerFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -package nostr.api.client; - -import nostr.api.WebSocketClientHandler; -import nostr.base.RelayUri; - -import java.util.concurrent.ExecutionException; - -/** - * Factory for creating {@link WebSocketClientHandler} instances. - */ -@FunctionalInterface -public interface WebSocketClientHandlerFactory { - /** - * Create a handler for the given relay definition. - * - * @param relayName logical relay identifier - * @param relayUri websocket URI of the relay - * @return initialized handler ready for use - * @throws ExecutionException if the underlying client initialization fails - * @throws InterruptedException if thread interruption occurs during initialization - */ - WebSocketClientHandler create(String relayName, RelayUri relayUri) - throws ExecutionException, InterruptedException; -} diff --git a/nostr-java-api/src/main/java/nostr/api/factory/BaseMessageFactory.java b/nostr-java-api/src/main/java/nostr/api/factory/BaseMessageFactory.java deleted file mode 100644 index 0cfffc573..000000000 --- a/nostr-java-api/src/main/java/nostr/api/factory/BaseMessageFactory.java +++ /dev/null @@ -1,16 +0,0 @@ -package nostr.api.factory; - -import lombok.NoArgsConstructor; -import nostr.event.BaseMessage; - -/** - * Base message factory for building protocol messages from inputs. - * - * @param message type - */ -@NoArgsConstructor -public abstract class BaseMessageFactory { - - /** Build the message instance. */ - public abstract T create(); -} diff --git a/nostr-java-api/src/main/java/nostr/api/factory/EventFactory.java b/nostr-java-api/src/main/java/nostr/api/factory/EventFactory.java deleted file mode 100644 index 6f2706a77..000000000 --- a/nostr-java-api/src/main/java/nostr/api/factory/EventFactory.java +++ /dev/null @@ -1,67 +0,0 @@ -package nostr.api.factory; - -import lombok.Data; -import nostr.base.PublicKey; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -import java.util.ArrayList; -import java.util.List; - -/** - * Base event factory collecting sender, tags, and content to build events. - */ -@Data -public abstract class EventFactory { - - private final Identity identity; - private final String content; - private final List tags; - - /** - * Initialize the factory with a sender identity. - */ - public EventFactory(Identity identity) { - this(identity, new ArrayList<>(), ""); - } - - /** Default constructor with no sender, no tags, and empty content. */ - protected EventFactory() { - this.identity = null; - this.content = ""; - this.tags = new ArrayList<>(); - } - - /** - * Initialize the factory with a sender and content. - */ - public EventFactory(Identity sender, String content) { - this(sender, new ArrayList<>(), content); - } - - /** - * Initialize the factory with a sender, tags and content. - */ - public EventFactory(Identity sender, List tags, String content) { - this.content = content; - this.tags = tags; - this.identity = sender; - } - - /** Build the event instance. */ - public abstract E create(); - - /** Add a tag to the internal list. */ - protected void addTag(T tag) { - this.tags.add(tag); - } - - /** Return the sender public key if a sender is configured. */ - protected PublicKey getSender() { - if (this.identity != null) { - return this.identity.getPublicKey(); - } - return null; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/factory/MessageFactory.java b/nostr-java-api/src/main/java/nostr/api/factory/MessageFactory.java deleted file mode 100644 index 6e5e9cf83..000000000 --- a/nostr-java-api/src/main/java/nostr/api/factory/MessageFactory.java +++ /dev/null @@ -1,16 +0,0 @@ -package nostr.api.factory; - -import lombok.NoArgsConstructor; -import nostr.event.BaseMessage; - -/** - * Legacy message factory abstraction; prefer BaseMessageFactory. - * - * @param message type - */ -@NoArgsConstructor -public abstract class MessageFactory { - - /** Build the message instance. */ - public abstract T create(); -} diff --git a/nostr-java-api/src/main/java/nostr/api/factory/impl/BaseTagFactory.java b/nostr-java-api/src/main/java/nostr/api/factory/impl/BaseTagFactory.java deleted file mode 100644 index e0f67b500..000000000 --- a/nostr-java-api/src/main/java/nostr/api/factory/impl/BaseTagFactory.java +++ /dev/null @@ -1,72 +0,0 @@ -package nostr.api.factory.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.event.BaseTag; -import nostr.event.json.codec.EventEncodingException; -import nostr.event.tag.GenericTag; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; - -/** - * Utility to create {@link BaseTag} instances from code and parameters or from JSON. - */ -@Data -@EqualsAndHashCode(callSuper = false) -public class BaseTagFactory { - - private final String code; - private final List params; - - private String jsonString; - - protected BaseTagFactory() { - this.code = ""; - this.params = new ArrayList<>(); - } - - /** - * Initialize with a tag code and params. - */ - public BaseTagFactory(@NonNull String code, @NonNull List params) { - this.code = code; - this.params = params; - } - - /** Initialize with a tag code and varargs params. */ - public BaseTagFactory(String code, String... params) { - this(code, Stream.of(params).filter(param -> param != null).toList()); - } - - /** Initialize from a JSON string representing a serialized tag. */ - public BaseTagFactory(@NonNull String jsonString) { - this.jsonString = jsonString; - this.code = ""; - this.params = new ArrayList<>(); - } - - /** - * Build the tag instance based on the factory configuration. - * - *

If a JSON payload was supplied, it is decoded into a {@link GenericTag}. Otherwise, a tag - * is built from the configured code and parameters. - * - * @return the constructed tag instance - * @throws EventEncodingException if the JSON payload cannot be parsed - */ - public BaseTag create() { - if (jsonString != null) { - try { - return new ObjectMapper().readValue(jsonString, GenericTag.class); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to decode tag from JSON", ex); - } - } - return BaseTag.create(code, params); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/factory/impl/EventMessageFactory.java b/nostr-java-api/src/main/java/nostr/api/factory/impl/EventMessageFactory.java deleted file mode 100644 index 8c3545620..000000000 --- a/nostr-java-api/src/main/java/nostr/api/factory/impl/EventMessageFactory.java +++ /dev/null @@ -1,40 +0,0 @@ -package nostr.api.factory.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.api.factory.BaseMessageFactory; -import nostr.event.impl.GenericEvent; -import nostr.event.message.EventMessage; - -import java.util.Optional; - -@Data -@EqualsAndHashCode(callSuper = false) -public class EventMessageFactory extends BaseMessageFactory { - - private final GenericEvent event; - private String subscriptionId; - - /** - * Initialize a factory for an EVENT message without a subscription id. - */ - public EventMessageFactory(@NonNull GenericEvent event) { - this.event = event; - } - - /** - * Initialize a factory for an EVENT message bound to a subscription id. - */ - public EventMessageFactory(@NonNull GenericEvent event, @NonNull String subscriptionId) { - this(event); - this.subscriptionId = subscriptionId; - } - - @Override - public EventMessage create() { - return Optional.ofNullable(subscriptionId) - .map(subscriptionId -> new EventMessage(event, subscriptionId)) - .orElse(new EventMessage(event)); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/factory/impl/GenericEventFactory.java b/nostr-java-api/src/main/java/nostr/api/factory/impl/GenericEventFactory.java deleted file mode 100644 index 426ce7eb9..000000000 --- a/nostr-java-api/src/main/java/nostr/api/factory/impl/GenericEventFactory.java +++ /dev/null @@ -1,78 +0,0 @@ -package nostr.api.factory.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.api.factory.EventFactory; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; - -import java.util.ArrayList; -import java.util.List; - -/** - * Factory for creating generic Nostr events with a specified kind. - * - *

Supports multiple construction paths (sender/content/tags) while ensuring a concrete - * {@code kind} is always provided. - */ -@EqualsAndHashCode(callSuper = true) -@Data -public class GenericEventFactory extends EventFactory { - - private final Integer kind; - - /** - * Create a factory for a given kind with no content and no sender. - * - * @param kind the event kind - */ - public GenericEventFactory(@NonNull Integer kind) { - super(); - this.kind = kind; - } - - /** - * Create a factory for a given kind and sender with no content. - */ - public GenericEventFactory(Identity sender, @NonNull Integer kind) { - super(sender); - this.kind = kind; - } - - /** - * Create a factory for a given kind and content with no sender. - */ - public GenericEventFactory(@NonNull Integer kind, @NonNull String content) { - super(null, content); - this.kind = kind; - } - - /** - * Create a factory for a given kind, sender and content. - */ - public GenericEventFactory(Identity sender, @NonNull Integer kind, @NonNull String content) { - super(sender, content); - this.kind = kind; - } - - /** - * Create a factory for a given kind with sender, tags and content. - */ - public GenericEventFactory( - Identity sender, @NonNull Integer kind, List tags, @NonNull String content) { - super(sender, tags, content); - this.kind = kind; - } - - /** - * Build a {@link GenericEvent} with the configured values. - * - * @return the newly created GenericEvent - */ - public GenericEvent create() { - return new GenericEvent( - getIdentity().getPublicKey(), getKind(), new ArrayList(getTags()), getContent()); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/nip01/NIP01EventBuilder.java b/nostr-java-api/src/main/java/nostr/api/nip01/NIP01EventBuilder.java deleted file mode 100644 index 0489da14d..000000000 --- a/nostr-java-api/src/main/java/nostr/api/nip01/NIP01EventBuilder.java +++ /dev/null @@ -1,85 +0,0 @@ -package nostr.api.nip01; - -import lombok.NonNull; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.base.Kind; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; - -import java.util.List; - -/** - * Builds common NIP-01 events while keeping {@link nostr.api.NIP01} focused on orchestration. - */ -public final class NIP01EventBuilder { - - private Identity defaultSender; - - public NIP01EventBuilder(Identity defaultSender) { - this.defaultSender = defaultSender; - } - - public void updateDefaultSender(Identity defaultSender) { - this.defaultSender = defaultSender; - } - - public GenericEvent buildTextNote(String content) { - return new GenericEventFactory(resolveSender(null), Kind.TEXT_NOTE.getValue(), content) - .create(); - } - - public GenericEvent buildRecipientTextNote(String content, List tags) { - return new GenericEventFactory<>(resolveSender(null), Kind.TEXT_NOTE.getValue(), tags, content) - .create(); - } - - public GenericEvent buildTaggedTextNote(@NonNull List tags, @NonNull String content) { - return new GenericEventFactory<>(resolveSender(null), Kind.TEXT_NOTE.getValue(), tags, content) - .create(); - } - - public GenericEvent buildMetadataEvent(@NonNull Identity sender, @NonNull String payload) { - return new GenericEventFactory(resolveSender(sender), Kind.SET_METADATA.getValue(), payload) - .create(); - } - - public GenericEvent buildMetadataEvent(@NonNull String payload) { - Identity sender = resolveSender(null); - if (sender != null) { - return buildMetadataEvent(sender, payload); - } - return new GenericEventFactory(Kind.SET_METADATA.getValue(), payload).create(); - } - - public GenericEvent buildReplaceableEvent(Integer kind, String content) { - return new GenericEventFactory(resolveSender(null), kind, content).create(); - } - - public GenericEvent buildReplaceableEvent(List tags, Integer kind, String content) { - return new GenericEventFactory<>(resolveSender(null), kind, tags, content).create(); - } - - public GenericEvent buildEphemeralEvent(List tags, Integer kind, String content) { - return new GenericEventFactory<>(resolveSender(null), kind, tags, content).create(); - } - - public GenericEvent buildEphemeralEvent(Integer kind, String content) { - return new GenericEventFactory(resolveSender(null), kind, content).create(); - } - - public GenericEvent buildAddressableEvent(Integer kind, String content) { - return new GenericEventFactory(resolveSender(null), kind, content).create(); - } - - public GenericEvent buildAddressableEvent( - @NonNull List tags, @NonNull Integer kind, String content) { - return new GenericEventFactory<>(resolveSender(null), kind, tags, content).create(); - } - - private Identity resolveSender(Identity override) { - return override != null ? override : defaultSender; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/nip01/NIP01MessageFactory.java b/nostr-java-api/src/main/java/nostr/api/nip01/NIP01MessageFactory.java deleted file mode 100644 index 6e771b45c..000000000 --- a/nostr-java-api/src/main/java/nostr/api/nip01/NIP01MessageFactory.java +++ /dev/null @@ -1,40 +0,0 @@ -package nostr.api.nip01; - -import lombok.NonNull; -import nostr.event.filter.Filters; -import nostr.event.impl.GenericEvent; -import nostr.event.message.CloseMessage; -import nostr.event.message.EoseMessage; -import nostr.event.message.EventMessage; -import nostr.event.message.NoticeMessage; -import nostr.event.message.ReqMessage; - -import java.util.List; - -/** - * Creates protocol messages referenced by {@link nostr.api.NIP01}. - */ -public final class NIP01MessageFactory { - - private NIP01MessageFactory() {} - - public static EventMessage eventMessage(@NonNull GenericEvent event, String subscriptionId) { - return subscriptionId != null ? new EventMessage(event, subscriptionId) : new EventMessage(event); - } - - public static ReqMessage reqMessage(@NonNull String subscriptionId, @NonNull List filters) { - return new ReqMessage(subscriptionId, filters); - } - - public static CloseMessage closeMessage(@NonNull String subscriptionId) { - return new CloseMessage(subscriptionId); - } - - public static EoseMessage eoseMessage(@NonNull String subscriptionId) { - return new EoseMessage(subscriptionId); - } - - public static NoticeMessage noticeMessage(@NonNull String message) { - return new NoticeMessage(message); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/nip01/NIP01TagFactory.java b/nostr-java-api/src/main/java/nostr/api/nip01/NIP01TagFactory.java deleted file mode 100644 index cddab00c7..000000000 --- a/nostr-java-api/src/main/java/nostr/api/nip01/NIP01TagFactory.java +++ /dev/null @@ -1,104 +0,0 @@ -package nostr.api.nip01; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.base.Marker; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.tag.IdentifierTag; - -import java.util.ArrayList; -import java.util.List; - -/** - * Creates the canonical tags used by NIP-01 helpers. - * - *

These tags follow the standard defined in - * NIP-01 and are used - * throughout the API builders for consistency. - */ -public final class NIP01TagFactory { - - private NIP01TagFactory() {} - - public static BaseTag eventTag(@NonNull String relatedEventId) { - return new BaseTagFactory(Constants.Tag.EVENT_CODE, List.of(relatedEventId)).create(); - } - - public static BaseTag eventTag(@NonNull String idEvent, String recommendedRelayUrl, Marker marker) { - List params = new ArrayList<>(); - params.add(idEvent); - if (recommendedRelayUrl != null) { - params.add(recommendedRelayUrl); - } - if (marker != null) { - params.add(marker.getValue()); - } - return new BaseTagFactory(Constants.Tag.EVENT_CODE, params).create(); - } - - public static BaseTag eventTag(@NonNull String idEvent, Marker marker) { - return eventTag(idEvent, (String) null, marker); - } - - public static BaseTag eventTag(@NonNull String idEvent, Relay recommendedRelay, Marker marker) { - return eventTag(idEvent, recommendedRelay != null ? recommendedRelay.getUri() : null, marker); - } - - public static BaseTag pubKeyTag(@NonNull PublicKey publicKey) { - return new BaseTagFactory(Constants.Tag.PUBKEY_CODE, List.of(publicKey.toString())).create(); - } - - public static BaseTag pubKeyTag(@NonNull PublicKey publicKey, String mainRelayUrl, String petName) { - List params = new ArrayList<>(); - params.add(publicKey.toString()); - params.add(mainRelayUrl); - params.add(petName); - return new BaseTagFactory(Constants.Tag.PUBKEY_CODE, params).create(); - } - - public static BaseTag pubKeyTag(@NonNull PublicKey publicKey, String mainRelayUrl) { - List params = new ArrayList<>(); - params.add(publicKey.toString()); - params.add(mainRelayUrl); - return new BaseTagFactory(Constants.Tag.PUBKEY_CODE, params).create(); - } - - public static BaseTag identifierTag(@NonNull String id) { - return new BaseTagFactory(Constants.Tag.IDENTITY_CODE, List.of(id)).create(); - } - - public static BaseTag addressTag( - @NonNull Integer kind, @NonNull PublicKey publicKey, BaseTag idTag, Relay relay) { - if (idTag != null && !(idTag instanceof IdentifierTag)) { - throw new IllegalArgumentException("idTag must be an identifier tag"); - } - - List params = new ArrayList<>(); - String param = kind + ":" + publicKey + ":"; - if (idTag instanceof IdentifierTag identifierTag) { - String uuid = identifierTag.getUuid(); - if (uuid != null) { - param += uuid; - } - } - params.add(param); - - if (relay != null) { - params.add(relay.getUri()); - } - - return new BaseTagFactory(Constants.Tag.ADDRESS_CODE, params).create(); - } - - public static BaseTag addressTag( - @NonNull Integer kind, @NonNull PublicKey publicKey, String id, Relay relay) { - return addressTag(kind, publicKey, identifierTag(id), relay); - } - - public static BaseTag addressTag(@NonNull Integer kind, @NonNull PublicKey publicKey, String id) { - return addressTag(kind, publicKey, identifierTag(id), null); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/nip57/Bolt11Util.java b/nostr-java-api/src/main/java/nostr/api/nip57/Bolt11Util.java deleted file mode 100644 index 99177262e..000000000 --- a/nostr-java-api/src/main/java/nostr/api/nip57/Bolt11Util.java +++ /dev/null @@ -1,67 +0,0 @@ -package nostr.api.nip57; - -import java.util.Locale; - -/** Utility to parse msats from a BOLT11 invoice HRP. */ -public final class Bolt11Util { - - private Bolt11Util() {} - - /** - * Parse millisatoshi amount from a BOLT11 invoice. - * - * Supports amounts encoded in the HRP using multipliers 'm', 'u', 'n', 'p'. If the invoice has - * no amount, returns -1 to indicate unknown/any amount. - * - * @param bolt11 bech32 invoice string - * @return amount in millisatoshis, or -1 if no amount present - * @throws IllegalArgumentException if the HRP is invalid or the amount cannot be parsed - */ - public static long parseMsat(String bolt11) { - if (bolt11 == null || bolt11.isBlank()) { - throw new IllegalArgumentException("bolt11 invoice is required"); - } - String lower = bolt11.toLowerCase(Locale.ROOT); - int sep = lower.lastIndexOf('1'); - if (!lower.startsWith("ln") || sep < 0) { - throw new IllegalArgumentException("Invalid BOLT11 invoice: missing HRP separator"); - } - String hrp = lower.substring(2, sep); // drop leading "ln" - // Expect network code (bc, tb, bcrt, etc.), then amount digits with optional unit - int idx = 0; - while (idx < hrp.length() && Character.isAlphabetic(hrp.charAt(idx))) idx++; - String amountPart = idx < hrp.length() ? hrp.substring(idx) : ""; - if (amountPart.isEmpty()) { - return -1; // any amount invoice - } - // Split numeric and optional unit suffix - int i = 0; - while (i < amountPart.length() && Character.isDigit(amountPart.charAt(i))) i++; - if (i == 0) { - throw new IllegalArgumentException("Invalid BOLT11 amount"); - } - long value = Long.parseLong(amountPart.substring(0, i)); - int exponent = 11; // convert BTC to msat => * 10^11 - if (i < amountPart.length()) { - char unit = amountPart.charAt(i); - exponent += switch (unit) { - case 'm' -> -3; // milliBTC - case 'u' -> -6; // microBTC - case 'n' -> -9; // nanoBTC - case 'p' -> -12; // picoBTC - default -> throw new IllegalArgumentException("Unsupported BOLT11 unit: " + unit); - }; - } - // value * 10^exponent can overflow; restrict to safe subset used in tests - java.math.BigInteger msat = java.math.BigInteger.valueOf(value); - if (exponent >= 0) { - msat = msat.multiply(java.math.BigInteger.TEN.pow(exponent)); - } else { - msat = msat.divide(java.math.BigInteger.TEN.pow(-exponent)); - } - if (msat.compareTo(java.math.BigInteger.valueOf(Long.MAX_VALUE)) > 0) { - throw new IllegalArgumentException("BOLT11 amount exceeds supported range"); - } - return msat.longValue(); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/nip57/NIP57TagFactory.java b/nostr-java-api/src/main/java/nostr/api/nip57/NIP57TagFactory.java deleted file mode 100644 index 31b829f36..000000000 --- a/nostr-java-api/src/main/java/nostr/api/nip57/NIP57TagFactory.java +++ /dev/null @@ -1,62 +0,0 @@ -package nostr.api.nip57; - -import lombok.NonNull; -import nostr.api.factory.impl.BaseTagFactory; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.config.Constants; -import nostr.event.BaseTag; - -import java.util.ArrayList; -import java.util.List; - -/** - * Centralizes construction of NIP-57 related tags. - */ -public final class NIP57TagFactory { - - private NIP57TagFactory() {} - - public static BaseTag lnurl(@NonNull String lnurl) { - return new BaseTagFactory(Constants.Tag.LNURL_CODE, lnurl).create(); - } - - public static BaseTag bolt11(@NonNull String bolt11) { - return new BaseTagFactory(Constants.Tag.BOLT11_CODE, bolt11).create(); - } - - public static BaseTag preimage(@NonNull String preimage) { - return new BaseTagFactory(Constants.Tag.PREIMAGE_CODE, preimage).create(); - } - - public static BaseTag description(@NonNull String description) { - return new BaseTagFactory(Constants.Tag.DESCRIPTION_CODE, description).create(); - } - - public static BaseTag descriptionHash(@NonNull String descriptionHashHex) { - return new BaseTagFactory(Constants.Tag.DESCRIPTION_HASH_CODE, descriptionHashHex).create(); - } - - public static BaseTag amount(@NonNull Number amount) { - return new BaseTagFactory(Constants.Tag.AMOUNT_CODE, amount.toString()).create(); - } - - public static BaseTag zapSender(@NonNull PublicKey publicKey) { - return new BaseTagFactory(Constants.Tag.RECIPIENT_PUBKEY_CODE, publicKey.toString()).create(); - } - - public static BaseTag zap( - @NonNull PublicKey receiver, @NonNull List relays, Integer weight) { - List params = new ArrayList<>(); - params.add(receiver.toString()); - relays.stream().map(Relay::getUri).forEach(params::add); - if (weight != null) { - params.add(weight.toString()); - } - return BaseTag.create(Constants.Tag.ZAP_CODE, params); - } - - public static BaseTag zap(@NonNull PublicKey receiver, @NonNull List relays) { - return zap(receiver, relays, null); - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/nip57/NIP57ZapReceiptBuilder.java b/nostr-java-api/src/main/java/nostr/api/nip57/NIP57ZapReceiptBuilder.java deleted file mode 100644 index 664c251a9..000000000 --- a/nostr-java-api/src/main/java/nostr/api/nip57/NIP57ZapReceiptBuilder.java +++ /dev/null @@ -1,99 +0,0 @@ -package nostr.api.nip57; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.NonNull; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.api.nip01.NIP01TagFactory; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.json.EventJsonMapper; -import nostr.event.filter.Filterable; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.EventEncodingException; -import nostr.event.tag.AddressTag; -import nostr.id.Identity; -import org.apache.commons.text.StringEscapeUtils; - -/** - * Builds zap receipt events for {@link nostr.api.NIP57}. - */ -public final class NIP57ZapReceiptBuilder { - - private Identity defaultSender; - - public NIP57ZapReceiptBuilder(Identity defaultSender) { - this.defaultSender = defaultSender; - } - - public void updateDefaultSender(Identity defaultSender) { - this.defaultSender = defaultSender; - } - - public GenericEvent build( - @NonNull GenericEvent zapRequestEvent, - @NonNull String bolt11, - @NonNull String preimage, - @NonNull PublicKey zapRecipient) { - GenericEvent receipt = - new GenericEventFactory(resolveSender(null), Kind.ZAP_RECEIPT.getValue(), "").create(); - - receipt.addTag(NIP01TagFactory.pubKeyTag(zapRecipient)); - try { - String description = EventJsonMapper.mapper().writeValueAsString(zapRequestEvent); - // Store description (escaped) and include description_hash for validation - receipt.addTag(NIP57TagFactory.description(StringEscapeUtils.escapeJson(description))); - var hash = nostr.util.NostrUtil.bytesToHex(nostr.util.NostrUtil.sha256(description.getBytes())); - receipt.addTag(NIP57TagFactory.descriptionHash(hash)); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to encode zap receipt description", ex); - } catch (java.security.NoSuchAlgorithmException ex) { - throw new IllegalStateException("SHA-256 algorithm not available", ex); - } - receipt.addTag(NIP57TagFactory.bolt11(bolt11)); - receipt.addTag(NIP57TagFactory.preimage(preimage)); - receipt.addTag(NIP57TagFactory.zapSender(zapRequestEvent.getPubKey())); - receipt.addTag(NIP01TagFactory.eventTag(zapRequestEvent.getId())); - - Filterable.getTypeSpecificTags(AddressTag.class, zapRequestEvent) - .stream() - .findFirst() - .ifPresent(receipt::addTag); - - // Validate invoice amount when available (best-effort) - try { - long invoiceMsat = Bolt11Util.parseMsat(bolt11); - if (invoiceMsat >= 0) { - var amountTag = - nostr.event.filter.Filterable.requireTagOfTypeWithCode( - nostr.event.tag.GenericTag.class, nostr.config.Constants.Tag.AMOUNT_CODE, zapRequestEvent); - String amountStr = amountTag.getAttributes().get(0).value().toString(); - long requestedMsat = Long.parseLong(amountStr); - if (requestedMsat != invoiceMsat) { - throw new IllegalArgumentException( - "Invoice amount does not match zap request amount: requested=" - + requestedMsat - + " msat, invoice=" - + invoiceMsat - + " msat"); - } - } - } catch (RuntimeException ex) { - // Preserve existing behavior for now: do not fail if amount tag is missing - // or invoice lacks amount; only propagate strict mismatches and parsing errors. - if (ex instanceof IllegalArgumentException) { - throw ex; - } - } - - receipt.setCreatedAt(zapRequestEvent.getCreatedAt()); - return receipt; - } - - private Identity resolveSender(Identity override) { - Identity resolved = override != null ? override : defaultSender; - if (resolved == null) { - throw new IllegalStateException("Sender identity is required to build zap receipts"); - } - return resolved; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/nip57/NIP57ZapRequestBuilder.java b/nostr-java-api/src/main/java/nostr/api/nip57/NIP57ZapRequestBuilder.java deleted file mode 100644 index f3e31adfb..000000000 --- a/nostr-java-api/src/main/java/nostr/api/nip57/NIP57ZapRequestBuilder.java +++ /dev/null @@ -1,161 +0,0 @@ -package nostr.api.nip57; - -import lombok.NonNull; -import nostr.api.factory.impl.GenericEventFactory; -import nostr.api.nip01.NIP01TagFactory; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.entities.ZapRequest; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.RelaysTag; -import nostr.id.Identity; - -import java.util.List; - -/** - * Builds zap request events for {@link nostr.api.NIP57}. - */ -public final class NIP57ZapRequestBuilder { - - private Identity defaultSender; - - public NIP57ZapRequestBuilder(Identity defaultSender) { - this.defaultSender = defaultSender; - } - - public void updateDefaultSender(Identity defaultSender) { - this.defaultSender = defaultSender; - } - - public GenericEvent buildFromZapRequest( - @NonNull Identity sender, - @NonNull ZapRequest zapRequest, - @NonNull String content, - PublicKey recipientPubKey, - GenericEvent zappedEvent, - BaseTag addressTag) { - GenericEvent genericEvent = initialiseZapRequest(sender, content); - populateCommonZapRequestTags( - genericEvent, - zapRequest.getRelaysTag(), - zapRequest.getAmount(), - zapRequest.getLnUrl(), - recipientPubKey, - zappedEvent, - addressTag); - return genericEvent; - } - - public GenericEvent buildFromZapRequest( - @NonNull ZapRequest zapRequest, - @NonNull String content, - PublicKey recipientPubKey, - GenericEvent zappedEvent, - BaseTag addressTag) { - return buildFromZapRequest(resolveSender(null), zapRequest, content, recipientPubKey, zappedEvent, addressTag); - } - - public GenericEvent build(@NonNull ZapRequestParameters parameters) { - GenericEvent genericEvent = - initialiseZapRequest(parameters.getSender(), parameters.contentOrDefault()); - populateCommonZapRequestTags( - genericEvent, - parameters.determineRelaysTag(), - parameters.getAmount(), - parameters.getLnUrl(), - parameters.getRecipientPubKey(), - parameters.getZappedEvent(), - parameters.getAddressTag()); - return genericEvent; - } - - public GenericEvent buildFromParameters( - Long amount, - String lnUrl, - BaseTag relaysTag, - String content, - PublicKey recipientPubKey, - GenericEvent zappedEvent, - BaseTag addressTag) { - if (!(relaysTag instanceof RelaysTag)) { - throw new IllegalArgumentException("tag must be of type RelaysTag"); - } - GenericEvent genericEvent = initialiseZapRequest(resolveSender(null), content); - populateCommonZapRequestTags( - genericEvent, (RelaysTag) relaysTag, amount, lnUrl, recipientPubKey, zappedEvent, addressTag); - return genericEvent; - } - - public GenericEvent buildFromParameters( - Long amount, - String lnUrl, - List relays, - String content, - PublicKey recipientPubKey, - GenericEvent zappedEvent, - BaseTag addressTag) { - return buildFromParameters( - amount, lnUrl, new RelaysTag(relays), content, recipientPubKey, zappedEvent, addressTag); - } - - public GenericEvent buildSimpleZapRequest( - Long amount, - String lnUrl, - List relays, - String content, - PublicKey recipientPubKey) { - return buildFromParameters( - amount, - lnUrl, - new RelaysTag(relays.stream().map(Relay::new).toList()), - content, - recipientPubKey, - null, - null); - } - - private GenericEvent initialiseZapRequest(Identity sender, String content) { - return new GenericEventFactory( - resolveSender(sender), - Kind.ZAP_REQUEST.getValue(), - content == null ? "" : content) - .create(); - } - - private void populateCommonZapRequestTags( - GenericEvent event, - RelaysTag relaysTag, - Number amount, - String lnUrl, - PublicKey recipientPubKey, - GenericEvent zappedEvent, - BaseTag addressTag) { - event.addTag(relaysTag); - event.addTag(NIP57TagFactory.amount(amount)); - event.addTag(NIP57TagFactory.lnurl(lnUrl)); - - if (recipientPubKey != null) { - event.addTag(NIP01TagFactory.pubKeyTag(recipientPubKey)); - } - if (zappedEvent != null) { - event.addTag(NIP01TagFactory.eventTag(zappedEvent.getId())); - } - if (addressTag != null) { - if (!Constants.Tag.ADDRESS_CODE.equals(addressTag.getCode())) { - throw new IllegalArgumentException("tag must be of type AddressTag"); - } - event.addTag(addressTag); - } - } - - private Identity resolveSender(Identity override) { - Identity resolved = override != null ? override : defaultSender; - if (resolved == null) { - throw new IllegalStateException("Sender identity is required to build zap requests"); - } - return resolved; - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/nip57/ZapRequestParameters.java b/nostr-java-api/src/main/java/nostr/api/nip57/ZapRequestParameters.java deleted file mode 100644 index ebe8e4df0..000000000 --- a/nostr-java-api/src/main/java/nostr/api/nip57/ZapRequestParameters.java +++ /dev/null @@ -1,47 +0,0 @@ -package nostr.api.nip57; - -import lombok.Builder; -import lombok.Getter; -import lombok.NonNull; -import lombok.Singular; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.RelaysTag; -import nostr.id.Identity; - -import java.util.List; - -/** - * Parameter object for building zap request events. Reduces long argument lists in {@link nostr.api.NIP57}. - */ -@Getter -@Builder -public final class ZapRequestParameters { - - private final Identity sender; - @NonNull private final Long amount; - @NonNull private final String lnUrl; - private final String content; - private final BaseTag addressTag; - private final GenericEvent zappedEvent; - private final PublicKey recipientPubKey; - private final RelaysTag relaysTag; - @Singular("relay") private final List relays; - - public String contentOrDefault() { - return content != null ? content : ""; - } - - public RelaysTag determineRelaysTag() { - if (relaysTag != null) { - return relaysTag; - } - if (relays != null && !relays.isEmpty()) { - return new RelaysTag(relays); - } - throw new IllegalStateException("A relays tag or relay list is required to build zap requests"); - } - -} diff --git a/nostr-java-api/src/main/java/nostr/api/service/NoteService.java b/nostr-java-api/src/main/java/nostr/api/service/NoteService.java deleted file mode 100644 index 67f3819a2..000000000 --- a/nostr-java-api/src/main/java/nostr/api/service/NoteService.java +++ /dev/null @@ -1,12 +0,0 @@ -package nostr.api.service; - -import lombok.NonNull; -import nostr.api.WebSocketClientHandler; -import nostr.base.IEvent; - -import java.util.List; -import java.util.Map; - -public interface NoteService { - List send(@NonNull IEvent event, @NonNull Map clients); -} diff --git a/nostr-java-api/src/main/java/nostr/api/service/impl/DefaultNoteService.java b/nostr-java-api/src/main/java/nostr/api/service/impl/DefaultNoteService.java deleted file mode 100644 index 66d389b15..000000000 --- a/nostr-java-api/src/main/java/nostr/api/service/impl/DefaultNoteService.java +++ /dev/null @@ -1,153 +0,0 @@ -package nostr.api.service.impl; - -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import nostr.api.WebSocketClientHandler; -import nostr.api.service.NoteService; -import nostr.base.IEvent; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** Default implementation that dispatches notes through all WebSocket clients. */ -@Slf4j -public class DefaultNoteService implements NoteService { - - private final ThreadLocal> lastFailures = - ThreadLocal.withInitial(HashMap::new); - private final ThreadLocal> lastFailureDetails = - ThreadLocal.withInitial(HashMap::new); - private java.util.function.Consumer> failureListener; - - /** - * Returns a snapshot of relay send failures recorded during the last {@code send} call on the - * current thread. - * - *

The map key is the relay name as registered in the client; the value is the exception thrown - * while attempting to send to that relay. A best effort is made to continue sending to other - * relays even if one relay fails. - * - * @return a copy of the last failure map; empty if the last send had no failures - */ - public Map getLastFailures() { - return new HashMap<>(lastFailures.get()); - } - - /** - * Returns structured failure details for the last {@code send} call on this thread. - * - *

Each entry includes timing, relay name and URI, the thrown exception class/message and the - * root cause class/message (if any). Use this for richer diagnostics and logging. - * - * @return a copy of the last failure details; empty if the last send had no failures - */ - public Map getLastFailureDetails() { - return new HashMap<>(lastFailureDetails.get()); - } - - /** - * Registers a listener that receives the per‑relay failures map after each {@code send} call. - * - *

The callback is invoked with a map of relay name to Throwable for relays that failed during - * the last send attempt. The listener runs on the calling thread and exceptions thrown by the - * listener are ignored to avoid impacting the main flow. - * - * @param listener consumer of the failure map; may be {@code null} to clear - */ - public void setFailureListener(java.util.function.Consumer> listener) { - this.failureListener = listener; - } - - @Override - public List send( - @NonNull IEvent event, @NonNull Map clients) { - ArrayList responses = new ArrayList<>(); - Map failures = new HashMap<>(); - Map details = new HashMap<>(); - RuntimeException lastFailure = null; - - for (Map.Entry entry : clients.entrySet()) { - String relayName = entry.getKey(); - WebSocketClientHandler client = entry.getValue(); - try { - responses.addAll(client.sendEvent(event)); - } catch (RuntimeException e) { - failures.put(relayName, e); - details.put(relayName, FailureInfo.from(relayName, client.getRelayUri().toString(), e)); - lastFailure = e; // capture and continue to attempt other relays - log.warn("Failed to send event on relay {}: {}", relayName, e.getMessage()); - } - } - - lastFailures.set(failures); - lastFailureDetails.set(details); - if (failureListener != null && !failures.isEmpty()) { - try { failureListener.accept(new HashMap<>(failures)); } catch (Exception ignored) {} - } - - if (responses.isEmpty() && lastFailure != null) { - throw lastFailure; - } - return responses.stream().distinct().toList(); - } - - /** - * Provides structured information about a relay send failure. - */ - public static final class FailureInfo { - public final long timestampEpochMillis; - public final String relayName; - public final String relayUri; - public final String exceptionClass; - public final String message; - public final String rootCauseClass; - public final String rootCauseMessage; - - private FailureInfo( - long ts, - String relayName, - String relayUri, - String cls, - String msg, - String rootCls, - String rootMsg) { - this.timestampEpochMillis = ts; - this.relayName = relayName; - this.relayUri = relayUri; - this.exceptionClass = cls; - this.message = msg; - this.rootCauseClass = rootCls; - this.rootCauseMessage = rootMsg; - } - - private static Throwable root(Throwable t) { - Throwable r = t; - while (r.getCause() != null && r.getCause() != r) { - r = r.getCause(); - } - return r; - } - - /** - * Create a {@link FailureInfo} from a relay identity and a thrown exception. - * - * @param relayName human‑readable name configured by the client - * @param relayUri websocket URI string of the relay - * @param t the thrown exception - * @return a populated {@link FailureInfo} - */ - public static FailureInfo from(String relayName, String relayUri, Throwable t) { - Throwable r = root(t); - return new FailureInfo( - java.time.Instant.now().toEpochMilli(), - relayName, - relayUri, - t.getClass().getName(), - String.valueOf(t.getMessage()), - r.getClass().getName(), - String.valueOf(r.getMessage())); - } - } -} diff --git a/nostr-java-api/src/main/java/nostr/config/Constants.java b/nostr-java-api/src/main/java/nostr/config/Constants.java deleted file mode 100644 index 049d7a3fa..000000000 --- a/nostr-java-api/src/main/java/nostr/config/Constants.java +++ /dev/null @@ -1,65 +0,0 @@ -package nostr.config; - -/** - * Collection of common constants used across the API. - * - *

Includes well-known tag codes defined by NIP-01 and used throughout the - * library to build and parse event tags. - * - * @see NIP-01 - */ -public final class Constants { - private Constants() {} - - // Deprecated Constants.Kind facade removed in 1.0.0. Use nostr.base.Kind instead. - - public static final class Tag { - private Tag() {} - - public static final String EVENT_CODE = "e"; - public static final String PUBKEY_CODE = "p"; - public static final String IDENTITY_CODE = "d"; - public static final String ADDRESS_CODE = "a"; - public static final String HASHTAG_CODE = "t"; - public static final String REFERENCE_CODE = "r"; - public static final String GEOHASH_CODE = "g"; - public static final String SUBJECT_CODE = "subject"; - public static final String TITLE_CODE = "title"; - public static final String IMAGE_CODE = "image"; - public static final String PUBLISHED_AT_CODE = "published_at"; - public static final String SUMMARY_CODE = "summary"; - public static final String KIND_CODE = "k"; - public static final String EMOJI_CODE = "emoji"; - public static final String ALT_CODE = "alt"; - public static final String NAMESPACE_CODE = "L"; - public static final String LABEL_CODE = "l"; - public static final String EXPIRATION_CODE = "expiration"; - public static final String RELAY_CODE = "relay"; - public static final String RELAYS_CODE = "relays"; - public static final String CHALLENGE_CODE = "challenge"; - public static final String AMOUNT_CODE = "amount"; - public static final String LNURL_CODE = "lnurl"; - public static final String BOLT11_CODE = "bolt11"; - public static final String PREIMAGE_CODE = "preimage"; - public static final String DESCRIPTION_CODE = "description"; - public static final String DESCRIPTION_HASH_CODE = "description_hash"; - public static final String ZAP_CODE = "zap"; - public static final String RECIPIENT_PUBKEY_CODE = "P"; - public static final String MINT_CODE = "mint"; - public static final String UNIT_CODE = "unit"; - public static final String PRIVKEY_CODE = "privkey"; - public static final String BALANCE_CODE = "balance"; - public static final String DIRECTION_CODE = "direction"; - public static final String P2PKH_CODE = "pubkey"; - public static final String URL_CODE = "u"; - public static final String PROOF_CODE = "proof"; - public static final String LOCATION_CODE = "location"; - public static final String PRICE_CODE = "price"; - public static final String STATUS_CODE = "status"; - public static final String START_CODE = "start"; - public static final String END_CODE = "end"; - public static final String START_TZID_CODE = "start_tzid"; - public static final String END_TZID_CODE = "end_tzid"; - public static final String FREE_BUSY_CODE = "fb"; - } -} diff --git a/nostr-java-api/src/main/java/nostr/config/RelayConfig.java b/nostr-java-api/src/main/java/nostr/config/RelayConfig.java deleted file mode 100644 index 503ecbee8..000000000 --- a/nostr-java-api/src/main/java/nostr/config/RelayConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package nostr.config; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import java.util.Map; - -@Configuration -@PropertySource("classpath:relays.properties") -@EnableConfigurationProperties(RelaysProperties.class) -public class RelayConfig { - - @Bean - public Map relays(RelaysProperties relaysProperties) { - return relaysProperties; - } - - // Legacy property loader removed in 1.0.0. Use RelaysProperties bean instead. -} diff --git a/nostr-java-api/src/main/java/nostr/config/RelaysProperties.java b/nostr-java-api/src/main/java/nostr/config/RelaysProperties.java deleted file mode 100644 index 8b3fed949..000000000 --- a/nostr-java-api/src/main/java/nostr/config/RelaysProperties.java +++ /dev/null @@ -1,11 +0,0 @@ -package nostr.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.io.Serial; -import java.util.HashMap; - -@ConfigurationProperties(prefix = "relays") -public class RelaysProperties extends HashMap { - @Serial private static final long serialVersionUID = 1L; -} diff --git a/nostr-java-api/src/main/resources/app.properties b/nostr-java-api/src/main/resources/app.properties deleted file mode 100644 index fdecc827f..000000000 --- a/nostr-java-api/src/main/resources/app.properties +++ /dev/null @@ -1 +0,0 @@ -relays=relays.properties diff --git a/nostr-java-api/src/main/resources/relays.properties b/nostr-java-api/src/main/resources/relays.properties deleted file mode 100644 index 238ea990a..000000000 --- a/nostr-java-api/src/main/resources/relays.properties +++ /dev/null @@ -1,2 +0,0 @@ -# Relay configuration in `relays.=` format -relays.nostr_rs_relay=ws://127.0.0.1:5555 diff --git a/nostr-java-api/src/test/java/nostr/api/NIP46RequestTest.java b/nostr-java-api/src/test/java/nostr/api/NIP46RequestTest.java deleted file mode 100644 index 23fe2c2d8..000000000 --- a/nostr-java-api/src/test/java/nostr/api/NIP46RequestTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package nostr.api; - -import org.junit.jupiter.api.Test; - -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class NIP46RequestTest { - - // Ensures params can be added and queried reliably. - @Test - void addAndQueryParams() { - NIP46.Request req = new NIP46.Request("id-1", "sign_event", Set.of("a")); - req.addParam("b"); - assertEquals(2, req.getParamCount()); - assertTrue(req.containsParam("a")); - assertTrue(req.containsParam("b")); - assertFalse(req.containsParam("c")); - } -} - diff --git a/nostr-java-api/src/test/java/nostr/api/TestHandlerFactory.java b/nostr-java-api/src/test/java/nostr/api/TestHandlerFactory.java deleted file mode 100644 index dc4e767ab..000000000 --- a/nostr-java-api/src/test/java/nostr/api/TestHandlerFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -package nostr.api; - -import lombok.NonNull; -import nostr.base.RelayUri; -import nostr.base.SubscriptionId; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.SpringWebSocketClient; - -import java.util.HashMap; -import java.util.concurrent.ExecutionException; -import java.util.function.Function; - -/** - * Test-only factory to construct {@link WebSocketClientHandler} while staying inside the - * {@code nostr.api} package to access package-private constructor. - */ -public final class TestHandlerFactory { - private TestHandlerFactory() {} - - public static WebSocketClientHandler create( - @NonNull String relayName, - @NonNull String relayUri, - @NonNull SpringWebSocketClient client, - @NonNull Function requestClientFactory, - @NonNull WebSocketClientFactory clientFactory) throws ExecutionException, InterruptedException { - return new WebSocketClientHandler( - relayName, - new RelayUri(relayUri), - client, - new HashMap<>(), - requestClientFactory, - clientFactory); - } -} - diff --git a/nostr-java-api/src/test/java/nostr/api/TestableWebSocketClientHandler.java b/nostr-java-api/src/test/java/nostr/api/TestableWebSocketClientHandler.java deleted file mode 100644 index 6e7072643..000000000 --- a/nostr-java-api/src/test/java/nostr/api/TestableWebSocketClientHandler.java +++ /dev/null @@ -1,24 +0,0 @@ -package nostr.api; - -import nostr.base.RelayUri; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.SpringWebSocketClientFactory; - -import java.util.Map; -import java.util.function.Function; - -public class TestableWebSocketClientHandler extends WebSocketClientHandler { - public TestableWebSocketClientHandler( - String relayName, - String relayUri, - SpringWebSocketClient eventClient, - Function requestClientFactory) { - super( - relayName, - new RelayUri(relayUri), - eventClient, - Map.of(), - requestClientFactory != null ? id -> requestClientFactory.apply(id.value()) : null, - new SpringWebSocketClientFactory()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/NostrRequestDispatcherEnsureClientsTest.java b/nostr-java-api/src/test/java/nostr/api/client/NostrRequestDispatcherEnsureClientsTest.java deleted file mode 100644 index 0b9eb4f24..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/NostrRequestDispatcherEnsureClientsTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package nostr.api.client; - -import nostr.api.WebSocketClientHandler; -import nostr.base.SubscriptionId; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** Verifies ensureRequestClients() is invoked per dispatcher call as expected. */ -public class NostrRequestDispatcherEnsureClientsTest { - - @Test - void ensureCalledOnceForSingleFilter() { - NostrRelayRegistry registry = mock(NostrRelayRegistry.class); - WebSocketClientHandler handler = mock(WebSocketClientHandler.class); - when(registry.requestHandlers(eq(SubscriptionId.of("sub-1")))).thenReturn(List.of(handler)); - NostrRequestDispatcher dispatcher = new NostrRequestDispatcher(registry); - - dispatcher.sendRequest(new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), "sub-1"); - verify(registry, times(1)).ensureRequestClients(eq(SubscriptionId.of("sub-1"))); - } - - @Test - void ensureCalledPerFilterForListVariant() { - NostrRelayRegistry registry = mock(NostrRelayRegistry.class); - WebSocketClientHandler handler = mock(WebSocketClientHandler.class); - when(registry.requestHandlers(eq(SubscriptionId.of("sub-2")))).thenReturn(List.of(handler)); - NostrRequestDispatcher dispatcher = new NostrRequestDispatcher(registry); - - List list = List.of( - new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), - new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)) - ); - dispatcher.sendRequest(list, "sub-2"); - verify(registry, times(2)).ensureRequestClients(eq(SubscriptionId.of("sub-2"))); - } -} - diff --git a/nostr-java-api/src/test/java/nostr/api/client/NostrRequestDispatcherTest.java b/nostr-java-api/src/test/java/nostr/api/client/NostrRequestDispatcherTest.java deleted file mode 100644 index f51e763c4..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/NostrRequestDispatcherTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package nostr.api.client; - -import nostr.api.WebSocketClientHandler; -import nostr.base.SubscriptionId; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** Tests for NostrRequestDispatcher multi-filter dispatch and aggregation. */ -public class NostrRequestDispatcherTest { - - @Test - void multiFilterDispatchAggregatesResponses() { - NostrRelayRegistry registry = mock(NostrRelayRegistry.class); - WebSocketClientHandler handler = mock(WebSocketClientHandler.class); - - when(registry.requestHandlers(eq(SubscriptionId.of("sub-Z")))).thenReturn(List.of(handler)); - doNothing().when(registry).ensureRequestClients(eq(SubscriptionId.of("sub-Z"))); - - when(handler.sendRequest(any(Filters.class), eq(SubscriptionId.of("sub-Z")))) - .thenReturn(List.of("R1")) - .thenReturn(List.of("R2")); - - NostrRequestDispatcher dispatcher = new NostrRequestDispatcher(registry); - List list = - List.of(new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), - new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE))); - - var out = dispatcher.sendRequest(list, "sub-Z"); - assertEquals(2, out.size()); - // ensure each filter triggered a send on handler - verify(handler, times(2)).sendRequest(any(Filters.class), eq(SubscriptionId.of("sub-Z"))); - } - - @Test - void multiFilterDispatchDeduplicatesResponses() { - NostrRelayRegistry registry = mock(NostrRelayRegistry.class); - WebSocketClientHandler handler = mock(WebSocketClientHandler.class); - when(registry.requestHandlers(eq(SubscriptionId.of("sub-D")))).thenReturn(List.of(handler)); - doNothing().when(registry).ensureRequestClients(eq(SubscriptionId.of("sub-D"))); - - // Return the same response for both filters; expect distinct aggregation - when(handler.sendRequest(any(Filters.class), eq(SubscriptionId.of("sub-D")))) - .thenReturn(List.of("DUP")) - .thenReturn(List.of("DUP")); - - NostrRequestDispatcher dispatcher = new NostrRequestDispatcher(registry); - List list = - List.of(new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), - new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE))); - - var out = dispatcher.sendRequest(list, "sub-D"); - assertEquals(1, out.size()); - verify(handler, times(2)).sendRequest(any(Filters.class), eq(SubscriptionId.of("sub-D"))); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientCloseLoggingTest.java b/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientCloseLoggingTest.java deleted file mode 100644 index 679aab628..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientCloseLoggingTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package nostr.api.client; - -import com.github.valfirst.slf4jtest.TestLogger; -import com.github.valfirst.slf4jtest.TestLoggerFactory; -import nostr.api.NostrSpringWebSocketClient; -import nostr.api.WebSocketClientHandler; -import nostr.base.RelayUri; -import nostr.base.SubscriptionId; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import nostr.id.Identity; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** Verifies default error listener logs WARN lines when close path encounters exceptions. */ -public class NostrSpringWebSocketClientCloseLoggingTest { - - private final TestLogger logger = TestLoggerFactory.getTestLogger(NostrSpringWebSocketClient.class); - - static class TestClient extends NostrSpringWebSocketClient { - private final WebSocketClientHandler handler; - TestClient(Identity sender, WebSocketClientHandler handler) { super(sender); this.handler = handler; } - - @Override - protected WebSocketClientHandler newWebSocketClientHandler(String relayName, RelayUri relayUri) - throws ExecutionException, InterruptedException { - return handler; - } - } - - @AfterEach - void cleanup() { TestLoggerFactory.clear(); } - - @Test - void logsWarnsOnCloseErrors() throws Exception { - // Prepare a handler with mocked Spring client throwing on close - SpringWebSocketClient client = mock(SpringWebSocketClient.class); - AutoCloseable delegate = mock(AutoCloseable.class); - AutoCloseable closeFrame = mock(AutoCloseable.class); - when(client.subscribe(any(nostr.event.message.ReqMessage.class), any(), any(), any())).thenReturn(delegate); - when(client.subscribe(any(nostr.event.message.CloseMessage.class), any(), any(), any())).thenReturn(closeFrame); - doThrow(new IOException("cf")).when(closeFrame).close(); - doThrow(new RuntimeException("del")).when(delegate).close(); - - WebSocketClientFactory factory = mock(WebSocketClientFactory.class); - Function reqFactory = k -> client; - WebSocketClientHandler handler = - new WebSocketClientHandler( - "relay-1", - new RelayUri("wss://relay1"), - client, - new HashMap<>(), - reqFactory, - factory); - - Identity sender = Identity.generateRandomIdentity(); - TestClient testClient = new TestClient(sender, handler); - testClient.setRelays(Map.of("r1", "wss://relay1")); - - AutoCloseable h = testClient.subscribe(new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), "sub-close-log", s -> {}); - try { - try { - h.close(); - } catch (IOException ignored) {} - boolean found = logger.getLoggingEvents().stream() - .anyMatch(e -> e.getLevel().toString().equals("WARN") - && e.getMessage().contains("Subscription error for {} on relays {}") - && e.getArguments().size() == 2 - && String.valueOf(e.getArguments().get(0)).contains("sub-close-log") - && String.valueOf(e.getArguments().get(1)).contains("r1")); - assertTrue(found); - } finally { - try { h.close(); } catch (Exception ignored) {} - } - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientHandlerIntegrationTest.java b/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientHandlerIntegrationTest.java deleted file mode 100644 index fcb44cee8..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientHandlerIntegrationTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package nostr.api.client; - -import nostr.api.NostrSpringWebSocketClient; -import nostr.api.WebSocketClientHandler; -import nostr.base.RelayUri; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** Wires NostrSpringWebSocketClient to a mocked handler and verifies subscribe/close flow. */ -public class NostrSpringWebSocketClientHandlerIntegrationTest { - - static class TestClient extends NostrSpringWebSocketClient { - private final WebSocketClientHandler handler; - TestClient(Identity sender, WebSocketClientHandler handler) { super(sender); this.handler = handler; } - - @Override - protected WebSocketClientHandler newWebSocketClientHandler(String relayName, RelayUri relayUri) - throws ExecutionException, InterruptedException { - return handler; - } - } - - @Test - void clientSubscribeDelegatesToHandlerAndCloseClosesHandle() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - WebSocketClientHandler handler = mock(WebSocketClientHandler.class); - AutoCloseable handle = mock(AutoCloseable.class); - when(handler.subscribe(any(), anyString(), any(Consumer.class), any())).thenReturn(handle); - - TestClient client = new TestClient(sender, handler); - client.setRelays(Map.of("r1", "wss://relay1")); - - AutoCloseable h = client.subscribe(new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), "sub-i", s -> {}); - verify(handler, times(1)).subscribe(any(), anyString(), any(Consumer.class), any()); - - h.close(); - verify(handle, times(1)).close(); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientLoggingTest.java b/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientLoggingTest.java deleted file mode 100644 index 7a6761e44..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientLoggingTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package nostr.api.client; - -import com.github.valfirst.slf4jtest.TestLogger; -import com.github.valfirst.slf4jtest.TestLoggerFactory; -import nostr.api.NostrSpringWebSocketClient; -import nostr.api.integration.support.FakeWebSocketClientFactory; -import nostr.api.service.impl.DefaultNoteService; -import nostr.base.Kind; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import nostr.id.Identity; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** Verifies default error listener path emits a WARN log entry. */ -public class NostrSpringWebSocketClientLoggingTest { - - private final TestLogger logger = TestLoggerFactory.getTestLogger(NostrSpringWebSocketClient.class); - - @AfterEach - void cleanup() { TestLoggerFactory.clear(); } - - @Test - void defaultErrorListenerEmitsWarnLog() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - NostrSpringWebSocketClient client = - new NostrSpringWebSocketClient(sender, new DefaultNoteService(), factory); - - client.setRelays(Map.of("relay", "wss://relay.example.com")); - AutoCloseable handle = client.subscribe(new Filters(new KindFilter<>(Kind.TEXT_NOTE)), "sub-log", s -> {}); - try { - factory.get("wss://relay.example.com").emitError(new RuntimeException("log-me")); - boolean found = logger.getLoggingEvents().stream() - .anyMatch(e -> e.getLevel().toString().equals("WARN") - && e.getMessage().contains("Subscription error for {} on relays {}") - && e.getArguments().size() == 2 - && String.valueOf(e.getArguments().get(0)).contains("sub-log") - && String.valueOf(e.getArguments().get(1)).contains("relay")); - assertTrue(found); - } finally { - handle.close(); - } - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientRelaysTest.java b/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientRelaysTest.java deleted file mode 100644 index 6d695f686..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientRelaysTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package nostr.api.client; - -import nostr.api.NostrSpringWebSocketClient; -import nostr.api.integration.support.FakeWebSocketClientFactory; -import nostr.api.service.impl.DefaultNoteService; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** Verifies getRelays returns the snapshot of relay names to URIs. */ -public class NostrSpringWebSocketClientRelaysTest { - - @Test - void getRelaysReflectsRegistration() { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(sender, new DefaultNoteService(), factory); - client.setRelays(Map.of( - "r1", "wss://relay1", - "r2", "wss://relay2")); - - Map snapshot = client.getRelays(); - assertEquals(2, snapshot.size()); - assertEquals("wss://relay1", snapshot.get("r1")); - assertEquals("wss://relay2", snapshot.get("r2")); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientSubscribeLoggingTest.java b/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientSubscribeLoggingTest.java deleted file mode 100644 index a9c183587..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/NostrSpringWebSocketClientSubscribeLoggingTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package nostr.api.client; - -import com.github.valfirst.slf4jtest.TestLogger; -import com.github.valfirst.slf4jtest.TestLoggerFactory; -import nostr.api.NostrSpringWebSocketClient; -import nostr.api.WebSocketClientHandler; -import nostr.base.RelayUri; -import nostr.base.SubscriptionId; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import nostr.id.Identity; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** Verifies default error listener emits WARN logs when subscribe path throws. */ -public class NostrSpringWebSocketClientSubscribeLoggingTest { - - private final TestLogger logger = TestLoggerFactory.getTestLogger(NostrSpringWebSocketClient.class); - - static class TestClient extends NostrSpringWebSocketClient { - private final WebSocketClientHandler handler; - TestClient(Identity sender, WebSocketClientHandler handler) { super(sender); this.handler = handler; } - @Override - protected WebSocketClientHandler newWebSocketClientHandler(String relayName, RelayUri relayUri) - throws ExecutionException, InterruptedException { - return handler; - } - } - - @AfterEach - void cleanup() { TestLoggerFactory.clear(); } - - @Test - void logsWarnOnSubscribeFailureWithDefaultErrorListener() throws Exception { - SpringWebSocketClient client = mock(SpringWebSocketClient.class); - // Throw on subscribe to simulate transport failure - when(client.subscribe(any(nostr.event.message.ReqMessage.class), any(), any(), any())) - .thenThrow(new IOException("subscribe-io")); - - WebSocketClientFactory factory = mock(WebSocketClientFactory.class); - Function reqFactory = k -> client; - WebSocketClientHandler handler = - new WebSocketClientHandler( - "relay-1", - new RelayUri("wss://relay1"), - client, - new HashMap<>(), - reqFactory, - factory); - - Identity sender = Identity.generateRandomIdentity(); - TestClient testClient = new TestClient(sender, handler); - testClient.setRelays(Map.of("r1", "wss://relay1")); - - try { - testClient.subscribe(new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), "sub-warn", s -> {}); - } catch (RuntimeException ignored) { - // default error listener warns; the exception is rethrown by handler subscribe path - } - boolean found = logger.getLoggingEvents().stream() - .anyMatch(e -> e.getLevel().toString().equals("WARN") - && e.getMessage().contains("Subscription error for {} on relays {}") - && e.getArguments().size() == 2 - && String.valueOf(e.getArguments().get(0)).contains("sub-warn") - && String.valueOf(e.getArguments().get(1)).contains("r1")); - assertTrue(found); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/NostrSubscriptionManagerCloseTest.java b/nostr-java-api/src/test/java/nostr/api/client/NostrSubscriptionManagerCloseTest.java deleted file mode 100644 index df67efb91..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/NostrSubscriptionManagerCloseTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package nostr.api.client; - -import nostr.api.WebSocketClientHandler; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** Tests close semantics and error aggregation in NostrSubscriptionManager. */ -public class NostrSubscriptionManagerCloseTest { - - @Test - // When closing multiple handles, IOException takes precedence; errors are reported to consumer. - void closesAllHandlesAndAggregatesErrors() throws Exception { - NostrRelayRegistry registry = mock(NostrRelayRegistry.class); - WebSocketClientHandler h1 = mock(WebSocketClientHandler.class); - WebSocketClientHandler h2 = mock(WebSocketClientHandler.class); - when(registry.baseHandlers()).thenReturn(List.of(h1, h2)); - - AutoCloseable c1 = mock(AutoCloseable.class); - AutoCloseable c2 = mock(AutoCloseable.class); - when(h1.subscribe(any(), anyString(), any(), any())).thenReturn(c1); - when(h2.subscribe(any(), anyString(), any(), any())).thenReturn(c2); - - NostrSubscriptionManager mgr = new NostrSubscriptionManager(registry); - AtomicInteger errorCount = new AtomicInteger(); - Consumer errorConsumer = t -> errorCount.incrementAndGet(); - AutoCloseable handle = mgr.subscribe(new nostr.event.filter.Filters(new nostr.event.filter.KindFilter<>(nostr.base.Kind.TEXT_NOTE)), "subX", s -> {}, errorConsumer); - - doThrow(new IOException("iofail")).when(c1).close(); - doThrow(new RuntimeException("boom")).when(c2).close(); - - IOException thrown = assertThrows(IOException.class, handle::close); - assertEquals("iofail", thrown.getMessage()); - // Both errors reported - assertEquals(2, errorCount.get()); - } - - @Test - // If subscribe fails mid-iteration, previously acquired handles are closed and error reported. - void subscribeFailureClosesAcquiredHandles() throws Exception { - NostrRelayRegistry registry = mock(NostrRelayRegistry.class); - WebSocketClientHandler h1 = mock(WebSocketClientHandler.class); - WebSocketClientHandler h2 = mock(WebSocketClientHandler.class); - when(registry.baseHandlers()).thenReturn(List.of(h1, h2)); - - AutoCloseable c1 = mock(AutoCloseable.class); - when(h1.subscribe(any(), anyString(), any(), any())).thenReturn(c1); - when(h2.subscribe(any(), anyString(), any(), any())).thenThrow(new RuntimeException("sub-fail")); - - NostrSubscriptionManager mgr = new NostrSubscriptionManager(registry); - AtomicInteger errorCount = new AtomicInteger(); - Consumer errorConsumer = t -> errorCount.incrementAndGet(); - - assertThrows(RuntimeException.class, () -> - mgr.subscribe(new nostr.event.filter.Filters(new nostr.event.filter.KindFilter<>(nostr.base.Kind.TEXT_NOTE)), "subY", s -> {}, errorConsumer)); - - // First handle should be closed due to failure in second subscribe - verify(c1, times(1)).close(); - // Error consumer not invoked because close succeeded (no exception during cleanup) - assertEquals(0, errorCount.get()); - } -} - diff --git a/nostr-java-api/src/test/java/nostr/api/client/README.md b/nostr-java-api/src/test/java/nostr/api/client/README.md deleted file mode 100644 index 28b331fa4..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Client/Handler Test Suite - -This package contains tests for the API client and the internal WebSocket handler. - -## Structure - -- `NostrSpringWebSocketClient*` — Tests for high-level client behavior (logging, relays, integration). -- `WebSocketHandler*` — Tests for internal handler semantics: - - `SendCloseFrame` — Ensures CLOSE frame is sent on handle close. - - `CloseSequencing` — Verifies close ordering and exception handling. - - `CloseIdempotent` — Double close does not throw. - - `SendRequest` — Encodes correct subscription id; multi-sub tests. - - `RequestError` — IOException wrapping as RuntimeException. -- `NostrRequestDispatcher*` — Tests REQ dispatch across handlers including de-duplication and ensureClient calls. -- `NostrSubscriptionManager*` — Tests subscribe lifecycle and close error aggregation. - -## Notes - -- `nostr.api.TestHandlerFactory` is used to instantiate a `WebSocketClientHandler` from outside the `nostr.api` package while preserving access to its package-private constructor. -- Logging assertions use `slf4j-test` to capture and inspect log events. diff --git a/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerCloseIdempotentTest.java b/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerCloseIdempotentTest.java deleted file mode 100644 index a0bf86316..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerCloseIdempotentTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package nostr.api.client; - -import nostr.base.SubscriptionId; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.concurrent.ExecutionException; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** Verifies calling close twice on a subscription handle does not throw. */ -public class WebSocketHandlerCloseIdempotentTest { - - @Test - void doubleCloseDoesNotThrow() throws ExecutionException, InterruptedException, IOException { - SpringWebSocketClient client = mock(SpringWebSocketClient.class); - AutoCloseable delegate = mock(AutoCloseable.class); - AutoCloseable closeFrame = mock(AutoCloseable.class); - when(client.subscribe(any(nostr.event.message.ReqMessage.class), any(), any(), any())) - .thenReturn(delegate); - when(client.subscribe(any(nostr.event.message.CloseMessage.class), any(), any(), any())) - .thenReturn(closeFrame); - - WebSocketClientFactory factory = mock(WebSocketClientFactory.class); - Function reqFactory = k -> client; - nostr.api.WebSocketClientHandler handler = - nostr.api.TestHandlerFactory.create( - "relay-1", "wss://relay1", client, reqFactory, factory); - - AutoCloseable handle = handler.subscribe(new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), "sub-dup", s -> {}, t -> {}); - assertDoesNotThrow(handle::close); - // Second close should also not throw - assertDoesNotThrow(handle::close); - verify(client, atLeastOnce()).close(); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerCloseSequencingTest.java b/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerCloseSequencingTest.java deleted file mode 100644 index 2b0ee65dc..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerCloseSequencingTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package nostr.api.client; - -import nostr.base.SubscriptionId; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import org.junit.jupiter.api.Test; -import org.mockito.InOrder; - -import java.io.IOException; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** Ensures CLOSE frame is sent before delegate and client close, even on exceptions. */ -public class WebSocketHandlerCloseSequencingTest { - - @Test - void closeOrderIsCloseFrameThenDelegateThenClient() throws Exception { - SpringWebSocketClient client = mock(SpringWebSocketClient.class); - AutoCloseable delegate = mock(AutoCloseable.class); - AutoCloseable closeFrame = mock(AutoCloseable.class); - when(client.subscribe(any(nostr.event.message.ReqMessage.class), any(), any(), any())) - .thenReturn(delegate); - when(client.subscribe(any(nostr.event.message.CloseMessage.class), any(), any(), any())) - .thenReturn(closeFrame); - - WebSocketClientFactory factory = mock(WebSocketClientFactory.class); - Function reqFactory = k -> client; - - nostr.api.WebSocketClientHandler handler = - nostr.api.TestHandlerFactory.create( - "relay-1", "wss://relay1", client, reqFactory, factory); - - AutoCloseable handle = handler.subscribe(new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), "sub-789", s -> {}, t -> {}); - handle.close(); - - InOrder inOrder = inOrder(closeFrame, delegate, client); - inOrder.verify(closeFrame, times(1)).close(); - inOrder.verify(delegate, times(1)).close(); - inOrder.verify(client, times(1)).close(); - } - - @Test - void exceptionsStillAttemptAllClosesAndThrowFirstIo() throws Exception { - SpringWebSocketClient client = mock(SpringWebSocketClient.class); - AutoCloseable delegate = mock(AutoCloseable.class); - AutoCloseable closeFrame = mock(AutoCloseable.class); - when(client.subscribe(any(nostr.event.message.ReqMessage.class), any(), any(), any())) - .thenReturn(delegate); - when(client.subscribe(any(nostr.event.message.CloseMessage.class), any(), any(), any())) - .thenReturn(closeFrame); - - doThrow(new IOException("frame-io")).when(closeFrame).close(); - doThrow(new RuntimeException("del-boom")).when(delegate).close(); - doThrow(new IOException("client-io")).when(client).close(); - - WebSocketClientFactory factory = mock(WebSocketClientFactory.class); - Function reqFactory = k -> client; - nostr.api.WebSocketClientHandler handler = - nostr.api.TestHandlerFactory.create( - "relay-1", "wss://relay1", client, reqFactory, factory); - - AutoCloseable handle = handler.subscribe(new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), "sub-err", s -> {}, t -> {}); - IOException thrown = assertEqualsType(IOException.class, () -> handle.close()); - assertEquals("frame-io", thrown.getMessage()); - - // All closes attempted even on exceptions - verify(closeFrame, times(1)).close(); - verify(delegate, times(1)).close(); - verify(client, times(1)).close(); - } - - private static T assertEqualsType(Class type, Executable executable) { - try { - executable.exec(); - throw new AssertionError("Expected exception: " + type.getSimpleName()); - } catch (Throwable t) { - if (type.isInstance(t)) { - return type.cast(t); - } - throw new AssertionError("Unexpected exception type: " + t.getClass(), t); - } - } - - @FunctionalInterface - private interface Executable { void exec() throws Exception; } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerRequestErrorTest.java b/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerRequestErrorTest.java deleted file mode 100644 index 2855f5961..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerRequestErrorTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package nostr.api.client; - -import nostr.base.SubscriptionId; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.concurrent.ExecutionException; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** Ensures sendRequest wraps IOExceptions as RuntimeException with context. */ -public class WebSocketHandlerRequestErrorTest { - - @Test - void sendRequestWrapsIOException() throws ExecutionException, InterruptedException, IOException { - SpringWebSocketClient client = mock(SpringWebSocketClient.class); - when(client.send(any(nostr.event.message.ReqMessage.class))).thenThrow(new IOException("net-broken")); - WebSocketClientFactory factory = mock(WebSocketClientFactory.class); - Function reqFactory = k -> client; - nostr.api.WebSocketClientHandler handler = - nostr.api.TestHandlerFactory.create( - "relay-x", "wss://relayx", client, reqFactory, factory); - - Filters filters = new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)); - RuntimeException ex = assertThrows(RuntimeException.class, () -> handler.sendRequest(filters, SubscriptionId.of("sub-err"))); - assertEquals("Failed to send request", ex.getMessage()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerSendCloseFrameTest.java b/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerSendCloseFrameTest.java deleted file mode 100644 index a436d1c15..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerSendCloseFrameTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package nostr.api.client; - -import nostr.base.SubscriptionId; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import nostr.event.message.CloseMessage; -import nostr.event.message.ReqMessage; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** Verifies WebSocketClientHandler close sends CLOSE frame and closes client. */ -public class WebSocketHandlerSendCloseFrameTest { - - @Test - void closeSendsCloseFrameAndClosesClient() throws Exception { - SpringWebSocketClient client = mock(SpringWebSocketClient.class); - when(client.subscribe(any(ReqMessage.class), any(), any(), any())).thenReturn(() -> {}); - when(client.subscribe(any(CloseMessage.class), any(), any(), any())).thenReturn(() -> {}); - - WebSocketClientFactory factory = mock(WebSocketClientFactory.class); - Function reqFactory = k -> client; - - nostr.api.WebSocketClientHandler handler = - nostr.api.TestHandlerFactory.create( - "relay-1", "wss://relay1", client, reqFactory, factory); - - AutoCloseable handle = handler.subscribe(new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)), "sub-123", s -> {}, t -> {}); - - // Close and verify a CLOSE frame was sent - handle.close(); - ArgumentCaptor captor = ArgumentCaptor.forClass(CloseMessage.class); - verify(client, atLeastOnce()).subscribe(captor.capture(), any(), any(), any()); - boolean closeSent = captor.getAllValues().stream().anyMatch(m -> m.encode().contains("\"CLOSE\",\"sub-123\"")); - assertTrue(closeSent); - verify(client, atLeastOnce()).close(); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerSendRequestTest.java b/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerSendRequestTest.java deleted file mode 100644 index 2b31bf4b3..000000000 --- a/nostr-java-api/src/test/java/nostr/api/client/WebSocketHandlerSendRequestTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package nostr.api.client; - -import nostr.base.SubscriptionId; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** Tests sendRequest for multiple sub ids and verifying subscription id usage. */ -public class WebSocketHandlerSendRequestTest { - - @Test - void sendsReqWithGivenSubscriptionId() throws ExecutionException, InterruptedException, IOException { - SpringWebSocketClient client = mock(SpringWebSocketClient.class); - when(client.send(any(nostr.event.message.ReqMessage.class))).thenReturn(List.of("OK")); - WebSocketClientFactory factory = mock(WebSocketClientFactory.class); - Function reqFactory = k -> client; - - nostr.api.WebSocketClientHandler handler = - nostr.api.TestHandlerFactory.create( - "relay-1", "wss://relay1", client, reqFactory, factory); - - Filters filters = new Filters(new KindFilter<>(nostr.base.Kind.TEXT_NOTE)); - handler.sendRequest(filters, SubscriptionId.of("sub-A")); - handler.sendRequest(filters, SubscriptionId.of("sub-B")); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(nostr.event.message.ReqMessage.class); - verify(client, times(2)).send(captor.capture()); - assertTrue(captor.getAllValues().get(0).encode().contains("\"sub-A\"")); - assertTrue(captor.getAllValues().get(1).encode().contains("\"sub-B\"")); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/ApiEventIT.java b/nostr-java-api/src/test/java/nostr/api/integration/ApiEventIT.java deleted file mode 100644 index e04aba431..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/ApiEventIT.java +++ /dev/null @@ -1,856 +0,0 @@ -package nostr.api.integration; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.extern.slf4j.Slf4j; -import nostr.api.EventNostr; -import nostr.api.NIP01; -import nostr.api.NIP04; -import nostr.api.NIP15; -import nostr.api.NIP52; -import nostr.api.NIP57; -import nostr.base.GenericTagQuery; -import nostr.base.PrivateKey; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.config.RelayConfig; -import nostr.crypto.bech32.Bech32; -import nostr.crypto.bech32.Bech32Prefix; -import nostr.event.BaseMessage; -import nostr.event.BaseTag; -import nostr.event.entities.CalendarContent; -import nostr.event.entities.Product; -import nostr.event.entities.Stall; -import nostr.event.entities.ZapReceipt; -import nostr.event.filter.Filters; -import nostr.event.filter.GenericTagQueryFilter; -import nostr.event.filter.GeohashTagFilter; -import nostr.event.filter.HashtagTagFilter; -import nostr.event.filter.UrlTagFilter; -import nostr.event.filter.VoteTagFilter; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.json.codec.EventEncodingException; -import nostr.event.message.EoseMessage; -import nostr.event.message.EventMessage; -import nostr.event.message.OkMessage; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.LabelNamespaceTag; -import nostr.event.tag.LabelTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.UrlTag; -import nostr.event.tag.VoteTag; -import nostr.id.Identity; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import java.io.IOException; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.awaitility.Awaitility.await; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringJUnitConfig(RelayConfig.class) -@Slf4j -public class ApiEventIT extends BaseRelayIntegrationTest { - private static final long RELAY_INDEX_DELAY_MS = 100; - - private Map relays; - - @BeforeEach - void setUp() { - // Use the dynamic Testcontainers relay URL instead of static relays.properties - relays = getTestRelays(); - } - - private void waitForRelayIndexing() { - try { - Thread.sleep(RELAY_INDEX_DELAY_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - @Test - public void testNIP01CreateTextNoteEvent() throws Exception { - System.out.println("testNIP01CreateTextNoteEvent"); - - var nip01 = new NIP01(Identity.generateRandomIdentity()); - var instance = - nip01 - .createTextNoteEvent( - List.of(NIP01.createPubKeyTag(Identity.generateRandomIdentity().getPublicKey())), - "Hello simplified nostr-java!") - .getEvent(); - instance.update(); - - assertNotNull(instance.getId()); - assertNotNull(instance.getCreatedAt()); - assertNull(instance.getSignature()); - - final String bech32 = instance.toBech32(); - assertNotNull(bech32); - assertEquals(Bech32Prefix.NOTE.getCode(), Bech32.decode(bech32).hrp); - - await().atMost(Duration.ofSeconds(3)); - } - - @Test - public void testNIP01SendTextNoteEvent() throws IOException { - System.out.println("testNIP01SendTextNoteEvent"); - - var nip01 = new NIP01(Identity.generateRandomIdentity()); - var instance = nip01.createTextNoteEvent("Hello simplified nostr-java!").sign(); - - var response = instance.setRelays(relays).send(); - assertInstanceOf(OkMessage.class, response); - assertEquals(nip01.getEvent().getId(), ((OkMessage) response).getEventId()); - - nip01.close(); - } - - @Test - public void testNIP04SendDirectMessage() throws IOException { - System.out.println("testNIP04SendDirectMessage"); - - var nip04 = - new NIP04( - Identity.generateRandomIdentity(), Identity.generateRandomIdentity().getPublicKey()); - - var instance = - nip04 - .createDirectMessageEvent( - "Quand on n'a que l'amour pour tracer un chemin et forcer le destin...") - .sign(); - - var signature = instance.getEvent().getSignature(); - assertNotNull(signature); - var response = instance.setRelays(relays).send(); - assertInstanceOf(OkMessage.class, response); - assertEquals(nip04.getEvent().getId(), ((OkMessage) response).getEventId()); - - nip04.close(); - } - - @Test - public void testNIP01SendTextNoteEventGeoHashTag() throws IOException { - System.out.println("testNIP01SendTextNoteEventGeoHashTag"); - - String targetString = "geohash_tag-location-testNIP01SendTextNoteEventGeoHashTag"; - GeohashTag geohashTag = new GeohashTag(targetString); - - NIP01 nip01 = new NIP01(Identity.generateRandomIdentity()); - nip01 - .createTextNoteEvent( - List.of(geohashTag), "GeohashTag Test location testNIP01SendTextNoteEventGeoHashTag") - .signAndSend(relays); - - waitForRelayIndexing(); - - Filters filters = new Filters(new GeohashTagFilter<>(new GeohashTag(targetString))); - - List result = nip01.sendRequest(filters, UUID.randomUUID().toString()); - - assertFalse(result.isEmpty()); - assertEquals(2, result.size()); - assertTrue(result.stream().anyMatch(s -> s.contains(targetString))); - - nip01.close(); - } - - @Test - public void testNIP01SendTextNoteEventHashtagTag() throws IOException { - System.out.println("testNIP01SendTextNoteEventHashtagTag"); - - String targetString = "hashtag-tag-value-testNIP01SendTextNoteEventHashtagTag"; - HashtagTag hashtagTag = new HashtagTag(targetString); - - NIP01 nip01 = new NIP01(Identity.generateRandomIdentity()); - nip01 - .createTextNoteEvent( - List.of(hashtagTag), "Hashtag Tag Test value testNIP01SendTextNoteEventHashtagTag") - .signAndSend(relays); - - waitForRelayIndexing(); - - Filters filters = new Filters(new HashtagTagFilter<>(new HashtagTag(targetString))); - - List result = nip01.sendRequest(filters, UUID.randomUUID().toString()); - - // assertFalse(result.isEmpty()); - assertEquals(2, result.size()); - assertTrue(result.stream().anyMatch(s -> s.contains(targetString))); - - nip01.close(); - } - - @Test - public void testNIP01SendTextNoteEventCustomGenericTag() throws IOException { - System.out.println("testNIP01SendTextNoteEventCustomGenericTag"); - - String targetString = "custom-generic-tag-testNIP01SendTextNoteEventCustomGenericTag"; - BaseTag genericTag = BaseTag.create("m", targetString); - - NIP01 nip01 = new NIP01(Identity.generateRandomIdentity()); - nip01 - .createTextNoteEvent( - List.of(genericTag), - "Custom Generic Tag Test testNIP01SendTextNoteEventCustomGenericTag") - .signAndSend(relays); - - waitForRelayIndexing(); - - Filters filters = - new Filters(new GenericTagQueryFilter<>(new GenericTagQuery("#m", targetString))); - - List result = nip01.sendRequest(filters, UUID.randomUUID().toString()); - - assertFalse(result.isEmpty()); - assertEquals(2, result.size()); - - String matcher = - """ - ["m","custom-generic-tag-testNIP01SendTextNoteEventCustomGenericTag"]\ - """; - - assertTrue(result.stream().anyMatch(s -> s.contains(matcher))); - - nip01.close(); - } - - @Test - public void testNIP01SendTextNoteEventRecipientGenericTag() throws IOException { - System.out.println("testNIP01SendTextNoteEventRecipientGenericTag"); - - Identity recipientIdentity = Identity.generateRandomIdentity(); - - PubKeyTag recipientTag = (PubKeyTag) NIP01.createPubKeyTag(recipientIdentity.getPublicKey()); - NIP01 nip01 = new NIP01(Identity.generateRandomIdentity()); - nip01 - .createTextNoteEvent("testNIP01SendTextNoteEventRecipientGenericTag", List.of(recipientTag)) - .signAndSend(relays); - - waitForRelayIndexing(); - - Filters filters = - new Filters( - new GenericTagQueryFilter<>( - new GenericTagQuery("#p", recipientTag.getPublicKey().toString()))); - - List result = nip01.sendRequest(filters, UUID.randomUUID().toString()); - - assertFalse(result.isEmpty()); - assertEquals(2, result.size()); - - String matcher = - """ - ["p","%s"]\ - """ - .formatted(recipientTag.getPublicKey().toString()); - - assertTrue(result.stream().anyMatch(s -> s.contains(matcher))); - - nip01.close(); - } - - @Test - public void testNIP01SendTextNoteEventUrlTag() throws IOException { - System.out.println("testNIP01SendTextNoteEventUrlTag"); - - String targetString = getRelayUri(); - BaseTag genericTag = BaseTag.create("u", targetString); - - NIP01 nip01 = new NIP01(Identity.generateRandomIdentity()); - nip01 - .createTextNoteEvent(List.of(genericTag), "testNIP01SendTextNoteEventUrlTag") - .signAndSend(relays); - - waitForRelayIndexing(); - - Filters filters = - new Filters(new GenericTagQueryFilter<>(new GenericTagQuery("#u", targetString))); - - List result = nip01.sendRequest(filters, UUID.randomUUID().toString()); - - assertEquals(2, result.size()); - - String matcher = - """ - ["u","%s"]\ - """ - .formatted(targetString); - - assertTrue(result.stream().anyMatch(s -> s.contains(matcher))); - - nip01.close(); - } - - @Test - public void testFilterUrlTag() throws IOException { - System.out.println("testFilterUrlTag"); - - String targetString = getRelayUri().replace("ws://", "https://"); - // UrlTag urlTag = new UrlTag(targetString); - BaseTag urlTag = BaseTag.create("u", targetString); - - NIP01 nip01 = new NIP01(Identity.generateRandomIdentity()); - nip01.createTextNoteEvent(List.of(urlTag), "testFilterUrlTag").signAndSend(relays); - - waitForRelayIndexing(); - - Filters filters = new Filters(new UrlTagFilter<>(new UrlTag(targetString))); - - List result = nip01.sendRequest(filters, UUID.randomUUID().toString()); - - assertEquals(2, result.size(), result.toString()); - - String matcher = - """ - ["u","%s"]\ - """ - .formatted(targetString); - - assertTrue(result.stream().anyMatch(s -> s.contains(matcher))); - - List messages = - result.stream() - .map( - json -> { - try { - return new BaseMessageDecoder<>().decode(json); - } catch (EventEncodingException e) { - throw new RuntimeException(e); - } - }) - .toList(); - - assertEquals(2, messages.size()); - - assertInstanceOf(EventMessage.class, messages.get(0)); - assertInstanceOf(EoseMessage.class, messages.get(1)); - - GenericEvent event = (GenericEvent) ((EventMessage) messages.get(0)).getEvent(); - - Optional optionalUrlTag = - event.getTags().stream().filter(t -> t instanceof UrlTag).map(t -> (UrlTag) t).findFirst(); - - assertTrue(optionalUrlTag.isPresent()); - assertEquals(targetString, optionalUrlTag.get().getUrl()); - nip01.close(); - } - - @Test - public void testFiltersListReturnSameSingularEvent() throws IOException { - System.out.println("testFiltersListReturnSameSingularEvent"); - - String geoHashTagTarget = "geohash_tag-location_SameSingularEvent"; - GeohashTag geohashTag = new GeohashTag(geoHashTagTarget); - - String genericTagTarget = "generic-tag-value_SameSingularEvent"; - BaseTag genericTag = BaseTag.create("m", genericTagTarget); - - NIP01 nip01 = new NIP01(Identity.generateRandomIdentity()); - - nip01 - .createTextNoteEvent(List.of(geohashTag, genericTag), "Multiple Filters") - .signAndSend(relays); - - waitForRelayIndexing(); - - Filters filters1 = new Filters(new GeohashTagFilter<>(new GeohashTag(geoHashTagTarget))); - Filters filters2 = - new Filters(new GenericTagQueryFilter<>(new GenericTagQuery("#m", genericTagTarget))); - - List result = - nip01.sendRequest(List.of(filters1, filters2), UUID.randomUUID().toString()); - - assertFalse(result.isEmpty()); - assertEquals(2, result.size()); - assertTrue(result.stream().anyMatch(s -> s.contains(geoHashTagTarget))); - - nip01.close(); - } - - @Test - public void testFiltersListReturnTwoDifferentEvents() throws IOException { - System.out.println("testFiltersListReturnTwoDifferentEvents"); - - // first event - String geoHashTagTarget1 = "geohash_tag-location-1"; - GeohashTag geohashTag1 = new GeohashTag(geoHashTagTarget1); - String genericTagTarget1 = "generic-tag-value-1"; - BaseTag genericTag1 = BaseTag.create("m", genericTagTarget1); - NIP01 nip01_1 = new NIP01(Identity.generateRandomIdentity()); - nip01_1 - .createTextNoteEvent(List.of(geohashTag1, genericTag1), "Multiple Filters 1") - .signAndSend(relays); - - // second event - String geoHashTagTarget2 = "geohash_tag-location-2"; - GeohashTag geohashTag2 = new GeohashTag(geoHashTagTarget2); - String genericTagTarget2 = "generic-tag-value-2"; - BaseTag genericTag2 = BaseTag.create("m", genericTagTarget2); - NIP01 nip01_2 = new NIP01(Identity.generateRandomIdentity()); - nip01_2 - .createTextNoteEvent(List.of(geohashTag2, genericTag2), "Multiple Filters 2") - .signAndSend(relays); - - waitForRelayIndexing(); - - Filters filters1 = - new Filters( - new GeohashTagFilter<>( - new GeohashTag(geoHashTagTarget1))); // 1st filter should find match in 1st event - - Filters filters2 = - new Filters( - new GenericTagQueryFilter<>( - new GenericTagQuery( - "#m", genericTagTarget2))); // 2nd filter should find match in 2nd event - - List result = - nip01_1.sendRequest(List.of(filters1, filters2), UUID.randomUUID().toString()); - - assertFalse(result.isEmpty()); - assertEquals(3, result.size()); - assertTrue(result.stream().anyMatch(s -> s.contains(geoHashTagTarget1))); - assertTrue(result.stream().anyMatch(s -> s.contains(genericTagTarget2))); - - nip01_1.close(); - nip01_2.close(); - } - - @Test - public void testMultipleFiltersDifferentTypesReturnSameEvent() throws IOException { - System.out.println("testMultipleFilters"); - - String geoHashTagTarget = "geohash_tag-location-DifferentTypesReturnSameEvent"; - GeohashTag geohashTag = new GeohashTag(geoHashTagTarget); - - String genericTagTarget = "generic-tag-value-DifferentTypesReturnSameEvent"; - BaseTag genericTag = BaseTag.create("m", genericTagTarget); - - NIP01 nip01 = new NIP01(Identity.generateRandomIdentity()); - nip01 - .createTextNoteEvent(List.of(geohashTag, genericTag), "Multiple Filters") - .signAndSend(relays); - - waitForRelayIndexing(); - - Filters filters = - new Filters( - new GeohashTagFilter<>(new GeohashTag(geoHashTagTarget)), - new GenericTagQueryFilter<>(new GenericTagQuery("#m", genericTagTarget))); - - List result = nip01.sendRequest(filters, UUID.randomUUID().toString()); - - assertFalse(result.isEmpty()); - assertEquals(2, result.size()); - assertTrue(result.stream().anyMatch(s -> s.contains(geoHashTagTarget))); - - nip01.close(); - } - - @Test - public void testNIP04EncryptDecrypt() { - System.out.println("testNIP04EncryptDecrypt"); - - Identity identity = Identity.generateRandomIdentity(); - var nip04 = new NIP04(identity, Identity.generateRandomIdentity().getPublicKey()); - var instance = - nip04 - .createDirectMessageEvent( - "Quand on n'a que l'amour pour tracer un chemin et forcer le destin...") - .sign(); - - var message = NIP04.decrypt(identity, instance.getEvent()); - - assertEquals("Quand on n'a que l'amour pour tracer un chemin et forcer le destin...", message); - } - - @Test - public void testNIP15CreateStallEvent() throws EventEncodingException { - System.out.println("testNIP15CreateStallEvent"); - - Stall stall = createStall(); - var nip15 = new NIP15(Identity.create(PrivateKey.generateRandomPrivKey())); - - // Create and send the nostr event - var instance = nip15.createCreateOrUpdateStallEvent(stall).sign(); - var signature = instance.getEvent().getSignature(); - assertNotNull(signature); - - // Fetch the content and compare with the above original - var content = instance.getEvent().getContent(); - var expected = readStall(content); - - assertEquals(expected, stall); - } - - private Stall readStall(String content) throws EventEncodingException { - try { - return mapper().readValue(content, Stall.class); - } catch (JsonProcessingException e) { - throw new EventEncodingException("Failed to decode stall content", e); - } - } - - @Test - public void testNIP15UpdateStallEvent() throws IOException { - System.out.println("testNIP15UpdateStallEvent"); - - var stall = createStall(); - var nip15 = new NIP15(Identity.create(PrivateKey.generateRandomPrivKey())); - - // Create and send the nostr event - var instance = nip15.createCreateOrUpdateStallEvent(stall).sign(); - var signature = instance.getEvent().getSignature(); - assertNotNull(signature); - - var response = instance.setRelays(relays).send(); - assertInstanceOf(OkMessage.class, response); - assertEquals(nip15.getEvent().getId(), ((OkMessage) response).getEventId()); - - // Update the shipping - var shipping = stall.getShipping(); - shipping.setCost(20.00f); - - EventNostr event = nip15.createCreateOrUpdateStallEvent(stall).sign(); - response = event.setRelays(relays).send(); - assertInstanceOf(OkMessage.class, response); - assertEquals(nip15.getEvent().getId(), ((OkMessage) response).getEventId()); - - nip15.close(); - } - - @Test - public void testNIP15CreateProductEvent() throws IOException { - - System.out.println("testNIP15CreateProductEvent"); - - // Create the stall object - var stall = createStall(); - var nip15 = new NIP15(Identity.create(PrivateKey.generateRandomPrivKey())); - - // Create the product - var product = createProduct(stall); - - List categories = new ArrayList<>(); - categories.add("bijoux"); - categories.add("Hommes"); - - EventNostr event = nip15.createCreateOrUpdateProductEvent(product, categories).sign(); - var response = event.setRelays(relays).send(); - assertInstanceOf(OkMessage.class, response); - assertEquals(nip15.getEvent().getId(), ((OkMessage) response).getEventId()); - - nip15.close(); - } - - @Test - public void testNIP15UpdateProductEvent() throws IOException { - - System.out.println("testNIP15UpdateProductEvent"); - - // Create the stall object - var stall = createStall(); - var nip15 = new NIP15(Identity.create(PrivateKey.generateRandomPrivKey())); - - // Create the product - var product = createProduct(stall); - - List categories = new ArrayList<>(); - categories.add("bijoux"); - categories.add("Hommes"); - - EventNostr event1 = nip15.createCreateOrUpdateProductEvent(product, categories).sign(); - var response = event1.setRelays(relays).send(); - assertInstanceOf(OkMessage.class, response); - assertEquals(nip15.getEvent().getId(), ((OkMessage) response).getEventId()); - - product.setDescription("Un nouveau bijou en or"); - categories.add("bagues"); - - EventNostr event2 = nip15.createCreateOrUpdateProductEvent(product, categories).sign(); - response = event2.setRelays(relays).send(); - assertInstanceOf(OkMessage.class, response); - assertEquals(nip15.getEvent().getId(), ((OkMessage) response).getEventId()); - - nip15.close(); - } - - /* - @Test - public void testNIP32CreateNameSpace() { - - System.out.println("testNIP32CreateNameSpace"); - - var langNS = NIP32.createNameSpaceTag("Languages"); - - assertEquals("L", langNS.getCode()); - assertEquals(1, langNS.getAttributes().size()); - assertEquals("Languages", langNS.getAttributes().iterator().next().value()); - } - - @Test - public void testNIP32CreateLabel1() { - - System.out.println("testNIP32CreateLabel1"); - - var label = NIP32.createLabelTag("Languages", "english"); - - assertEquals("l", label.getCode()); - assertEquals(2, label.getAttributes().size()); - assertTrue(label.getAttributes().contains(new ElementAttribute("param0", "english"))); - assertTrue(label.getAttributes().contains(new ElementAttribute("param1", "Languages"))); - } - @Test - public void testNIP32CreateLabel2() { - - System.out.println("testNIP32CreateLabel2"); - - var label = NIP32.createLabelTag("Languages", "english"); - - assertEquals("l", label.getCode()); - assertTrue(label.getAttributes().contains(new ElementAttribute("param0", "english"))); - assertTrue(label.getAttributes().contains(new ElementAttribute("param1", "Languages"))); - } - */ - - @Test - public void testNIP52CalendarTimeBasedEventEvent() throws IOException { - System.out.println("testNIP52CalendarTimeBasedEventEvent"); - - CalendarContent calendarContent = - new CalendarContent<>( - new IdentifierTag("UUID-CalendarTimeBasedEventTest"), - "Calendar Time-Based Event title", - 1716513986268L); - - calendarContent.setStartTzid("1687765220"); - calendarContent.setEndTzid("1687765230"); - calendarContent.addLabelNamespaceTags(List.of(new LabelNamespaceTag("audiospace"))); - calendarContent.addLabelTags( - List.of( - new LabelTag("english", "audiospace"), new LabelTag("mycenaean greek", "audiospace"))); - - List tags = new ArrayList<>(); - tags.add( - new PubKeyTag(Identity.generateRandomIdentity().getPublicKey(), getRelayUri(), "ISSUER")); - tags.add(new PubKeyTag(Identity.generateRandomIdentity().getPublicKey(), "", "COUNTERPARTY")); - - var nip52 = new NIP52(Identity.create(PrivateKey.generateRandomPrivKey())); - EventNostr event = nip52.createCalendarTimeBasedEvent(tags, "content", calendarContent).sign(); - var response = event.setRelays(relays).send(); - assertInstanceOf(OkMessage.class, response); - assertEquals(nip52.getEvent().getId(), ((OkMessage) response).getEventId()); - - nip52.close(); - } - - @Test - void testNIP57CreateZapRequestEvent() throws Exception { - System.out.println("testNIP57CreateZapRequestEvent"); - - var nip57 = new NIP57(Identity.generateRandomIdentity()); - final String ZAP_REQUEST_CONTENT = "zap request content"; - final Long AMOUNT = 1232456L; - final String LNURL = "lnUrl"; - final String RELAYS_TAG = getRelayUri(); - - var instance = - nip57 - .createZapRequestEvent( - AMOUNT, - LNURL, - List.of(new Relay(RELAYS_TAG)), - ZAP_REQUEST_CONTENT, - Identity.generateRandomIdentity().getPublicKey(), - null, - null) - .getEvent(); - - instance.update(); - - assertNotNull(instance.getId()); - assertNotNull(instance.getCreatedAt()); - assertNotNull(instance.getContent()); - assertNull(instance.getSignature()); - - // TODO test with the tags - - /* - assertNotNull(instance.getZapRequest()); - assertNotNull(instance.getZapRequest().getRelaysTag()); - assertNotNull(instance.getZapRequest().getAmount()); - assertNotNull(instance.getZapRequest().getLnUrl()); - - assertEquals(ZAP_REQUEST_CONTENT, instance.getContent()); - assertTrue(instance.getZapRequest().getRelaysTag().getRelays().stream() - .anyMatch(relay -> relay.getUri().equals(RELAYS_TAG))); - assertEquals(AMOUNT, instance.getZapRequest().getAmount()); - assertEquals(LNURL, instance.getZapRequest().getLnUrl()); - */ - - final String bech32 = instance.toBech32(); - assertNotNull(bech32); - assertEquals(Bech32Prefix.NOTE.getCode(), Bech32.decode(bech32).hrp); - } - - @Test - void testNIP57CreateZapReceiptEvent() throws Exception { - System.out.println("testNIP57CreateZapReceiptEvent"); - - String zapRequestPubKeyTag = Identity.generateRandomIdentity().getPublicKey().toString(); - String zapRequestEventTag = Identity.generateRandomIdentity().getPublicKey().toString(); - String zapSender = Identity.generateRandomIdentity().getPublicKey().toString(); - PublicKey zapRecipient = Identity.generateRandomIdentity().getPublicKey(); - final String ZAP_RECEIPT_IDENTIFIER = "ipsum"; - final String ZAP_RECEIPT_RELAY_URI = getRelayUri(); - final String BOLT_11 = "lnbc12324560p1pqwertyuiopasd"; // Valid BOLT11 format (1232456 picoBTC = 1232456 msat) - final String DESCRIPTION_SHA256 = "descriptionSha256"; - final String PRE_IMAGE = "preimage"; - var nip57 = new NIP57(Identity.generateRandomIdentity()); - - var zapReceipt = new ZapReceipt(BOLT_11, DESCRIPTION_SHA256, PRE_IMAGE); - - /* - var instance = nip57.createZapReceiptEvent( - new PubKeyTag(new PublicKey(zapRequestPubKeyTag)), - new EventTag(zapRequestEventTag), - new PublicKey(zapSender), - zapRecipient, - new AddressTag(Kind.ZAP_RECEIPT.getValue(), new PublicKey(zapSender), new IdentifierTag(ZAP_RECEIPT_IDENTIFIER), new Relay(ZAP_RECEIPT_RELAY_URI)), - zapReceipt, - DESCRIPTION_SHA256) - .getEvent(); - */ - final String ZAP_REQUEST_CONTENT = "zap request content"; - final Long AMOUNT = 1232456L; - final String LNURL = "lnUrl"; - final String RELAYS_TAG = getRelayUri(); - - var zapRequestEvent = - nip57 - .createZapRequestEvent( - AMOUNT, - LNURL, - List.of(new Relay(RELAYS_TAG)), - ZAP_REQUEST_CONTENT, - zapRecipient, - null, - null) - .getEvent(); - - var instance = - nip57.createZapReceiptEvent(zapRequestEvent, BOLT_11, PRE_IMAGE, zapRecipient).getEvent(); - - instance.update(); - - assertNotNull(instance.getId()); - assertNotNull(instance.getCreatedAt()); - assertNull(instance.getSignature()); - - // TODO test with the tags - /* - assertNotNull(instance.getZapReceipt()); - assertNotNull(instance.getZapReceipt().getBolt11()); - assertNotNull(instance.getZapReceipt().getDescriptionSha256()); - assertNotNull(instance.getZapReceipt().getPreimage()); - - assertEquals(BOLT_11, instance.getZapReceipt().getBolt11()); - assertEquals(DESCRIPTION_SHA256, instance.getZapReceipt().getDescriptionSha256()); - assertEquals(PRE_IMAGE, instance.getZapReceipt().getPreimage()); - */ - - final String bech32 = instance.toBech32(); - assertNotNull(bech32); - assertEquals(Bech32Prefix.NOTE.getCode(), Bech32.decode(bech32).hrp); - } - - private static List getBaseTags() { - return new ArrayList(); - } - - public static Stall createStall() { - - // Create the county list - List countries = new ArrayList<>(); - countries.add("France"); - countries.add("Canada"); - countries.add("Cameroun"); - - // Create the shipping object - var shipping = new Stall.Shipping(); - shipping.setCost(12.00f); - shipping.setCountries(countries); - shipping.setName("French Countries"); - - // Create the stall object - var stall = new Stall(); - stall.setCurrency("USD"); - stall.setDescription("This is a test stall"); - stall.setName("Maximus Primus"); - stall.setShipping(shipping); - - return stall; - } - - public static Product createProduct(Stall stall) { - - // Create the product - var product = new Product(); - product.setCurrency("USD"); - product.setDescription("Un bijou en or"); - product.setImages(new ArrayList<>()); - product.setName("Bague"); - product.setPrice(450.00f); - product.setQuantity(4); - List specs = new ArrayList<>(); - specs.add(new Product.Spec("couleur", "or")); - specs.add(new Product.Spec("poids", "150g")); - product.setSpecs(specs); - product.setStall(stall); - - return product; - } - - @Test - public void testNIP01SendTextNoteEventVoteTag() throws IOException { - System.out.println("testNIP01SendTextNoteEventVoteTag"); - - Integer targetVote = 1; - VoteTag voteTag = new VoteTag(targetVote); - - NIP01 nip01 = new NIP01(Identity.generateRandomIdentity()); - nip01 - .createTextNoteEvent( - List.of(voteTag), "Vote Tag Test value testNIP01SendTextNoteEventVoteTag") - .signAndSend(relays); - - waitForRelayIndexing(); - - Filters filters = new Filters(new VoteTagFilter<>(new VoteTag(targetVote))); - - List result = nip01.sendRequest(filters, UUID.randomUUID().toString()); - - assertFalse(result.isEmpty()); - assertEquals(2, result.size()); - assertTrue(result.stream().anyMatch(s -> s.contains(targetVote.toString()))); - - nip01.close(); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/ApiEventTestUsingSpringWebSocketClientIT.java b/nostr-java-api/src/test/java/nostr/api/integration/ApiEventTestUsingSpringWebSocketClientIT.java deleted file mode 100644 index b469753ed..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/ApiEventTestUsingSpringWebSocketClientIT.java +++ /dev/null @@ -1,109 +0,0 @@ -package nostr.api.integration; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import nostr.api.NIP15; -import nostr.base.PrivateKey; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.StandardWebSocketClient; -import nostr.event.impl.GenericEvent; -import nostr.event.message.EventMessage; -import nostr.id.Identity; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import static nostr.api.integration.ApiEventIT.createProduct; -import static nostr.api.integration.ApiEventIT.createStall; -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; - -class ApiEventTestUsingSpringWebSocketClientIT extends BaseRelayIntegrationTest { - private static final int MAX_CLIENT_CONNECTION_ATTEMPTS = 3; - private static final long CONNECTION_RETRY_DELAY_MS = 1_000L; - - @Test - // Executes the NIP-15 product event test against every configured relay endpoint. - void doForEach() throws InterruptedException { - // Give the relay a moment to fully initialize after container startup - Thread.sleep(500); - List.of(getRelayUri()) - .forEach( - relayUri -> { - try { - testNIP15SendProductEventUsingSpringWebSocketClient(relayUri); - } catch (java.io.IOException e) { - Assertions.fail("Failed to execute NIP-15 test for relay " + relayUri, e); - } - }); - } - - void testNIP15SendProductEventUsingSpringWebSocketClient( - String relayUri) throws java.io.IOException { - System.out.println("testNIP15CreateProductEventUsingSpringWebSocketClient"); - var product = createProduct(createStall()); - - List categories = new ArrayList<>(); - categories.add("bijoux"); - categories.add("Hommes"); - - var nip15 = new NIP15(Identity.create(PrivateKey.generateRandomPrivKey())); - - GenericEvent event = - nip15.createCreateOrUpdateProductEvent(product, categories).sign().getEvent(); - EventMessage message = new EventMessage(event); - - try (SpringWebSocketClient client = createSpringWebSocketClient(relayUri)) { - String eventResponse = client.send(message).stream().findFirst().orElseThrow(); - - try { - JsonNode actualNode = mapper().readTree(eventResponse); - - // Verify OK response format: ["OK", "", , ""] - assertEquals("OK", actualNode.get(0).asText(), "Response should be an OK message"); - assertEquals(event.getId(), actualNode.get(1).asText(), "Event ID should match"); - // Note: success flag (element 2) varies by relay implementation, so we just log it - System.out.println("Relay response: success=" + actualNode.get(2).asBoolean() - + ", message=" + (actualNode.has(3) ? actualNode.get(3).asText() : "none")); - } catch (JsonProcessingException ex) { - Assertions.fail("Failed to parse relay response JSON: " + ex.getMessage(), ex); - } - } - } - - private SpringWebSocketClient createSpringWebSocketClient(String relayUri) { - ExecutionException lastException = null; - - for (int attempt = 1; attempt <= MAX_CLIENT_CONNECTION_ATTEMPTS; attempt++) { - try { - return new SpringWebSocketClient(new StandardWebSocketClient(relayUri), relayUri); - } catch (ExecutionException e) { - lastException = e; - delayBeforeRetry(attempt); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while connecting to " + relayUri, e); - } - } - - throw new IllegalStateException( - "Failed to initialize WebSocket client for " + relayUri + " after " - + MAX_CLIENT_CONNECTION_ATTEMPTS - + " attempts", - lastException); - } - - private void delayBeforeRetry(int attempt) { - if (attempt >= MAX_CLIENT_CONNECTION_ATTEMPTS) { - return; - } - try { - Thread.sleep(CONNECTION_RETRY_DELAY_MS); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP52EventIT.java b/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP52EventIT.java deleted file mode 100644 index 5df791934..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP52EventIT.java +++ /dev/null @@ -1,113 +0,0 @@ -package nostr.api.integration; - -import nostr.api.NIP52; -import nostr.base.PrivateKey; -import nostr.base.PublicKey; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.StandardWebSocketClient; -import nostr.event.BaseTag; -import nostr.event.entities.CalendarContent; -import nostr.event.impl.GenericEvent; -import nostr.event.message.EventMessage; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; - -@ActiveProfiles("test") -class ApiNIP52EventIT extends BaseRelayIntegrationTest { - private static final int MAX_CLIENT_CONNECTION_ATTEMPTS = 3; - private static final long CONNECTION_RETRY_DELAY_MS = 1_000L; - - @Test - void testNIP52CalendarTimeBasedEventEventUsingSpringWebSocketClient() - throws IOException, InterruptedException { - // Give the relay a moment to fully initialize after container startup - Thread.sleep(500); - System.out.println("testNIP52CalendarTimeBasedEventEventUsingSpringWebSocketClient"); - - List tags = new ArrayList<>(); - tags.add( - new PubKeyTag( - new PublicKey("2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76985"), - null, - "PAYER")); - tags.add( - new PubKeyTag( - new PublicKey("494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4347"), - null, - "PAYEE")); - - var nip52 = new NIP52(Identity.create(PrivateKey.generateRandomPrivKey())); - - GenericEvent event = - nip52 - .createCalendarTimeBasedEvent(tags, "content", createCalendarContent()) - .sign() - .getEvent(); - EventMessage message = new EventMessage(event); - - try (SpringWebSocketClient client = createSpringWebSocketClient(getRelayUri())) { - var actualJson = - mapper().readTree(client.send(message).stream().findFirst().orElseThrow()); - - // Verify OK response format: ["OK", "", , ""] - assertEquals("OK", actualJson.get(0).asText(), "Response should be an OK message"); - assertEquals(event.getId(), actualJson.get(1).asText(), "Event ID should match"); - // Note: success flag (element 2) varies by relay implementation, so we just log it - System.out.println("Relay response: success=" + actualJson.get(2).asBoolean() - + ", message=" + (actualJson.has(3) ? actualJson.get(3).asText() : "none")); - } - } - - private SpringWebSocketClient createSpringWebSocketClient(String relayUri) { - ExecutionException lastException = null; - - for (int attempt = 1; attempt <= MAX_CLIENT_CONNECTION_ATTEMPTS; attempt++) { - try { - return new SpringWebSocketClient(new StandardWebSocketClient(relayUri), relayUri); - } catch (ExecutionException e) { - lastException = e; - delayBeforeRetry(attempt); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while connecting to " + relayUri, e); - } - } - - throw new IllegalStateException( - "Failed to initialize WebSocket client for " - + relayUri - + " after " - + MAX_CLIENT_CONNECTION_ATTEMPTS - + " attempts", - lastException); - } - - private void delayBeforeRetry(int attempt) { - if (attempt >= MAX_CLIENT_CONNECTION_ATTEMPTS) { - return; - } - try { - Thread.sleep(CONNECTION_RETRY_DELAY_MS); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } - } - - private CalendarContent createCalendarContent() { - return new CalendarContent<>( - new IdentifierTag("UUID-CalendarTimeBasedEventTest"), - "Calendar Time-Based Event title", - 1716513986268L); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP52RequestIT.java b/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP52RequestIT.java deleted file mode 100644 index 947cd2cb9..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP52RequestIT.java +++ /dev/null @@ -1,256 +0,0 @@ -package nostr.api.integration; - -import nostr.api.NIP52; -import nostr.base.PublicKey; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.StandardWebSocketClient; -import nostr.event.BaseTag; -import nostr.event.entities.CalendarContent; -import nostr.event.impl.GenericEvent; -import nostr.event.message.EventMessage; -import nostr.event.tag.EventTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.ReferenceTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; - -@ActiveProfiles("test") -class ApiNIP52RequestIT extends BaseRelayIntegrationTest { - private static final String PRV_KEY_VALUE = - "23c011c4c02de9aa98d48c3646c70bb0e7ae30bdae1dfed4d251cbceadaeeb7b"; - private static final String UUID_CALENDAR_TIME_BASED_EVENT_TEST = - "UUID-CalendarTimeBasedEventTest"; - - public static final String ID = - "299ab85049a7923e9cd82329c0fa489ca6fd6d21feeeac33543b1237e14a9e07"; - public static final String KIND = "31923"; - public static final String CALENDAR_CONTENT = "calendar content"; - public static final String PUB_KEY = - "cccd79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984"; - public static final String CREATED_AT = "1726114798510"; - public static final String START = "1726114798610"; - public static final String END = "1726114798710"; - - public static final String START_TZID = "America/Costa_Rica"; - public static final String END_TZID = "America/Costa_Rica"; - - public static final String E_TAG_HEX = - "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"; - public static final String G_TAG_VALUE = "calendar geo-tag-1"; - public static final String T_TAG_VALUE = "calendar hash-tag-1111"; - public static final String P1_TAG_HEX = - "444d79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984"; - public static final String P1_ROLE = "PAYER"; - public static final String P2_ROLE = "PAYEE"; - - public static final String P2_TAG_HEX = - "555d79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984"; - public static final String TITLE = "calendar title"; - public static final String SUMMARY = "calendar summary"; - public static final String LOCATION = "calendar location"; - - public static final EventTag E_TAG = new EventTag(E_TAG_HEX); - public static final GeohashTag G_TAG = new GeohashTag(G_TAG_VALUE); - public static final HashtagTag T_TAG = new HashtagTag(T_TAG_VALUE); - - public static final String LABEL_NAMESPACE = "audiospace"; - public static final String LABEL_1 = "calendar label 1 of 2"; - public static final String LABEL_2 = "calendar label 2 of 2"; - - public static final String START_TZID_CODE = "start_tzid"; - public static final String END_TZID_CODE = "end_tzid"; - public static final String SUMMARY_CODE = "summary"; - public static final String LABEL_CODE = "l"; - public static final String LOCATION_CODE = "location"; - public static final String END_CODE = "end"; - - public String eventId; - public String eventPubKey; - public String signature; - - @Test - void testNIP99CalendarContentPreRequest() throws Exception { - System.out.println("testNIP52CalendarContentEvent"); - - List tags = new ArrayList<>(); - tags.add(E_TAG); - PubKeyTag p1Tag = new PubKeyTag(new PublicKey(P1_TAG_HEX), getRelayUri(), P1_ROLE); - PubKeyTag p2Tag = new PubKeyTag(new PublicKey(P2_TAG_HEX), getRelayUri(), P2_ROLE); - tags.add(p1Tag); - tags.add(p2Tag); - tags.add(BaseTag.create(START_TZID_CODE, START_TZID)); - tags.add(BaseTag.create(END_TZID_CODE, END_TZID)); - tags.add(BaseTag.create(SUMMARY_CODE, SUMMARY)); - tags.add(BaseTag.create(LABEL_CODE, LABEL_1, LABEL_NAMESPACE)); - tags.add(BaseTag.create(LABEL_CODE, LABEL_2, LABEL_NAMESPACE)); - tags.add(BaseTag.create(LOCATION_CODE, LOCATION)); - tags.add(BaseTag.create(END_CODE, END)); - tags.add(G_TAG); - tags.add(T_TAG); - tags.add(new ReferenceTag(URI.create(getRelayUri()))); - - CalendarContent calendarContent = - new CalendarContent<>( - new IdentifierTag(UUID_CALENDAR_TIME_BASED_EVENT_TEST), TITLE, Long.valueOf(START)); - - var nip52 = new NIP52(Identity.create(PRV_KEY_VALUE)); - - GenericEvent event = - nip52 - .createCalendarTimeBasedEvent(tags, CALENDAR_CONTENT, calendarContent) - .sign() - .getEvent(); - eventId = event.getId(); - signature = event.getSignature().toString(); - eventPubKey = event.getPubKey().toString(); - EventMessage eventMessage = new EventMessage(event); - - try (SpringWebSocketClient springWebSocketEventClient = - new SpringWebSocketClient(new StandardWebSocketClient(getRelayUri()), getRelayUri())) { - String eventResponse = - springWebSocketEventClient.send(eventMessage).stream().findFirst().orElseThrow(); - - // Extract and compare only first 3 elements of the JSON array - var expectedArray = - mapper().readTree(expectedEventResponseJson(event.getId())).get(0).asText(); - var expectedSubscriptionId = - mapper().readTree(expectedEventResponseJson(event.getId())).get(1).asText(); - var expectedSuccess = - mapper().readTree(expectedEventResponseJson(event.getId())).get(2).asBoolean(); - - var actualArray = mapper().readTree(eventResponse).get(0).asText(); - var actualSubscriptionId = mapper().readTree(eventResponse).get(1).asText(); - var actualSuccess = mapper().readTree(eventResponse).get(2).asBoolean(); - - assertEquals(expectedArray, actualArray, "First element should match"); - assertEquals(expectedSubscriptionId, actualSubscriptionId, "Subscription ID should match"); - assertEquals(expectedSuccess, actualSuccess, "Success flag should match"); - } - - // TODO - This assertion fails with superdonductor and nostr-rs-relay - - try (SpringWebSocketClient springWebSocketRequestClient = - new SpringWebSocketClient(new StandardWebSocketClient(getRelayUri()), getRelayUri())) { - String subscriberId = UUID.randomUUID().toString(); - String reqJson = createReqJson(subscriberId, eventId); - String reqResponse = - springWebSocketRequestClient.send(reqJson).stream().findFirst().orElseThrow(); - - String expected = expectedRequestResponseJson(subscriberId); - // TODO - This assertion keeps failing... - /* - assertTrue( - JsonComparator.isEquivalentJson( - mapper().readTree(expected), - mapper().readTree(reqResponse))); - */ - } - } - - private String expectedEventResponseJson(String subscriptionId) { - return "[\"OK\",\"" + subscriptionId + "\",true,\"success: request processed\"]"; - } - - private String createReqJson(String subscriberId, String id) { - return "[\"REQ\",\"" + subscriberId + "\",{\"ids\":[\"" + id + "\"]}]"; - } - - private String expectedRequestResponseJson(String subscriberId) { - return " [\"EVENT\",\"" - + subscriberId - + "\",\n" - + " {\"id\": \"" - + eventId - + "\",\n" - + " \"kind\": " - + KIND - + ",\n" - + " \"content\": \"" - + CALENDAR_CONTENT - + "\",\n" - + " \"pubkey\": \"" - + eventPubKey - + "\",\n" - + " \"created_at\": " - + CREATED_AT - + ",\n" - + " \"tags\": [\n" - + " [ \"e\", \"" - + E_TAG.getIdEvent() - + "\" ],\n" - + " [ \"g\", \"" - + G_TAG.getLocation() - + "\" ],\n" - + " [ \"t\", \"" - + T_TAG.getHashTag() - + "\" ],\n" - + " [ \"d\", \"" - + UUID_CALENDAR_TIME_BASED_EVENT_TEST - + "\" ],\n" - + " [ \"p\", \"" - + P1_TAG_HEX - + "\", \"" - + getRelayUri() - + "\", \"" - + P1_ROLE - + "\" ],\n" - + " [ \"p\", \"" - + P2_TAG_HEX - + "\", \"" - + getRelayUri() - + "\", \"" - + P2_ROLE - + "\" ],\n" - + " [ \"start_tzid\", \"" - + START_TZID - + "\" ],\n" - + " [ \"end_tzid\", \"" - + END_TZID - + "\" ],\n" - + " [ \"summary\", \"" - + SUMMARY - + "\" ],\n" - + " [ \"l\", \"" - + LABEL_1 - + "\", \"" - + LABEL_NAMESPACE - + "\" ],\n" - + " [ \"l\", \"" - + LABEL_2 - + "\", \"" - + LABEL_NAMESPACE - + "\" ],\n" - + " [ \"location\", \"" - + LOCATION - + "\" ],\n" - + " [ \"r\", \"" - + URI.create(getRelayUri()) - + "\" ],\n" - + " [ \"title\", \"" - + TITLE - + "\" ],\n" - + " [ \"start\", \"" - + START - + "\" ],\n" - + " [ \"end\", \"" - + END - + "\" ]\n" - + " ],\n" - + " \"sig\": \"" - + signature - + "\"\n" - + " }]"; - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP99EventIT.java b/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP99EventIT.java deleted file mode 100644 index 055166940..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP99EventIT.java +++ /dev/null @@ -1,139 +0,0 @@ -package nostr.api.integration; - -import nostr.api.NIP99; -import nostr.base.PrivateKey; -import nostr.base.PublicKey; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.StandardWebSocketClient; -import nostr.event.BaseTag; -import nostr.event.entities.ClassifiedListing; -import nostr.event.impl.GenericEvent; -import nostr.event.message.EventMessage; -import nostr.event.tag.EventTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.PriceTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.SubjectTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -import java.io.IOException; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; - -@ActiveProfiles("test") -class ApiNIP99EventIT extends BaseRelayIntegrationTest { - public static final String CLASSIFIED_LISTING_CONTENT = "classified listing content"; - - public static final String PTAG_HEX = - "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76985"; - public static final String ETAG_HEX = - "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4347"; - - public static final PubKeyTag P_TAG = new PubKeyTag(new PublicKey(PTAG_HEX)); - public static final EventTag E_TAG = new EventTag(ETAG_HEX); - - public static final String SUBJECT = "Classified Listing Test Subject Tag"; - public static final SubjectTag SUBJECT_TAG = new SubjectTag(SUBJECT); - public static final GeohashTag G_TAG = new GeohashTag("Classified Listing Test Geohash Tag"); - public static final HashtagTag T_TAG = new HashtagTag("Classified Listing Test Hashtag Tag"); - - public static final BigDecimal NUMBER = new BigDecimal("2.71"); - public static final String FREQUENCY = "NANOSECOND"; - public static final String CURRENCY = "BTC"; - - public static final String CLASSIFIED_LISTING_PUBLISHED_AT = "1687765220"; - public static final String CLASSIFIED_LISTING_LOCATION = "classified listing location"; - public static final String TITLE_CODE = "title"; - public static final String SUMMARY_CODE = "summary"; - public static final String PUBLISHED_AT_CODE = "published_at"; - public static final String LOCATION_CODE = "location"; - - private static final int MAX_CLIENT_CONNECTION_ATTEMPTS = 3; - private static final long CONNECTION_RETRY_DELAY_MS = 1_000L; - - @Test - void testNIP99ClassifiedListingEvent() throws IOException, InterruptedException { - // Give the relay a moment to fully initialize after container startup - Thread.sleep(500); - System.out.println("testNIP99ClassifiedListingEvent"); - - List tags = new ArrayList<>(); - tags.add(E_TAG); - tags.add(P_TAG); - tags.add(BaseTag.create(PUBLISHED_AT_CODE, CLASSIFIED_LISTING_PUBLISHED_AT)); - tags.add(BaseTag.create(LOCATION_CODE, CLASSIFIED_LISTING_LOCATION)); - tags.add(SUBJECT_TAG); - tags.add(G_TAG); - tags.add(T_TAG); - - PriceTag priceTag = new PriceTag(NUMBER, CURRENCY, FREQUENCY); - ClassifiedListing classifiedListing = - ClassifiedListing.builder(TITLE_CODE, SUMMARY_CODE, priceTag).build(); - - classifiedListing.setPublishedAt(Long.parseLong(CLASSIFIED_LISTING_PUBLISHED_AT)); - - var nip99 = new NIP99(Identity.create(PrivateKey.generateRandomPrivKey())); - - GenericEvent event = - nip99 - .createClassifiedListingEvent(tags, CLASSIFIED_LISTING_CONTENT, classifiedListing) - .sign() - .getEvent(); - EventMessage message = new EventMessage(event); - - try (SpringWebSocketClient client = createSpringWebSocketClient(getRelayUri())) { - String eventResponse = client.send(message).stream().findFirst().orElseThrow(); - var actualJson = mapper().readTree(eventResponse); - - // Verify OK response format: ["OK", "", , ""] - assertEquals("OK", actualJson.get(0).asText(), "Response should be an OK message"); - assertEquals(event.getId(), actualJson.get(1).asText(), "Event ID should match"); - // Note: success flag (element 2) varies by relay implementation, so we just log it - System.out.println("Relay response: success=" + actualJson.get(2).asBoolean() - + ", message=" + (actualJson.has(3) ? actualJson.get(3).asText() : "none")); - } - } - - private SpringWebSocketClient createSpringWebSocketClient(String relayUri) { - ExecutionException lastException = null; - - for (int attempt = 1; attempt <= MAX_CLIENT_CONNECTION_ATTEMPTS; attempt++) { - try { - return new SpringWebSocketClient(new StandardWebSocketClient(relayUri), relayUri); - } catch (ExecutionException e) { - lastException = e; - delayBeforeRetry(attempt); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while connecting to " + relayUri, e); - } - } - - throw new IllegalStateException( - "Failed to initialize WebSocket client for " - + relayUri - + " after " - + MAX_CLIENT_CONNECTION_ATTEMPTS - + " attempts", - lastException); - } - - private void delayBeforeRetry(int attempt) { - if (attempt >= MAX_CLIENT_CONNECTION_ATTEMPTS) { - return; - } - try { - Thread.sleep(CONNECTION_RETRY_DELAY_MS); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP99RequestIT.java b/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP99RequestIT.java deleted file mode 100644 index 76ea9ba59..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/ApiNIP99RequestIT.java +++ /dev/null @@ -1,235 +0,0 @@ -package nostr.api.integration; - -import com.fasterxml.jackson.databind.JsonNode; -import nostr.api.NIP99; -import nostr.base.PublicKey; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.StandardWebSocketClient; -import nostr.event.BaseTag; -import nostr.event.entities.ClassifiedListing; -import nostr.event.impl.GenericEvent; -import nostr.event.message.EventMessage; -import nostr.event.tag.EventTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.PriceTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.SubjectTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@ActiveProfiles("test") -class ApiNIP99RequestIT extends BaseRelayIntegrationTest { - private static final String PRV_KEY_VALUE = - "23c011c4c02de9aa98d48c3646c70bb0e7ae30bdae1dfed4d251cbceadaeeb7b"; - public static final String PUBLISHED_AT_CODE = "published_at"; - public static final String LOCATION_CODE = "location"; - - public static final String ID = - "299ab85049a7923e9cd82329c0fa489ca6fd6d21feeeac33543b1237e14a9e07"; - public static final String KIND = "30402"; - public static final String CLASSIFIED_CONTENT = "classified content"; - public static final String PUB_KEY = - "cccd79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984"; - public static final String CREATED_AT = "1726114798510"; - public static final String E_TAG_HEX = - "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"; - public static final String G_TAG_VALUE = "classified geo-tag-1"; - public static final String T_TAG_VALUE = "classified hash-tag-1111"; - public static final String P_TAG_HEX = - "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984"; - public static final String SUBJECT = "classified subject"; - public static final String TITLE = "classified title"; - public static final String SUMMARY = "classified summary"; - public static final String LOCATION = "classified location"; - - public static final SubjectTag SUBJECT_TAG = new SubjectTag(SUBJECT); - public static final EventTag E_TAG = new EventTag(E_TAG_HEX); - public static final PubKeyTag P_TAG = new PubKeyTag(new PublicKey(P_TAG_HEX)); - public static final GeohashTag G_TAG = new GeohashTag(G_TAG_VALUE); - public static final HashtagTag T_TAG = new HashtagTag(T_TAG_VALUE); - - public static final String PRICE_NUMBER = "271.00"; - public static final String CURRENCY = "BTC"; - public static final String FREQUENCY = "1"; - public static final BigDecimal NUMBER = new BigDecimal(PRICE_NUMBER); - - public String eventId; - public Long eventCreatedAt; - public String eventPubKey; - public String signature; - - @Test - void testNIP99ClassifiedListingPreRequest() throws Exception { - System.out.println("testNIP99ClassifiedListingEvent"); - - List tags = new ArrayList<>(); - tags.add(E_TAG); - tags.add(P_TAG); - tags.add(BaseTag.create(PUBLISHED_AT_CODE, CREATED_AT)); - tags.add(BaseTag.create(LOCATION_CODE, LOCATION)); - tags.add(SUBJECT_TAG); - tags.add(G_TAG); - tags.add(T_TAG); - - PriceTag priceTag = new PriceTag(NUMBER, CURRENCY, FREQUENCY); - ClassifiedListing classifiedListing = - ClassifiedListing.builder(TITLE, SUMMARY, priceTag).build(); - - classifiedListing.setPublishedAt(Long.parseLong(CREATED_AT)); - classifiedListing.setLocation(LOCATION); - - var nip99 = new NIP99(Identity.create(PRV_KEY_VALUE)); - - GenericEvent event = - nip99 - .createClassifiedListingEvent(tags, CLASSIFIED_CONTENT, classifiedListing) - .sign() - .getEvent(); - eventId = event.getId(); - eventCreatedAt = event.getCreatedAt(); - signature = event.getSignature().toString(); - eventPubKey = event.getPubKey().toString(); - EventMessage eventMessage = new EventMessage(event); - - try (SpringWebSocketClient springWebSocketEventClient = - new SpringWebSocketClient(new StandardWebSocketClient(getRelayUri()), getRelayUri())) { - List eventResponses = springWebSocketEventClient.send(eventMessage); - - assertEquals( - 1, eventResponses.size(), "Expected 1 event response, but got " + eventResponses.size()); - - // Extract and compare only first 3 elements of the JSON array - var expectedArray = - mapper().readTree(expectedEventResponseJson(event.getId())).get(0).asText(); - var expectedSubscriptionId = - mapper().readTree(expectedEventResponseJson(event.getId())).get(1).asText(); - var expectedSuccess = - mapper().readTree(expectedEventResponseJson(event.getId())).get(2).asBoolean(); - - var actualArray = mapper().readTree(eventResponses.getFirst()).get(0).asText(); - var actualSubscriptionId = - mapper().readTree(eventResponses.getFirst()).get(1).asText(); - var actualSuccess = mapper().readTree(eventResponses.getFirst()).get(2).asBoolean(); - - assertEquals(expectedArray, actualArray, "First element should match"); - assertEquals(expectedSubscriptionId, actualSubscriptionId, "Subscription ID should match"); - assertEquals(expectedSuccess, actualSuccess, "Success flag should match"); - } - - // TODO - Investigate why EOSE, instead of EVENT, is returned from nostr-rs-relay, and not - // superconductor - - try (SpringWebSocketClient springWebSocketRequestClient = - new SpringWebSocketClient(new StandardWebSocketClient(getRelayUri()), getRelayUri())) { - String reqJson = createReqJson(UUID.randomUUID().toString(), eventId); - List reqResponses = springWebSocketRequestClient.send(reqJson).stream().toList(); - - // Some relays may emit EOSE or NOTICE before EVENT; find the EVENT response deterministically - JsonNode eventArray = - reqResponses.stream() - .map( - json -> { - try { - return mapper().readTree(json); - } catch (Exception e) { - throw new RuntimeException(e); - } - }) - .filter(node -> node.isArray() && node.size() >= 3) - .filter(node -> "EVENT".equals(node.get(0).asText())) - .findFirst() - .orElseThrow( - () -> - new AssertionError( - "No EVENT response found. Got: " + String.join(" | ", reqResponses))); - - var expectedJson = mapper().readTree(expectedRequestResponseJson()); - - // Verify only required fields - assertEquals(3, eventArray.size(), "Expected 3 elements in the array, but got " + eventArray.size()); - assertEquals( - eventArray.get(2).get("id").asText(), - expectedJson.get(2).get("id").asText(), - "ID should match"); - assertEquals( - eventArray.get(2).get("kind").asInt(), - expectedJson.get(2).get("kind").asInt(), - "Kind should match"); - - // Verify required tags - var actualTags = eventArray.get(2).get("tags"); - assertTrue( - hasRequiredTag(actualTags, "price", NUMBER.toString()), "Price tag should be present"); - assertTrue(hasRequiredTag(actualTags, "title", TITLE), "Title tag should be present"); - assertTrue(hasRequiredTag(actualTags, "summary", SUMMARY), "Summary tag should be present"); - } - // */ - } - - private String expectedEventResponseJson(String subscriptionId) { - return "[\"OK\",\"" + subscriptionId + "\",true,\"success: request processed\"]"; - } - - private String createReqJson(String subscriberId, String id) { - return "[\"REQ\",\"" + subscriberId + "\",{\"ids\":[\"" + id + "\"]}]"; - } - - private String expectedRequestResponseJson() { - return " [\"EVENT\",\"ApiNIP99RequestTest-subscriber_001" - + "\",\n" - + " {\"id\": \"" - + eventId - + "\",\n" - + " \"kind\": " - + KIND - + ",\n" - + " \"content\": \"" - + CLASSIFIED_CONTENT - + "\",\n" - + " \"pubkey\": \"" - + eventPubKey - + "\",\n" - + " \"created_at\": " - + eventCreatedAt - + ",\n" - + " \"tags\": [\n" - + " [ \"price\", \"" - + NUMBER - + "\", \"" - + CURRENCY - + "\", \"" - + FREQUENCY - + "\" ],\n" - + " [ \"title\", \"" - + TITLE - + "\" ],\n" - + " [ \"summary\", \"" - + SUMMARY - + "\" ]\n" - + " ],\n" - + " \"sig\": \"" - + signature - + "\"\n" - + " }]"; - } - - private boolean hasRequiredTag(JsonNode tags, String tagName, String expectedValue) { - for (JsonNode tag : tags) { - if (tag.get(0).asText().equals(tagName) && tag.get(1).asText().equals(expectedValue)) { - return true; - } - } - return false; - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/BaseRelayIntegrationTest.java b/nostr-java-api/src/test/java/nostr/api/integration/BaseRelayIntegrationTest.java deleted file mode 100644 index a1550380b..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/BaseRelayIntegrationTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package nostr.api.integration; - -import com.github.dockerjava.api.model.Ulimit; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.condition.DisabledIfSystemProperty; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.DockerClientFactory; -import org.testcontainers.containers.BindMode; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.time.Duration; -import java.util.Map; -import java.util.ResourceBundle; - -/** - * Base class for Testcontainers-backed relay integration tests. - * - *

Uses strfry relay by default. Configure via relay-container.properties. - * - *

Disabled automatically when the system property `noDocker=true` is set (e.g. CI without Docker). - */ -@DisabledIfSystemProperty(named = "noDocker", matches = "true") -@Testcontainers -public abstract class BaseRelayIntegrationTest { - - private static final String RESOURCE_BUNDLE = "relay-container"; - private static final String IMAGE_KEY = "relay.container.image"; - private static final String PORT_KEY = "relay.container.port"; - private static final int DEFAULT_PORT = 7777; - - private static final int relayPort; - - @Container private static final GenericContainer RELAY; - - static { - ResourceBundle bundle = ResourceBundle.getBundle(RESOURCE_BUNDLE); - String image = bundle.getString(IMAGE_KEY); - relayPort = bundle.containsKey(PORT_KEY) - ? Integer.parseInt(bundle.getString(PORT_KEY)) - : DEFAULT_PORT; - - RELAY = - new GenericContainer<>(image) - .withExposedPorts(relayPort) - .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig() - .withUlimits(new Ulimit[] {new Ulimit("nofile", 1000000L, 1000000L)})) - .withClasspathResourceMapping( - "strfry.conf", "/etc/strfry.conf", BindMode.READ_ONLY) - .withTmpFs(Map.of("/app/strfry-db", "rw")) - .waitingFor( - Wait.forLogMessage(".*Started websocket server on.*", 1) - .withStartupTimeout(Duration.ofSeconds(30))); - } - - private static String relayUri; - - @BeforeAll - static void ensureDockerAvailable() { - Assumptions.assumeTrue( - DockerClientFactory.instance().isDockerAvailable(), - "Docker is required to run relay container"); - String host = RELAY.getHost(); - relayUri = String.format("ws://%s:%d", host, RELAY.getMappedPort(relayPort)); - } - - @DynamicPropertySource - static void registerRelayProperties(DynamicPropertyRegistry registry) { - String host = RELAY.getHost(); - relayUri = String.format("ws://%s:%d", host, RELAY.getMappedPort(relayPort)); - registry.add("relays.nostr_rs_relay", () -> relayUri); - } - - static String getRelayUri() { - return relayUri; - } - - /** - * Returns a relay map containing the Testcontainers relay URI. - * Use this instead of autowired relays to ensure tests use the dynamic container port. - */ - static Map getTestRelays() { - return Map.of("nostr_rs_relay", relayUri); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/MultiRelayIT.java b/nostr-java-api/src/test/java/nostr/api/integration/MultiRelayIT.java deleted file mode 100644 index 4f398b5cd..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/MultiRelayIT.java +++ /dev/null @@ -1,155 +0,0 @@ -package nostr.api.integration; - -import nostr.api.NostrSpringWebSocketClient; -import nostr.api.integration.support.FakeWebSocketClient; -import nostr.api.integration.support.FakeWebSocketClientFactory; -import nostr.api.service.impl.DefaultNoteService; -import nostr.base.Kind; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Integration tests covering multi-relay behavior using a fake WebSocket client factory. - */ -public class MultiRelayIT { - - /** - * Verifies that sending an event broadcasts to all configured relays and returns responses from - * each relay. - */ - @Test - void testBroadcastToMultipleRelays() { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - NostrSpringWebSocketClient client = - new NostrSpringWebSocketClient(sender, new DefaultNoteService(), factory); - - Map relays = - Map.of( - "relay1", "wss://relay1.example.com", - "relay2", "wss://relay2.example.com", - "relay3", "wss://relay3.example.com"); - client.setRelays(relays); - - GenericEvent event = - GenericEvent.builder() - .pubKey(sender.getPublicKey()) - .kind(Kind.TEXT_NOTE) - .content("hello nostr") - .build(); - event.update(); - client.sign(sender, event); - - List responses = client.sendEvent(event); - assertEquals(3, responses.size(), "Should receive one response per relay"); - assertTrue(responses.contains("OK:wss://relay1.example.com")); - assertTrue(responses.contains("OK:wss://relay2.example.com")); - assertTrue(responses.contains("OK:wss://relay3.example.com")); - - // Also check each fake recorded the payload - for (String uri : relays.values()) { - FakeWebSocketClient fake = factory.get(uri); - assertTrue( - fake.getSentPayloads().stream().anyMatch(p -> p.contains("EVENT")), - "Relay should have been sent an EVENT message: " + uri); - } - } - - /** - * Ensures that if one relay fails to send, other relay responses are still returned and - * the failure is recorded for diagnostics. - */ - @Test - void testRelayFailoverReturnsAvailableResponses() { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - DefaultNoteService noteService = new DefaultNoteService(); - NostrSpringWebSocketClient client = - new NostrSpringWebSocketClient(sender, noteService, factory); - - Map relays = - Map.of( - "relayA", "wss://relayA.example.com", - "relayB", "wss://relayB.example.com"); - client.setRelays(relays); - - GenericEvent event = - GenericEvent.builder() - .pubKey(sender.getPublicKey()) - .kind(Kind.TEXT_NOTE) - .content("broadcast with partial availability") - .build(); - event.update(); - client.sign(sender, event); - - // Simulate relayB failure - FakeWebSocketClient relayB = factory.get("wss://relayB.example.com"); - try { relayB.close(); } catch (Exception ignored) {} - - List responses = client.sendEvent(event); - assertEquals(1, responses.size()); - assertTrue(responses.contains("OK:wss://relayA.example.com")); - - Map failures = noteService.getLastFailures(); - assertTrue(failures.containsKey("relayB")); - - // Also visible via client accessors - Map clientFailures = client.getLastSendFailures(); - assertTrue(clientFailures.containsKey("relayB")); - - // Structured details available as well - var details = client.getLastSendFailureDetails(); - assertTrue(details.containsKey("relayB")); - } - - /** - * Verifies that a REQ is sent per relay and contains the subscription id. - */ - @Test - void testCrossRelayEventRetrievalViaReq() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - NostrSpringWebSocketClient client = - new NostrSpringWebSocketClient(sender, new DefaultNoteService(), factory); - - Map relays = - Map.of( - "relay1", "wss://relay1.example.com", - "relay2", "wss://relay2.example.com"); - client.setRelays(relays); - - // Open a subscription (so request clients exist) and then send a REQ - var received = new CopyOnWriteArrayList(); - var handle = - client.subscribe( - new nostr.event.filter.Filters(new nostr.event.filter.KindFilter<>(Kind.TEXT_NOTE)), - "sub-123", - received::add); - try { - List reqResponses = - client.sendRequest( - new nostr.event.filter.Filters( - new nostr.event.filter.KindFilter<>(Kind.TEXT_NOTE)), - "sub-123"); - assertEquals(2, reqResponses.size()); - - // Check REQ payloads captured by fakes - for (String uri : relays.values()) { - FakeWebSocketClient fake = factory.get(uri); - assertTrue( - fake.getSentPayloads().stream().anyMatch(p -> p.contains("\"REQ\",\"sub-123\"")), - "Relay should have been sent a REQ for sub-123: " + uri); - } - } finally { - handle.close(); - } - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/NostrSpringWebSocketClientSubscriptionIT.java b/nostr-java-api/src/test/java/nostr/api/integration/NostrSpringWebSocketClientSubscriptionIT.java deleted file mode 100644 index 3169fe661..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/NostrSpringWebSocketClientSubscriptionIT.java +++ /dev/null @@ -1,161 +0,0 @@ -package nostr.api.integration; - -import lombok.NonNull; -import nostr.api.NostrSpringWebSocketClient; -import nostr.api.TestableWebSocketClientHandler; -import nostr.api.WebSocketClientHandler; -import nostr.base.Kind; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.WebSocketClientIF; -import nostr.event.BaseMessage; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class NostrSpringWebSocketClientSubscriptionIT { - - // Ensures that long-lived subscriptions stream events and send CLOSE frames on cancellation. - @Test - void subscriptionStreamsAndClosesCleanly() throws Exception { - RecordingNostrClient client = new RecordingNostrClient(); - client.setRelays(Map.of("relay-a", "ws://relay")); - - Filters filters = new Filters(new KindFilter<>(Kind.TEXT_NOTE)); - List received = new ArrayList<>(); - List errors = new ArrayList<>(); - - AutoCloseable handle = - client.subscribe(filters, "sub-123", received::add, errors::add); - - RecordingHandler handler = client.getHandler("relay-a"); - StubWebSocketClient stub = handler.getSubscriptionClient("sub-123"); - assertFalse(stub.isClosed()); - assertTrue(stub.getSentMessages().getFirst().contains("REQ")); - - stub.emit("event-1"); - Thread.sleep(10L); - stub.emit("event-2"); - - assertEquals(List.of("event-1", "event-2"), received); - assertTrue(errors.isEmpty()); - - handle.close(); - assertTrue(stub.isClosed()); - assertTrue(stub.getSentMessages().stream().anyMatch(payload -> payload.contains("CLOSE"))); - - stub.emit("event-3"); - assertEquals(2, received.size()); - } - - private static final class RecordingNostrClient extends NostrSpringWebSocketClient { - private final Map handlers = new ConcurrentHashMap<>(); - - @Override - protected WebSocketClientHandler newWebSocketClientHandler(String relayName, nostr.base.RelayUri relayUri) { - RecordingHandler handler = new RecordingHandler(relayName, relayUri.toString()); - handlers.put(relayName, handler); - return handler; - } - - RecordingHandler getHandler(String relayName) { - return handlers.get(relayName); - } - } - - private static final class RecordingHandler extends TestableWebSocketClientHandler { - private final Map subscriptionClients; - - RecordingHandler(String relayName, String relayUri) { - this(relayName, relayUri, new ConcurrentHashMap<>()); - } - - private RecordingHandler( - String relayName, String relayUri, Map subscriptionClients) { - super( - relayName, - relayUri, - new SpringWebSocketClient(new StubWebSocketClient(), relayUri), - key -> { - StubWebSocketClient stub = new StubWebSocketClient(); - subscriptionClients.put(key, stub); - return new SpringWebSocketClient(stub, relayUri); - }); - this.subscriptionClients = subscriptionClients; - } - - StubWebSocketClient getSubscriptionClient(String subscriptionId) { - return subscriptionClients.get(subscriptionId); - } - } - - private static final class StubWebSocketClient implements WebSocketClientIF { - private final List sentMessages = new CopyOnWriteArrayList<>(); - private final Map> messageListeners = new ConcurrentHashMap<>(); - private final Map> errorListeners = new ConcurrentHashMap<>(); - private final Map closeListeners = new ConcurrentHashMap<>(); - private final AtomicBoolean closed = new AtomicBoolean(false); - - @Override - public List send(@NonNull T eventMessage) throws IOException { - return send(eventMessage.encode()); - } - - @Override - public List send(String json) { - sentMessages.add(json); - return List.of(); - } - - @Override - public AutoCloseable subscribe( - String requestJson, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) - throws IOException { - String id = UUID.randomUUID().toString(); - sentMessages.add(requestJson); - messageListeners.put(id, messageListener); - errorListeners.put(id, errorListener); - if (closeListener != null) { - closeListeners.put(id, closeListener); - } - return () -> { - messageListeners.remove(id); - errorListeners.remove(id); - closeListeners.remove(id); - }; - } - - @Override - public void close() { - closed.set(true); - } - - void emit(String payload) { - messageListeners.values().forEach(listener -> listener.accept(payload)); - } - - boolean isClosed() { - return closed.get(); - } - - List getSentMessages() { - return sentMessages; - } - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/SubscriptionLifecycleIT.java b/nostr-java-api/src/test/java/nostr/api/integration/SubscriptionLifecycleIT.java deleted file mode 100644 index c97dfb63e..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/SubscriptionLifecycleIT.java +++ /dev/null @@ -1,192 +0,0 @@ -package nostr.api.integration; - -import nostr.api.NostrSpringWebSocketClient; -import nostr.api.integration.support.FakeWebSocketClient; -import nostr.api.integration.support.FakeWebSocketClientFactory; -import nostr.api.service.impl.DefaultNoteService; -import nostr.base.Kind; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Integration tests for subscription lifecycle using a fake WebSocket client. - */ -public class SubscriptionLifecycleIT { - - /** - * Validates that subscription listeners receive messages emitted by all relays. - */ - @Test - void testSubscriptionReceivesNewEvents() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - NostrSpringWebSocketClient client = - new NostrSpringWebSocketClient(sender, new DefaultNoteService(), factory); - - Map relays = - Map.of( - "relay1", "wss://relay1.example.com", - "relay2", "wss://relay2.example.com"); - client.setRelays(relays); - - List received = new CopyOnWriteArrayList<>(); - AutoCloseable handle = - client.subscribe(new Filters(new KindFilter<>(Kind.TEXT_NOTE)), "sub-evt", received::add); - try { - // Simulate inbound events from both relays - factory.get("wss://relay1.example.com").emit("EVENT from relay1"); - factory.get("wss://relay2.example.com").emit("EVENT from relay2"); - - // Both messages should be received - assertTrue(received.stream().anyMatch(s -> s.contains("relay1"))); - assertTrue(received.stream().anyMatch(s -> s.contains("relay2"))); - } finally { - handle.close(); - } - } - - /** - * Validates concurrent subscriptions receive their respective messages without interference. - */ - @Test - void testConcurrentSubscriptions() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - NostrSpringWebSocketClient client = - new NostrSpringWebSocketClient(sender, new DefaultNoteService(), factory); - - Map relays = - Map.of( - "relay1", "wss://relay1.example.com", - "relay2", "wss://relay2.example.com"); - client.setRelays(relays); - - List s1 = new CopyOnWriteArrayList<>(); - List s2 = new CopyOnWriteArrayList<>(); - - AutoCloseable h1 = client.subscribe(new Filters(new KindFilter<>(Kind.TEXT_NOTE)), "sub-A", s1::add); - AutoCloseable h2 = client.subscribe(new Filters(new KindFilter<>(Kind.TEXT_NOTE)), "sub-B", s2::add); - try { - factory.get("wss://relay1.example.com").emit("[\"EVENT\",\"sub-A\",{}]"); - factory.get("wss://relay2.example.com").emit("[\"EVENT\",\"sub-B\",{}]"); - - assertTrue(s1.stream().anyMatch(m -> m.contains("sub-A"))); - assertTrue(s2.stream().anyMatch(m -> m.contains("sub-B"))); - } finally { - h1.close(); - h2.close(); - } - } - - /** - * Errors emitted by the underlying client should propagate to the provided error listener. - */ - @Test - void testErrorPropagationToListener() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - NostrSpringWebSocketClient client = - new NostrSpringWebSocketClient(sender, new DefaultNoteService(), factory); - - Map relays = Map.of("relay", "wss://relay.example.com"); - client.setRelays(relays); - - List errors = new CopyOnWriteArrayList<>(); - AutoCloseable handle = - client.subscribe( - new Filters(new KindFilter<>(Kind.TEXT_NOTE)), - "sub-err", - m -> {}, - errors::add); - try { - factory.get("wss://relay.example.com").emitError(new RuntimeException("x")); - assertTrue(errors.stream().anyMatch(e -> "x".equals(e.getMessage()))); - } finally { - handle.close(); - } - } - - /** - * Subscribing without an explicit error listener should use a safe default and not throw when - * errors occur. - */ - @Test - void testSubscribeWithoutErrorListenerUsesSafeDefault() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - NostrSpringWebSocketClient client = - new NostrSpringWebSocketClient(sender, new DefaultNoteService(), factory); - - Map relays = Map.of("relay", "wss://relay.example.com"); - client.setRelays(relays); - - AutoCloseable handle = - client.subscribe(new Filters(new KindFilter<>(Kind.TEXT_NOTE)), "sub-safe", m -> {}); - try { - // Emit an error; should be handled by safe default error consumer, not rethrown - factory.get("wss://relay.example.com").emitError(new RuntimeException("err-safe")); - assertTrue(true); - } finally { - handle.close(); - } - } - - /** - * Confirms that EOSE markers propagate to listeners as regular messages. - */ - @Test - void testEOSEMarkerReceived() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - NostrSpringWebSocketClient client = - new NostrSpringWebSocketClient(sender, new DefaultNoteService(), factory); - - Map relays = Map.of("relay", "wss://relay.example.com"); - client.setRelays(relays); - - List received = new ArrayList<>(); - AutoCloseable handle = - client.subscribe(new Filters(new KindFilter<>(Kind.TEXT_NOTE)), "sub-eose", received::add); - try { - factory.get("wss://relay.example.com").emit("[\"EOSE\",\"sub-eose\"]"); - assertTrue(received.stream().anyMatch(s -> s.contains("EOSE"))); - } finally { - handle.close(); - } - } - - /** - * Ensures cancellation closes underlying subscription and sends CLOSE frame. - */ - @Test - void testCancelSubscriptionSendsClose() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - FakeWebSocketClientFactory factory = new FakeWebSocketClientFactory(); - NostrSpringWebSocketClient client = - new NostrSpringWebSocketClient(sender, new DefaultNoteService(), factory); - - Map relays = Map.of("relay", "wss://relay.example.com"); - client.setRelays(relays); - - AutoCloseable handle = - client.subscribe(new Filters(new KindFilter<>(Kind.TEXT_NOTE)), "sub-close", s -> {}); - FakeWebSocketClient fake = factory.get("wss://relay.example.com"); - try { - handle.close(); - } finally { - // Verify a CLOSE message was sent (subscribe called with CLOSE frame) - assertTrue( - fake.getSentPayloads().stream().anyMatch(p -> p.contains("\"CLOSE\",\"sub-close\"")), - "Close frame should be sent for subscription id"); - } - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/ZDoLastApiNIP09EventIT.java b/nostr-java-api/src/test/java/nostr/api/integration/ZDoLastApiNIP09EventIT.java deleted file mode 100644 index f8821703b..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/ZDoLastApiNIP09EventIT.java +++ /dev/null @@ -1,149 +0,0 @@ -package nostr.api.integration; - -import nostr.api.NIP01; -import nostr.api.NIP09; -import nostr.base.Kind; -import nostr.base.Relay; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.StandardWebSocketClient; -import nostr.config.RelayConfig; -import nostr.event.BaseTag; -import nostr.event.filter.AuthorFilter; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.message.EventMessage; -import nostr.event.message.OkMessage; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.IdentifierTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -@SpringJUnitConfig(RelayConfig.class) -@ActiveProfiles("test") -public class ZDoLastApiNIP09EventIT extends BaseRelayIntegrationTest { - - @Test - public void deleteEvent() throws Exception { - - Identity identity = Identity.generateRandomIdentity(); - - NIP09 nip09 = new NIP09(identity); - NIP01 nip01 = new NIP01(identity); - - try (SpringWebSocketClient springWebSocketClient = - new SpringWebSocketClient(new StandardWebSocketClient(getRelayUri()), getRelayUri())) { - GenericEvent event = nip01.createTextNoteEvent("Delete me!").sign().getEvent(); - EventMessage message = new EventMessage(event); - springWebSocketClient.send(message); - - Filters filters = - new Filters( - new KindFilter<>(Kind.TEXT_NOTE), new AuthorFilter<>(identity.getPublicKey())); - - List result = - NIP01.sendRequest(springWebSocketClient, filters, UUID.randomUUID().toString()); - - assertFalse(result.isEmpty()); - assertEquals(2, result.size()); - - var nip09Event = nip09.createDeletionEvent(nip01.getEvent()).sign().getEvent(); - EventMessage nip09Message = new EventMessage(nip09Event); - springWebSocketClient.send(nip09Message); - - result = NIP01.sendRequest(springWebSocketClient, filters, UUID.randomUUID().toString()); - - assertFalse(result.isEmpty()); - assertEquals(1, result.size()); - } - - nip01.close(); - nip09.close(); - } - - @Test - public void deleteEventWithRef() throws Exception { - final String RELAY_URI = getRelayUri(); - Identity identity = Identity.generateRandomIdentity(); - - NIP01 nip011 = new NIP01(identity); - GenericEvent replaceableEvent = - nip011.createReplaceableEvent(10_001, "replaceable event").sign().getEvent(); - EventMessage replaceableEventMessage = new EventMessage(replaceableEvent); - - try (SpringWebSocketClient springWebSocketClient = - new SpringWebSocketClient(new StandardWebSocketClient(getRelayUri()), getRelayUri())) { - List jsonReplaceableMessageList = springWebSocketClient.send(replaceableEventMessage); - - BaseMessageDecoder decoder = new BaseMessageDecoder<>(); - OkMessage okMessage = decoder.decode(jsonReplaceableMessageList.get(0)); - - assertNotNull(jsonReplaceableMessageList); - assertInstanceOf(OkMessage.class, okMessage); - - IdentifierTag identifierTag = new IdentifierTag(replaceableEvent.getId()); - - NIP01 nip01 = new NIP01(identity); - nip01 - .createTextNoteEvent("Reference me!") - .getEvent() - .addTag( - NIP01.createAddressTag( - 10_001, identity.getPublicKey(), identifierTag, new Relay(RELAY_URI))); - - GenericEvent nip01Event = nip01.sign().getEvent(); - EventMessage eventMessage = new EventMessage(nip01Event); - List jsonMessageList = springWebSocketClient.send(eventMessage); - - decoder = new BaseMessageDecoder<>(); - okMessage = decoder.decode(jsonReplaceableMessageList.get(0)); - - assertNotNull(jsonMessageList); - assertInstanceOf(OkMessage.class, okMessage); - - NIP09 nip09 = new NIP09(identity); - GenericEvent deletedEvent = nip09.createDeletionEvent(nip01Event).getEvent(); - - assertEquals(4, deletedEvent.getTags().size()); - - List eventTags = - deletedEvent.getTags().stream().filter(t -> "e".equals(t.getCode())).toList(); - - assertEquals(1, eventTags.size()); - - EventTag eventTag = (EventTag) eventTags.get(0); - assertEquals(nip01Event.getId(), eventTag.getIdEvent()); - - List addressTags = - deletedEvent.getTags().stream().filter(t -> "a".equals(t.getCode())).toList(); - - assertEquals(1, addressTags.size()); - - AddressTag addressTag = (AddressTag) addressTags.get(0); - assertEquals(10_001, addressTag.getKind()); - assertEquals(replaceableEvent.getId(), addressTag.getIdentifierTag().getUuid()); - assertEquals(identity.getPublicKey(), addressTag.getPublicKey()); - - List kindTags = - deletedEvent.getTags().stream().filter(t -> "k".equals(t.getCode())).toList(); - - assertEquals(2, kindTags.size()); - - nip01.close(); - nip011.close(); - nip09.close(); - } - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/support/FakeWebSocketClient.java b/nostr-java-api/src/test/java/nostr/api/integration/support/FakeWebSocketClient.java deleted file mode 100644 index fbc6af6d9..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/support/FakeWebSocketClient.java +++ /dev/null @@ -1,137 +0,0 @@ -package nostr.api.integration.support; - -import lombok.Getter; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import nostr.client.springwebsocket.WebSocketClientIF; -import nostr.event.BaseMessage; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Consumer; - -/** - * Minimal in‑memory WebSocket client used by integration tests to simulate relay behavior. - * - *

Records sent payloads and allows tests to emit inbound messages or errors to subscribed - * listeners. Intended for deterministic, fast, and offline test scenarios. - */ -@Slf4j -public class FakeWebSocketClient implements WebSocketClientIF { - - /** The relay URL this fake is bound to (for assertions/identification). */ - @Getter private final String relayUrl; - - private volatile boolean open = true; - - private final List sentPayloads = Collections.synchronizedList(new ArrayList<>()); - private final ConcurrentMap listeners = new ConcurrentHashMap<>(); - - /** - * Creates a fake client for the given relay URL. - * - * @param relayUrl relay endpoint identifier - */ - public FakeWebSocketClient(@NonNull String relayUrl) { - this.relayUrl = relayUrl; - } - - /** - * Encodes and forwards a message for {@link #send(String)}. - */ - @Override - public List send(T eventMessage) throws IOException { - return send(eventMessage.encode()); - } - - /** - * Appends the raw JSON to the internal log and returns an OK stub response. - */ - @Override - public List send(String json) throws IOException { - if (!open) { - throw new IOException("WebSocket session is closed for " + relayUrl); - } - sentPayloads.add(json); - // Return a simple response containing the relay URL for identification - return List.of("OK:" + relayUrl); - } - - /** - * Registers a listener and records the subscription REQ payload. - */ - @Override - public AutoCloseable subscribe( - String requestJson, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) - throws IOException { - Objects.requireNonNull(messageListener, "messageListener"); - Objects.requireNonNull(errorListener, "errorListener"); - sentPayloads.add(requestJson); - if (!open) { - log.debug("Subscription on closed WebSocket for {}, returning no-op handle", relayUrl); - return () -> {}; // No-op handle since client is already closed - } - String id = UUID.randomUUID().toString(); - listeners.put(id, new Listener(messageListener, errorListener, closeListener)); - return () -> listeners.remove(id); - } - - /** - * Closes the fake session and notifies close listeners once. - */ - @Override - public void close() throws IOException { - if (!open) return; - open = false; - // Notify close listeners once - for (Listener listener : listeners.values()) { - try { - if (listener.closeListener != null) listener.closeListener.run(); - } catch (Exception e) { - log.warn("Close listener threw on {}", relayUrl, e); - } - } - listeners.clear(); - } - - /** - * Returns a snapshot of all sent payloads. - */ - public List getSentPayloads() { - return List.copyOf(sentPayloads); - } - - /** - * Emits an inbound message to all registered listeners. - */ - public void emit(String payload) { - for (Listener listener : listeners.values()) { - try { - listener.messageListener.accept(payload); - } catch (Exception e) { - if (listener.errorListener != null) listener.errorListener.accept(e); - } - } - } - - /** - * Emits an inbound error to all registered error listeners. - */ - public void emitError(Throwable t) { - for (Listener listener : listeners.values()) { - if (listener.errorListener != null) listener.errorListener.accept(t); - } - } - - private record Listener( - Consumer messageListener, Consumer errorListener, Runnable closeListener) {} -} diff --git a/nostr-java-api/src/test/java/nostr/api/integration/support/FakeWebSocketClientFactory.java b/nostr-java-api/src/test/java/nostr/api/integration/support/FakeWebSocketClientFactory.java deleted file mode 100644 index 9de7c2c66..000000000 --- a/nostr-java-api/src/test/java/nostr/api/integration/support/FakeWebSocketClientFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -package nostr.api.integration.support; - -import lombok.NonNull; -import nostr.base.RelayUri; -import nostr.client.WebSocketClientFactory; -import nostr.client.springwebsocket.WebSocketClientIF; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; - -/** - * In-memory {@link WebSocketClientFactory} for tests. - * - *

Produces {@link FakeWebSocketClient} instances keyed by relay URI and caches them so tests - * can both inject behavior and later inspect what messages were sent. - */ -public class FakeWebSocketClientFactory implements WebSocketClientFactory { - - private final Map clients = new ConcurrentHashMap<>(); - - /** - * Returns a cached fake client for the given relay or creates a new one. - * - * @param relayUri target relay URI - * @return a {@link WebSocketClientIF} backed by {@link FakeWebSocketClient} - */ - @Override - public WebSocketClientIF create(@NonNull RelayUri relayUri) - throws ExecutionException, InterruptedException { - return clients.computeIfAbsent(relayUri.toString(), FakeWebSocketClient::new); - } - - /** - * Retrieves a previously created fake client by its relay URI. - * - * @param relayUri string form of the relay URI - * @return the fake client or {@code null} if none was created yet - */ - public FakeWebSocketClient get(String relayUri) { - return clients.get(relayUri); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/Bolt11UtilTest.java b/nostr-java-api/src/test/java/nostr/api/unit/Bolt11UtilTest.java deleted file mode 100644 index 11afc298d..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/Bolt11UtilTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package nostr.api.unit; - -import nostr.api.nip57.Bolt11Util; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * Unit tests for Bolt11Util amount parsing. - */ -public class Bolt11UtilTest { - - @Test - // Parses nanoBTC amount (n) into msat. Example: 50n BTC → 5000 msat. - void parseNanoBtcToMsat() { - // 50n BTC = 50 * 10^-9 BTC → 50 * 10^2 sat → 5000 msat - long msat = Bolt11Util.parseMsat("lnbc50n1pxyz"); - assertEquals(5_000L, msat); - } - - @Test - // Parses picoBTC amount (p) into msat. Example: 2000p BTC → 200 msat. - void parsePicoBtcToMsat() { - // 2000p BTC = 2000 * 10^-12 BTC → 0.2 sat → 200 msat - long msat = Bolt11Util.parseMsat("lnbc2000p1pabc"); - assertEquals(200L, msat); - } - - @Test - // Invoice without amount returns -1 to indicate any-amount invoice. - void parseNoAmountInvoice() { - long msat = Bolt11Util.parseMsat("lnbc1pnoamount"); - assertEquals(-1L, msat); - } - - @Test - // Invalid HRP throws IllegalArgumentException. - void invalidInvoiceThrows() { - assertThrows(IllegalArgumentException.class, () -> Bolt11Util.parseMsat("notbolt11")); - } - - @Test - // Parses milliBTC (m) unit into msat. Example: 2m BTC → 200,000,000 msat. - void parseMilliBtcToMsat() { - long msat = Bolt11Util.parseMsat("lnbc2m1ptest"); - assertEquals(200_000_000L, msat); - } - - @Test - // Parses microBTC (u) unit into msat. Example: 25u BTC → 2,500,000 msat. - void parseMicroBtcToMsat() { - long msat = Bolt11Util.parseMsat("lntb25u1ptest"); - assertEquals(2_500_000L, msat); - } - - @Test - // Parses BTC with no unit. Example: 1 BTC → 100,000,000,000 msat. - void parseWholeBtcNoUnit() { - long msat = Bolt11Util.parseMsat("lnbc11some"); - assertEquals(100_000_000_000L, msat); - } - - @Test - // Accepts uppercase invoice strings by normalizing to lowercase. - void parseUppercaseInvoice() { - long msat = Bolt11Util.parseMsat("LNBC50N1PUPPER"); - assertEquals(5_000L, msat); - } - - @Test - // Supports testnet network code (lntb...). - void parseTestnetNano() { - long msat = Bolt11Util.parseMsat("lntb50n1pxyz"); - assertEquals(5_000L, msat); - } - - @Test - // Supports regtest network code (lnbcrt...). - void parseRegtestNano() { - long msat = Bolt11Util.parseMsat("lnbcrt50n1pxyz"); - assertEquals(5_000L, msat); - } - - @Test - // Excessively large amounts should throw due to overflow protection. - void parseTooLargeThrows() { - // This crafts a huge value: 9999999999999999999m BTC -> will exceed Long.MAX_VALUE in msat - String huge = "lnbc9999999999999999999m1pbig"; - assertThrows(IllegalArgumentException.class, () -> Bolt11Util.parseMsat(huge)); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/CalendarTimeBasedEventTest.java b/nostr-java-api/src/test/java/nostr/api/unit/CalendarTimeBasedEventTest.java deleted file mode 100644 index 1b35fe255..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/CalendarTimeBasedEventTest.java +++ /dev/null @@ -1,195 +0,0 @@ -package nostr.api.unit; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import nostr.api.NIP52; -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import nostr.event.entities.CalendarContent; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseEventEncoder; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.SubjectTag; -import nostr.id.Identity; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.function.BiFunction; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class CalendarTimeBasedEventTest { - // required fields - public static final Identity identity = Identity.generateRandomIdentity(); - public static final PublicKey senderPubkey = new PublicKey(identity.getPublicKey().toString()); - - public static final String CALENDAR_TIME_BASED_EVENT_TITLE = "Calendar Time-Based Event title"; - public static final String CALENDAR_TIME_BASED_EVENT_CONTENT = - "calendar Time-Based Event content"; - public static final IdentifierTag identifierTag = - new IdentifierTag("UUID-CalendarTimeBasedEventTest"); - public static final Long START = 1716513986268L; - - // optional fields - public static final String str = "http://some.url"; - public static final String PTAG_1_HEX = - "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76985"; - public static final PubKeyTag P_1_TAG = new PubKeyTag(new PublicKey(PTAG_1_HEX), str, "ISSUER"); - public static final String PTAG_2_HEX = - "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4347"; - public static final PubKeyTag P_2_TAG = - new PubKeyTag(new PublicKey(PTAG_2_HEX), str, "COUNTERPARTY"); - - public static final String SUBJECT = "Calendar Time-Based Event Test Subject Tag"; - public static final SubjectTag SUBJECT_TAG = new SubjectTag(SUBJECT); - public static final GeohashTag G_TAG = - new GeohashTag("Calendar Time-Based Event Test Geohash Tag"); - public static final HashtagTag T_TAG = - new HashtagTag("Calendar Time-Based Event Test Hashtag Tag"); - - public static final String CALENDAR_TIME_BASED_EVENT_SUMMARY = - "Calendar Time-Based Event summary"; - public static final String CALENDAR_TIME_BASED_EVENT_START_TZID = "1687765220"; - public static final String CALENDAR_TIME_BASED_EVENT_END_TZID = "1687765220"; - public static final String CALENDAR_TIME_BASED_EVENT_LOCATION = - "Calendar Time-Based Event location"; - - // keys - public static final String START_TZID_CODE = "start_tzid"; - public static final String END_CODE = "end"; - public static final String LOCATION_CODE = "location"; - - private GenericEvent instance; - String expectedEncodedJson; - Signature signature; - - @BeforeAll - void setup() throws URISyntaxException { - // a random set of base tags - List tags = new ArrayList<>(); - tags.add(P_1_TAG); - tags.add(P_2_TAG); - tags.add(BaseTag.create(LOCATION_CODE, CALENDAR_TIME_BASED_EVENT_LOCATION)); - tags.add(SUBJECT_TAG); - tags.add(G_TAG); - tags.add(T_TAG); - tags.add(BaseTag.create(START_TZID_CODE, CALENDAR_TIME_BASED_EVENT_START_TZID)); - Long l = START + 100L; - tags.add(BaseTag.create(END_CODE, l.toString())); - - CalendarContent calendarContent = - new CalendarContent<>(identifierTag, CALENDAR_TIME_BASED_EVENT_TITLE, START); - // a random set of calendar tags - // calendarContent.setEndTzid(CALENDAR_TIME_BASED_EVENT_END_TZID); - calendarContent.setSummary(CALENDAR_TIME_BASED_EVENT_SUMMARY); - URI uri = new URI(str); - // calendarContent.setReferenceTags(List.of(new ReferenceTag(uri))); - - instance = - new NIP52(identity) - .createCalendarTimeBasedEvent(tags, CALENDAR_TIME_BASED_EVENT_CONTENT, calendarContent) - .getEvent(); - signature = identity.sign(instance); - instance.setSignature(signature); - - expectedEncodedJson = - "{" - + "\"id\":\"" - + instance.getId() - + "\"," - + "\"kind\":31923," - + "\"content\":\"calendar Time-Based Event content\"," - + "\"pubkey\":\"" - + senderPubkey - + "\"," - + "\"created_at\":" - + instance.getCreatedAt() - + ",\"tags\":[" - + "[\"p\",\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76985\",\"http://some.url\",\"ISSUER\"]," - + "[\"p\",\"494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4347\",\"http://some.url\",\"COUNTERPARTY\"],[\"location\",\"Calendar" - + " Time-Based Event location\"],[\"subject\",\"Calendar Time-Based Event Test Subject" - + " Tag\"],[\"g\",\"Calendar Time-Based Event Test Geohash Tag\"],[\"t\",\"Calendar" - + " Time-Based Event Test Hashtag Tag\"],[\"start_tzid\",\"1687765220\"]," - + "[\"end\",\"1716513986368\"],[\"d\",\"UUID-CalendarTimeBasedEventTest\"],[\"title\",\"Calendar" - + " Time-Based Event" - + " title\"],[\"start\",\"1716513986268\"],[\"end_tzid\",\"1687765220\"],[\"summary\",\"Calendar" - + " Time-Based Event summary\"],[\"r\",\"http://some.url\"]],\"sig\":\"" - + signature.toString() - + "\"" - + "}"; - } - - @Test - void testCalendarTimeBasedEventEncoding() throws JsonProcessingException { - var instanceJson = mapper().readTree(new BaseEventEncoder<>(instance).encode()); - var expectedJson = mapper().readTree(expectedEncodedJson); - - // Helper function to find tag value - BiFunction findTagArray = - (tags, tagName) -> { - for (JsonNode tag : tags) { - if (tag.isArray() && tag.get(0).asText().equals(tagName)) { - return tag; - } - } - return null; - }; - - // Verify required fields match - assertEquals( - findTagArray.apply(instanceJson.get("tags"), "d").get(1).asText(), - findTagArray.apply(expectedJson.get("tags"), "d").get(1).asText()); - assertEquals( - findTagArray.apply(instanceJson.get("tags"), "title").get(1).asText(), - findTagArray.apply(expectedJson.get("tags"), "title").get(1).asText()); - assertEquals( - findTagArray.apply(instanceJson.get("tags"), "start").get(1).asText(), - findTagArray.apply(expectedJson.get("tags"), "start").get(1).asText()); - } - - @Test - void testCalendarTimeBasedEventDecoding() throws JsonProcessingException { - var decodedJson = - mapper().readTree( - new BaseEventEncoder<>( - mapper().readValue(expectedEncodedJson, GenericEvent.class)) - .encode()); - var instanceJson = mapper().readTree(new BaseEventEncoder<>(instance).encode()); - - // Helper function to find tag value - BiFunction findTagArray = - (tags, tagName) -> { - for (JsonNode tag : tags) { - if (tag.isArray() && tag.get(0).asText().equals(tagName)) { - return tag; - } - } - return null; - }; - - // Verify required fields match after decode/encode cycle - var decodedTags = decodedJson.get("tags"); - var instanceTags = instanceJson.get("tags"); - - assertEquals( - findTagArray.apply(decodedTags, "d").get(1).asText(), - findTagArray.apply(instanceTags, "d").get(1).asText()); - assertEquals( - findTagArray.apply(decodedTags, "title").get(1).asText(), - findTagArray.apply(instanceTags, "title").get(1).asText()); - assertEquals( - findTagArray.apply(decodedTags, "start").get(1).asText(), - findTagArray.apply(instanceTags, "start").get(1).asText()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/ConstantsTest.java b/nostr-java-api/src/test/java/nostr/api/unit/ConstantsTest.java deleted file mode 100644 index 7b65b2892..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/ConstantsTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package nostr.api.unit; - -import nostr.base.Kind; -import nostr.config.Constants; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseEventEncoder; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class ConstantsTest { - - @Test - void testKindValues() { - // Validate a few representative Kind enum values remain stable - assertEquals(0, Kind.SET_METADATA.getValue()); - assertEquals(1, Kind.TEXT_NOTE.getValue()); - assertEquals(42, Kind.CHANNEL_MESSAGE.getValue()); - } - - @Test - void testTagValues() { - assertEquals("e", Constants.Tag.EVENT_CODE); - assertEquals("p", Constants.Tag.PUBKEY_CODE); - } - - @Test - void testSerializationWithConstants() throws Exception { - Identity identity = Identity.generateRandomIdentity(); - GenericEvent event = new GenericEvent(); - event.setKind(Kind.TEXT_NOTE.getValue()); - event.setPubKey(identity.getPublicKey()); - event.setCreatedAt(0L); - event.setContent("test"); - - String json = new BaseEventEncoder<>(event).encode(); - assertEquals(Kind.TEXT_NOTE.getValue(), mapper().readTree(json).get("kind").asInt()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/JsonParseTest.java b/nostr-java-api/src/test/java/nostr/api/unit/JsonParseTest.java deleted file mode 100644 index 1d6de5dfc..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/JsonParseTest.java +++ /dev/null @@ -1,992 +0,0 @@ -package nostr.api.unit; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.extern.slf4j.Slf4j; -import nostr.api.NIP01; -import nostr.api.util.JsonComparator; -import nostr.base.Command; -import nostr.base.ElementAttribute; -import nostr.base.GenericTagQuery; -import nostr.base.Kind; -import nostr.base.Marker; -import nostr.base.PublicKey; -import nostr.crypto.bech32.Bech32; -import nostr.event.BaseEvent; -import nostr.event.BaseMessage; -import nostr.event.BaseTag; -import nostr.event.filter.AddressTagFilter; -import nostr.event.filter.AuthorFilter; -import nostr.event.filter.EventFilter; -import nostr.event.filter.Filterable; -import nostr.event.filter.Filters; -import nostr.event.filter.GenericTagQueryFilter; -import nostr.event.filter.GeohashTagFilter; -import nostr.event.filter.HashtagTagFilter; -import nostr.event.filter.IdentifierTagFilter; -import nostr.event.filter.KindFilter; -import nostr.event.filter.ReferencedEventFilter; -import nostr.event.filter.ReferencedPublicKeyFilter; -import nostr.event.filter.VoteTagFilter; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseEventEncoder; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.json.codec.BaseTagDecoder; -import nostr.event.json.codec.GenericEventDecoder; -import nostr.event.json.codec.GenericTagDecoder; -import nostr.event.message.EventMessage; -import nostr.event.message.ReqMessage; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PriceTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.VoteTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.util.List; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * @author eric - */ -@Slf4j -public class JsonParseTest { - @Test - public void testBaseMessageDecoderEventFilter() throws JsonProcessingException { - - String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - final String parseTarget = - "[\"REQ\", " - + "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " - + "{\"kinds\": [1], " - + "\"ids\": [\"" - + eventId - + "\"]," - + "\"#p\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; - - final var message = new BaseMessageDecoder<>().decode(parseTarget); - - assertEquals(Command.REQ.toString(), message.getCommand()); - assertEquals( - "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh", - ((ReqMessage) message).getSubscriptionId()); - assertEquals(1, ((ReqMessage) message).getFiltersList().size()); - - Filters filters = ((ReqMessage) message).getFiltersList().getFirst(); - - List kindFilters = filters.getFilterByType(KindFilter.FILTER_KEY); - assertEquals(1, kindFilters.size()); - assertEquals(new KindFilter<>(Kind.TEXT_NOTE), kindFilters.getFirst()); - - List eventFilter = filters.getFilterByType(EventFilter.FILTER_KEY); - assertEquals(1, eventFilter.size()); - assertEquals( - new EventFilter<>( - new GenericEvent("f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75")), - eventFilter.getFirst()); - - List referencedPublicKeyfilter = - filters.getFilterByType(ReferencedPublicKeyFilter.FILTER_KEY); - assertEquals(1, referencedPublicKeyfilter.size()); - assertEquals( - new ReferencedPublicKeyFilter<>( - new PubKeyTag( - new PublicKey("fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"))), - referencedPublicKeyfilter.getFirst()); - } - - @Test - public void testBaseMessageDecoderKindsAuthorsReferencedPublicKey() - throws JsonProcessingException { - - final String parseTarget = - "[\"REQ\", " - + "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " - + "{\"kinds\": [1], " - + "\"authors\": [\"f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75\"]," - + "\"#p\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; - - final var message = new BaseMessageDecoder<>().decode(parseTarget); - - assertEquals(Command.REQ.toString(), message.getCommand()); - assertEquals( - "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh", - ((ReqMessage) message).getSubscriptionId()); - assertEquals(1, ((ReqMessage) message).getFiltersList().size()); - - Filters filters = ((ReqMessage) message).getFiltersList().getFirst(); - - List kindFilters = filters.getFilterByType(KindFilter.FILTER_KEY); - assertEquals(1, kindFilters.size()); - assertEquals(new KindFilter<>(Kind.TEXT_NOTE), kindFilters.getFirst()); - - List authorFilters = filters.getFilterByType(AuthorFilter.FILTER_KEY); - assertEquals(1, authorFilters.size()); - assertEquals( - new AuthorFilter<>( - new PublicKey("f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75")), - authorFilters.getFirst()); - - List referencedPublicKeyfilter = - filters.getFilterByType(ReferencedPublicKeyFilter.FILTER_KEY); - assertEquals(1, referencedPublicKeyfilter.size()); - assertEquals( - new ReferencedPublicKeyFilter<>( - new PubKeyTag( - new PublicKey("fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"))), - referencedPublicKeyfilter.getFirst()); - } - - @Test - public void testBaseMessageDecoderKindsAuthorsReferencedEvents() throws JsonProcessingException { - - final String parseTarget = - "[\"REQ\", " - + "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " - + "{\"kinds\": [1], " - + "\"authors\": [\"f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75\"]," - + "\"#e\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; - - final var message = new BaseMessageDecoder<>().decode(parseTarget); - - assertEquals(Command.REQ.toString(), message.getCommand()); - assertEquals( - "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh", - ((ReqMessage) message).getSubscriptionId()); - assertEquals(1, ((ReqMessage) message).getFiltersList().size()); - - Filters filters = ((ReqMessage) message).getFiltersList().getFirst(); - - List kindFilters = filters.getFilterByType(KindFilter.FILTER_KEY); - assertEquals(1, kindFilters.size()); - assertEquals(new KindFilter<>(Kind.TEXT_NOTE), kindFilters.getFirst()); - - List authorFilters = filters.getFilterByType(AuthorFilter.FILTER_KEY); - assertEquals(1, authorFilters.size()); - assertEquals( - new AuthorFilter<>( - new PublicKey("f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75")), - authorFilters.getFirst()); - - List referencedEventFilters = - filters.getFilterByType(ReferencedEventFilter.FILTER_KEY); - assertEquals(1, referencedEventFilters.size()); - assertEquals( - new ReferencedEventFilter<>( - new EventTag("fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712")), - referencedEventFilters.getFirst()); - } - - @Test - public void testBaseReqMessageDecoder() throws JsonProcessingException { - - var publicKey = Identity.generateRandomIdentity().getPublicKey(); - - final var expectedReqMessage = - new ReqMessage( - publicKey.toString(), - new Filters( - new KindFilter<>(Kind.SET_METADATA), - new KindFilter<>(Kind.TEXT_NOTE), - new KindFilter<>(Kind.CONTACT_LIST), - new KindFilter<>(Kind.DELETION), - new AuthorFilter<>(publicKey))); - - String jsonMessage = expectedReqMessage.encode(); - - String jsonMsg = jsonMessage.substring(1, jsonMessage.length() - 1); - - System.out.println(jsonMessage); - - String[] parts = jsonMsg.split(","); - assertEquals("\"REQ\"", parts[0]); - assertEquals("\"" + publicKey.toHexString() + "\"", parts[1]); - assertFalse(parts[2].startsWith("[")); - assertFalse(parts[parts.length - 1].endsWith("]")); - - ReqMessage actualMessage = new BaseMessageDecoder().decode(jsonMessage); - - assertEquals(jsonMessage, actualMessage.encode()); - assertEquals(expectedReqMessage, actualMessage); - } - - @Test - public void testBaseEventMessageDecoder() throws JsonProcessingException { - - final String parseTarget = - "[\"EVENT\",\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\",{" - + "\"content\":\"直んないわ。まあええか\",\"created_at\":1686199583," - + "\"id\":\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"," - + "\"kind\":1," - + "\"pubkey\":\"8c59239319637f97e007dad0d681e65ce35b1ace333b629e2d33f9465c132608\"," - + "\"sig\":\"9584afd231c52fcbcec6ce668a2cc4b6dc9b4d9da20510dcb9005c6844679b4844edb7a2e1e0591958b0295241567c774dbf7d39a73932877542de1a5f963f4b\"," - + "\"tags\":[]}]"; - - final var message = new BaseMessageDecoder<>().decode(parseTarget); - - assertEquals(Command.EVENT.toString(), message.getCommand()); - - final var event = (GenericEvent) (((EventMessage) message).getEvent()); - assertEquals( - "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh", - ((EventMessage) message).getSubscriptionId()); - assertEquals(1, event.getKind().intValue()); - assertEquals(1686199583, event.getCreatedAt().longValue()); - assertEquals("fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712", event.getId()); - } - - @Test - public void testBaseEventMessageMarkerDecoder() throws JsonProcessingException { - - final String json = - "[\"EVENT\",\"temp20230627\",{" - + "\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\"," - + "\"kind\":1," - + "\"pubkey\":\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"," - + "\"created_at\":1687765220,\"content\":\"手順書が間違ってたら作業者は無理だな\",\"tags\":[" - + "[\"e\",\"494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346\",\"\",\"root\"]," - + "[\"p\",\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"]]," - + "\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" - + "}]"; - - BaseMessage message = new BaseMessageDecoder<>().decode(json); - - final var event = (GenericEvent) (((EventMessage) message).getEvent()); - var tags = event.getTags(); - for (BaseTag t : tags) { - if (t.getCode().equalsIgnoreCase("e")) { - EventTag et = (EventTag) t; - assertEquals(Marker.ROOT, et.getMarker()); - } - } - } - - @Test - public void testGenericTagDecoder() { - final String jsonString = "[\"saturn\", \"jetpack\", false]"; - - var tag = new GenericTagDecoder<>().decode(jsonString); - - assertEquals("saturn", tag.getCode()); - assertEquals(2, tag.getAttributes().size()); - assertEquals("jetpack", ((ElementAttribute) (tag.getAttributes().toArray())[0]).value()); - assertEquals( - false, - Boolean.valueOf( - ((ElementAttribute) (tag.getAttributes().toArray())[1]).value().toString())); - } - - @Test - public void testClassifiedListingTagSerializer() throws JsonProcessingException { - final String classifiedListingEventJson = - "{\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\",\"kind\":30402,\"content\":\"content" - + " ipsum\"," - + "\"pubkey\":\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\"," - + "\"created_at\":1687765220,\"tags\":[" - + "[\"p\",\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\"],[\"title\",\"title" - + " ipsum\"],[\"summary\",\"summary" - + " ipsum\"],[\"published_at\",\"1687765220\"],[\"location\",\"location ipsum\"]," - + "[\"price\",\"11111\",\"BTC\",\"1\"]]," - + "\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" - + "}]"; - - GenericEvent event = new GenericEventDecoder<>().decode(classifiedListingEventJson); - EventMessage message = NIP01.createEventMessage(event, "1"); - assertEquals("1", message.getNip()); - String encoded = new BaseEventEncoder<>((BaseEvent) message.getEvent()).encode(); - assertEquals( - "{\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\",\"kind\":30402,\"content\":\"content" - + " ipsum\",\"pubkey\":\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\",\"created_at\":1687765220,\"tags\":[[\"p\",\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\"],[\"title\",\"title" - + " ipsum\"],[\"summary\",\"summary" - + " ipsum\"],[\"published_at\",\"1687765220\"],[\"location\",\"location" - + " ipsum\"],[\"price\",\"11111\",\"BTC\",\"1\"]],\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"}", - encoded); - - assertEquals("28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a", event.getId()); - assertEquals(30402, event.getKind()); - assertEquals("content ipsum", event.getContent()); - assertEquals( - "ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de", - event.getPubKey().toString()); - assertEquals(1687765220L, event.getCreatedAt()); - assertEquals( - "86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546", - event.getSignature().toString()); - - assertEquals( - new BigDecimal("11111"), - event.getTags().stream() - .filter(baseTag -> baseTag.getCode().equalsIgnoreCase("price")) - .filter(PriceTag.class::isInstance) - .map(PriceTag.class::cast) - .map(PriceTag::getNumber) - .findFirst() - .orElseThrow()); - - assertEquals( - "BTC", - event.getTags().stream() - .filter(baseTag -> baseTag.getCode().equalsIgnoreCase("price")) - .filter(PriceTag.class::isInstance) - .map(PriceTag.class::cast) - .map(PriceTag::getCurrency) - .findFirst() - .orElseThrow()); - - assertEquals( - "1", - event.getTags().stream() - .filter(baseTag -> baseTag.getCode().equalsIgnoreCase("price")) - .filter(PriceTag.class::isInstance) - .map(PriceTag.class::cast) - .map(PriceTag::getFrequency) - .findFirst() - .orElseThrow()); - - List genericTags = - event.getTags().stream() - .filter(GenericTag.class::isInstance) - .map(GenericTag.class::cast) - .toList(); - - assertEquals( - "title ipsum", - genericTags.stream() - .filter(tag -> tag.getCode().equalsIgnoreCase("title")) - .map(GenericTag::getAttributes) - .toList() - .getFirst() - .getFirst() - .value()); - - assertEquals( - "summary ipsum", - genericTags.stream() - .filter(tag -> tag.getCode().equalsIgnoreCase("summary")) - .map(GenericTag::getAttributes) - .toList() - .getFirst() - .getFirst() - .value()); - - assertEquals( - "1687765220", - genericTags.stream() - .filter(tag -> tag.getCode().equalsIgnoreCase("published_at")) - .map(GenericTag::getAttributes) - .toList() - .getFirst() - .getFirst() - .value()); - - assertEquals( - "location ipsum", - genericTags.stream() - .filter(tag -> tag.getCode().equalsIgnoreCase("location")) - .map(GenericTag::getAttributes) - .toList() - .getFirst() - .getFirst() - .value()); - } - - @Test - public void testDeserializeTag() throws Exception { - - String npubHex = - new PublicKey( - Bech32.fromBech32( - "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9")) - .toString(); - final String jsonString = "[\"p\", \"" + npubHex + "\", \"wss://nostr.java\", \"alice\"]"; - var tag = new BaseTagDecoder<>().decode(jsonString); - - assertInstanceOf(PubKeyTag.class, tag); - - PubKeyTag pTag = (PubKeyTag) tag; - assertEquals("wss://nostr.java", pTag.getMainRelayUrl()); - assertEquals(npubHex, pTag.getPublicKey().toString()); - assertEquals("alice", pTag.getPetName()); - } - - @Test - public void testDeserializeGenericTag() throws Exception { - String npubHex = - new PublicKey( - Bech32.fromBech32( - "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9")) - .toString(); - final String jsonString = "[\"gt\", \"" + npubHex + "\", \"wss://nostr.java\", \"alice\"]"; - var tag = new BaseTagDecoder<>().decode(jsonString); - - assertInstanceOf(GenericTag.class, tag); - - GenericTag gTag = (GenericTag) tag; - assertEquals("gt", gTag.getCode()); - } - - @Test - public void testReqMessageFilterListSerializer() { - - String new_geohash = "2vghde"; - String second_geohash = "3abcde"; - - ReqMessage reqMessage = - new ReqMessage( - "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9", - new Filters( - new GenericTagQueryFilter<>(new GenericTagQuery("#g", new_geohash)), - new GenericTagQueryFilter<>(new GenericTagQuery("#g", second_geohash)))); - - assertDoesNotThrow( - () -> { - String jsonMessage = reqMessage.encode(); - String expected = - "[\"REQ\",\"npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9\",{\"#g\":[\"2vghde\",\"3abcde\"]}]"; - assertEquals(expected, jsonMessage); - }); - } - - @Test - public void testReqMessageGeohashTagDeserializer() throws JsonProcessingException { - - String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; - String geohashKey = "#g"; - String geohashValue = "2vghde"; - String reqJsonWithCustomTagQueryFilterToDecode = - "[\"REQ\",\"" + subscriptionId + "\",{\"" + geohashKey + "\":[\"" + geohashValue + "\"]}]"; - - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - ReqMessage expectedReqMessage = - new ReqMessage( - subscriptionId, new Filters(new GeohashTagFilter<>(new GeohashTag(geohashValue)))); - - assertEquals(expectedReqMessage, decodedReqMessage); - } - - @Test - public void testReqMessageGeohashFilterListDecoder() { - - String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; - String geohashKey = "#g"; - String geohashValue1 = "2vghde"; - String geohashValue2 = "3abcde"; - String reqJsonWithCustomTagQueryFiltersToDecode = - "[\"REQ\",\"" - + subscriptionId - + "\",{\"" - + geohashKey - + "\":[\"" - + geohashValue1 - + "\",\"" - + geohashValue2 - + "\"]}]"; - - assertDoesNotThrow( - () -> { - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFiltersToDecode); - - ReqMessage expectedReqMessage = - new ReqMessage( - subscriptionId, - new Filters( - new GeohashTagFilter<>(new GeohashTag(geohashValue1)), - new GeohashTagFilter<>(new GeohashTag(geohashValue2)))); - - assertEquals(reqJsonWithCustomTagQueryFiltersToDecode, decodedReqMessage.encode()); - assertEquals(expectedReqMessage, decodedReqMessage); - }); - } - - @Test - public void testReqMessageHashtagTagDeserializer() throws JsonProcessingException { - - String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; - String hashtagKey = "#t"; - String hashtagValue = "2vghde"; - String reqJsonWithCustomTagQueryFilterToDecode = - "[\"REQ\",\"" + subscriptionId + "\",{\"" + hashtagKey + "\":[\"" + hashtagValue + "\"]}]"; - - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - ReqMessage expectedReqMessage = - new ReqMessage( - subscriptionId, new Filters(new HashtagTagFilter<>(new HashtagTag(hashtagValue)))); - - assertEquals(expectedReqMessage, decodedReqMessage); - } - - @Test - public void testReqMessageHashtagTagFilterListDecoder() { - - String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; - String hashtagKey = "#t"; - String hashtagValue1 = "2vghde"; - String hashtagValue2 = "3abcde"; - String reqJsonWithCustomTagQueryFiltersToDecode = - "[\"REQ\",\"" - + subscriptionId - + "\",{\"" - + hashtagKey - + "\":[\"" - + hashtagValue1 - + "\",\"" - + hashtagValue2 - + "\"]}]"; - - assertDoesNotThrow( - () -> { - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFiltersToDecode); - - ReqMessage expectedReqMessage = - new ReqMessage( - subscriptionId, - new Filters( - new HashtagTagFilter<>(new HashtagTag(hashtagValue1)), - new HashtagTagFilter<>(new HashtagTag(hashtagValue2)))); - - assertEquals(reqJsonWithCustomTagQueryFiltersToDecode, decodedReqMessage.encode()); - assertEquals(expectedReqMessage, decodedReqMessage); - }); - } - - @Test - public void testReqMessagePopulatedFilterDecoder() { - - String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; - String kind = "1"; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String geohashKey = "#g"; - String geohashValue1 = "2vghde"; - String geohashValue2 = "3abcde"; - String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; - String reqJsonWithCustomTagQueryFilterToDecode = - "[\"REQ\", " - + "\"" - + subscriptionId - + "\", " - + "{\"kinds\": [" - + kind - + "], " - + "\"authors\": [\"" - + author - + "\"]," - + "\"" - + geohashKey - + "\": [\"" - + geohashValue1 - + "\",\"" - + geohashValue2 - + "\"]," - + "\"#e\": [\"" - + referencedEventId - + "\"]," - + "\"#p\": [\"" - + author - + "\"]" - + "}]"; - - assertDoesNotThrow( - () -> { - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - ReqMessage expectedReqMessage = - new ReqMessage( - subscriptionId, - new Filters( - new GeohashTagFilter<>(new GeohashTag(geohashValue1)), - new GeohashTagFilter<>(new GeohashTag(geohashValue2)), - new ReferencedPublicKeyFilter<>(new PubKeyTag(new PublicKey(author))), - new KindFilter<>(Kind.TEXT_NOTE), - new AuthorFilter<>(new PublicKey(author)), - new ReferencedEventFilter<>(new EventTag(referencedEventId)))); - - assertEquals(expectedReqMessage, decodedReqMessage); - }); - } - - @Test - public void testReqMessagePopulatedListOfFiltersWithIdentityDecoder() - throws JsonProcessingException { - - String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; - String kind = "1"; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String geohashKey = "#g"; - String geohashValue1 = "2vghde"; - String geohashValue2 = "3abcde"; - String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; - String uuidKey = "#d"; - String uuidValue1 = "UUID-1"; - String uuidValue2 = "UUID-2"; - String reqJsonWithCustomTagQueryFilterToDecode = - "[\"REQ\", " - + "\"" - + subscriptionId - + "\", " - + "{\"kinds\": [" - + kind - + "], " - + "\"authors\": [\"" - + author - + "\"]," - + "\"" - + geohashKey - + "\": [\"" - + geohashValue1 - + "\",\"" - + geohashValue2 - + "\"]," - + "\"" - + uuidKey - + "\": [\"" - + uuidValue1 - + "\",\"" - + uuidValue2 - + "\"]," - + "\"#e\": [\"" - + referencedEventId - + "\"]}]"; - - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - ReqMessage expectedReqMessage = - new ReqMessage( - subscriptionId, - new Filters( - new KindFilter<>(Kind.TEXT_NOTE), - new AuthorFilter<>(new PublicKey(author)), - new ReferencedEventFilter<>(new EventTag(referencedEventId)), - new GeohashTagFilter<>(new GeohashTag(geohashValue1)), - new GeohashTagFilter<>(new GeohashTag(geohashValue2)), - new IdentifierTagFilter<>(new IdentifierTag(uuidValue1)), - new IdentifierTagFilter<>(new IdentifierTag(uuidValue2)))); - - assertEquals(expectedReqMessage, decodedReqMessage); - } - - @Test - public void testReqMessagePopulatedListOfFiltersListDecoder() throws JsonProcessingException { - - String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; - Integer kind = 1; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; - String uuidValue1 = "UUID-1"; - - String addressableTag = String.join(":", String.valueOf(kind), author, uuidValue1); - - String reqJsonWithCustomTagQueryFilterToDecode = - "[\"REQ\", " - + "\"" - + subscriptionId - + "\", " - + "{\"kinds\": [" - + kind - + "], " - + "\"authors\": [\"" - + author - + "\"]," - + "\"#e\": [\"" - + referencedEventId - + "\"]," - + "\"#a\": [\"" - + addressableTag - + "\"]," - + "\"#p\": [\"" - + author - + "\"]" - + "}]"; - - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - AddressTag addressTag1 = new AddressTag(); - addressTag1.setKind(kind); - addressTag1.setPublicKey(new PublicKey(author)); - addressTag1.setIdentifierTag(new IdentifierTag(uuidValue1)); - - ReqMessage expectedReqMessage = - new ReqMessage( - subscriptionId, - new Filters( - new KindFilter<>(Kind.TEXT_NOTE), - new AuthorFilter<>(new PublicKey(author)), - new ReferencedEventFilter<>(new EventTag(referencedEventId)), - new ReferencedPublicKeyFilter<>(new PubKeyTag(new PublicKey(author))), - new AddressTagFilter<>(addressTag1))); - - assertEquals(expectedReqMessage.encode(), decodedReqMessage.encode()); - assertEquals(expectedReqMessage, decodedReqMessage); - } - - @Test - public void testReqMessagePopulatedListOfMultipleTypeFiltersListDecoder() - throws JsonProcessingException { - - String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; - String kind = "1"; - String kind2 = "2"; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String author2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; - String reqJsonWithCustomTagQueryFilterToDecode = - "[\"REQ\", " - + "\"" - + subscriptionId - + "\", " - + "{\"kinds\": [" - + kind - + ", " - + kind2 - + "], " - + "\"authors\": [\"" - + author - + "\",\"" - + author2 - + "\"]," - + "\"#e\": [\"" - + referencedEventId - + "\"]" - + "}]"; - - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - ReqMessage expectedReqMessage = - new ReqMessage( - subscriptionId, - new Filters( - new KindFilter<>(Kind.TEXT_NOTE), - new KindFilter<>(Kind.RECOMMEND_SERVER), - new AuthorFilter<>(new PublicKey(author)), - new AuthorFilter<>(new PublicKey(author2)), - new ReferencedEventFilter<>(new EventTag(referencedEventId)))); - - assertEquals(expectedReqMessage.encode(), decodedReqMessage.encode()); - assertEquals(expectedReqMessage, decodedReqMessage); - } - - @Test - public void testGenericTagQueryListDecoder() throws JsonProcessingException { - - String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; - String kind = "1"; - String kind2 = "2"; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String author2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - String geohashKey = "#g"; - String geohashValue1 = "2vghde"; - String geohashValue2 = "3abcde"; - String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; - String uuidKey = "#d"; - String uuidValue1 = "UUID-1"; - String uuidValue2 = "UUID-2"; - String reqJsonWithCustomTagQueryFilterToDecode = - "[\"REQ\", " - + "\"" - + subscriptionId - + "\", " - + "{\"kinds\": [" - + kind - + ", " - + kind2 - + "], " - + "\"authors\": [\"" - + author - + "\",\"" - + author2 - + "\"]," - + "\"" - + geohashKey - + "\": [\"" - + geohashValue1 - + "\",\"" - + geohashValue2 - + "\"]," - + "\"" - + uuidKey - + "\": [\"" - + uuidValue1 - + "\",\"" - + uuidValue2 - + "\"]," - + "\"#e\": [\"" - + referencedEventId - + "\"]" - + "}]"; - - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - ReqMessage expectedReqMessage = - new ReqMessage( - subscriptionId, - new Filters( - new KindFilter<>(Kind.TEXT_NOTE), - new KindFilter<>(Kind.RECOMMEND_SERVER), - new AuthorFilter<>(new PublicKey(author)), - new AuthorFilter<>(new PublicKey(author2)), - new ReferencedEventFilter<>(new EventTag(referencedEventId)), - new GeohashTagFilter<>(new GeohashTag(geohashValue1)), - new GeohashTagFilter<>(new GeohashTag(geohashValue2)), - new IdentifierTagFilter<>(new IdentifierTag(uuidValue1)), - new IdentifierTagFilter<>(new IdentifierTag(uuidValue2)))); - - assertTrue( - JsonComparator.isEquivalentJson( - mapper() - .createArrayNode() - .add(mapper().readTree(expectedReqMessage.encode())), - mapper() - .createArrayNode() - .add(mapper().readTree(decodedReqMessage.encode())))); - assertEquals(expectedReqMessage, decodedReqMessage); - } - - @Test - public void testReqMessageAddressableTagDeserializer() throws JsonProcessingException { - - Integer kind = 1; - String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String uuidKey = "#a"; - String uuidValue1 = "UUID-1"; - - String joined1 = String.join(":", String.valueOf(kind), author, uuidValue1); - - String reqJsonWithCustomTagQueryFilterToDecode = - "[\"REQ\",\"" + subscriptionId + "\",{\"" + uuidKey + "\":[\"" + joined1 + "\"]}]"; - - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - AddressTag addressTag1 = new AddressTag(); - addressTag1.setKind(kind); - addressTag1.setPublicKey(new PublicKey(author)); - addressTag1.setIdentifierTag(new IdentifierTag(uuidValue1)); - - ReqMessage expectedReqMessage = - new ReqMessage(subscriptionId, new Filters(new AddressTagFilter<>(addressTag1))); - - assertEquals(expectedReqMessage.encode(), decodedReqMessage.encode()); - assertEquals(expectedReqMessage, decodedReqMessage); - } - - @Test - public void testReqMessageSubscriptionIdTooLong() { - - String malformedSubscriptionId = - "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujhaa"; - final String parseTarget = - "[\"REQ\", " - + "\"" - + malformedSubscriptionId - + "\", " - + "{\"kinds\": [1], " - + "\"authors\": [\"f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75\"]," - + "\"#p\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; - - assertThrows( - IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(parseTarget)); - } - - @Test - public void testReqMessageSubscriptionIdTooShort() { - - String malformedSubscriptionId = ""; - final String parseTarget = - "[\"REQ\", " - + "\"" - + malformedSubscriptionId - + "\", " - + "{\"kinds\": [1], " - + "\"authors\": [\"f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75\"]," - + "\"#p\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; - - assertThrows( - IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(parseTarget)); - } - - @Test - public void testBaseEventMessageDecoderMultipleFiltersJson() throws JsonProcessingException { - - final String eventJson = - "[\"EVENT\",{\"content\":\"直ん直んないわ。まあええか\",\"created_at\":1786199583," - + "\"id\":\"ec7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"," - + "\"kind\":1," - + "\"pubkey\":\"9c59239319637f97e007dad0d681e65ce35b1ace333b629e2d33f9465c132608\"," - + "\"sig\":\"9584afd231c52fcbcec6ce668a2cc4b6dc9b4d9da20510dcb9005c6844679b4844edb7a2e1e0591958b0295241567c774dbf7d39a73932877542de1a5f963f4b\"," - + "\"tags\":[]}]"; - - final var eventMessage = new BaseMessageDecoder<>().decode(eventJson); - - assertEquals(Command.EVENT.toString(), eventMessage.getCommand()); - - final var event = (GenericEvent) (((EventMessage) eventMessage).getEvent()); - assertEquals(1, event.getKind().intValue()); - assertEquals(1786199583, event.getCreatedAt().longValue()); - assertEquals("ec7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712", event.getId()); - - String subscriptionId = "npub27x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; - final String requestJson = - "[\"REQ\", " - + "\"" - + subscriptionId - + "\", {\"kinds\": [1], \"authors\":" - + " [\"9c59239319637f97e007dad0d681e65ce35b1ace333b629e2d33f9465c132608\"]}," - + // first filter set - "{\"kinds\": [1], \"#p\":" - + " [\"ec7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}" - + // second filter set - "]"; - - final var message = new BaseMessageDecoder<>().decode(requestJson); - - assertEquals(Command.REQ.toString(), message.getCommand()); - assertEquals(subscriptionId, ((ReqMessage) message).getSubscriptionId()); - assertEquals(2, ((ReqMessage) message).getFiltersList().size()); - } - - @Test - public void testReqMessageVoteTagFilterDecoder() { - - String subscriptionId = "npub333k6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; - String voteTagKey = "#v"; - Integer voteTagValue = 1; - String reqJsonWithVoteTagFilterToDecode = - "[\"REQ\",\"" + subscriptionId + "\",{\"" + voteTagKey + "\":[\"" + voteTagValue + "\"]}]"; - - assertDoesNotThrow( - () -> { - ReqMessage decodedReqMessage = - new BaseMessageDecoder().decode(reqJsonWithVoteTagFilterToDecode); - - ReqMessage expectedReqMessage = - new ReqMessage( - subscriptionId, new Filters(new VoteTagFilter<>(new VoteTag(voteTagValue)))); - - assertEquals(reqJsonWithVoteTagFilterToDecode, decodedReqMessage.encode()); - assertEquals(expectedReqMessage, decodedReqMessage); - }); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP01EventBuilderTest.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP01EventBuilderTest.java deleted file mode 100644 index 7701629e8..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP01EventBuilderTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package nostr.api.unit; - -import nostr.api.nip01.NIP01EventBuilder; -import nostr.base.PrivateKey; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class NIP01EventBuilderTest { - - // Ensures that updating the default sender identity is respected by the builder. - @Test - void buildTextNoteUsesUpdatedIdentity() { - Identity defaultSender = Identity.create(PrivateKey.generateRandomPrivKey()); - Identity overrideSender = Identity.create(PrivateKey.generateRandomPrivKey()); - NIP01EventBuilder builder = new NIP01EventBuilder(defaultSender); - - // Update the default sender and ensure new events use it - builder.updateDefaultSender(overrideSender); - GenericEvent event = builder.buildTextNote("override"); - - assertEquals(overrideSender.getPublicKey(), event.getPubKey()); - } - - // Ensures that the builder uses the initially configured default sender when no update occurs. - @Test - void buildTextNoteUsesDefaultIdentityWhenOverrideMissing() { - Identity defaultSender = Identity.create(PrivateKey.generateRandomPrivKey()); - NIP01EventBuilder builder = new NIP01EventBuilder(defaultSender); - - GenericEvent event = builder.buildTextNote("fallback"); - - assertEquals(defaultSender.getPublicKey(), event.getPubKey()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP01MessagesTest.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP01MessagesTest.java deleted file mode 100644 index 94e81cf6f..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP01MessagesTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP01; -import nostr.base.Kind; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import nostr.event.impl.GenericEvent; -import nostr.event.message.CloseMessage; -import nostr.event.message.EoseMessage; -import nostr.event.message.EventMessage; -import nostr.event.message.NoticeMessage; -import nostr.event.message.ReqMessage; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** Unit tests for NIP-01 message creation and encoding. */ -public class NIP01MessagesTest { - - @Test - // EVENT message encodes with command and optional subscription id - void eventMessageEncodes() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - GenericEvent event = nip01.createTextNoteEvent("hi").sign().getEvent(); - - EventMessage msg = NIP01.createEventMessage(event, "sub-ev"); - String json = msg.encode(); - assertTrue(json.contains("\"EVENT\"")); - assertTrue(json.contains("\"sub-ev\"")); - } - - @Test - // REQ message encodes subscription id and filters - void reqMessageEncodes() throws Exception { - Filters filters = new Filters(new KindFilter<>(Kind.TEXT_NOTE)); - ReqMessage msg = NIP01.createReqMessage("sub-req", List.of(filters)); - String json = msg.encode(); - assertTrue(json.contains("\"REQ\"")); - assertTrue(json.contains("\"sub-req\"")); - assertTrue(json.contains("\"kinds\"")); - } - - @Test - // CLOSE message encodes subscription id - void closeMessageEncodes() throws Exception { - CloseMessage msg = NIP01.createCloseMessage("sub-close"); - String json = msg.encode(); - assertTrue(json.contains("\"CLOSE\"")); - assertTrue(json.contains("\"sub-close\"")); - } - - @Test - // EOSE message encodes subscription id - void eoseMessageEncodes() throws Exception { - EoseMessage msg = NIP01.createEoseMessage("sub-eose"); - String json = msg.encode(); - assertTrue(json.contains("\"EOSE\"")); - assertTrue(json.contains("\"sub-eose\"")); - } - - @Test - // NOTICE message encodes human readable message - void noticeMessageEncodes() throws Exception { - NoticeMessage msg = NIP01.createNoticeMessage("hello"); - String json = msg.encode(); - assertTrue(json.contains("\"NOTICE\"")); - assertTrue(json.contains("\"hello\"")); - } -} - diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP01Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP01Test.java deleted file mode 100644 index 1e2faf750..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP01Test.java +++ /dev/null @@ -1,311 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP01; -import nostr.event.BaseTag; -import nostr.event.entities.UserProfile; -import nostr.event.impl.GenericEvent; -import nostr.event.impl.InternetIdentifierMetadataEvent; -import nostr.event.impl.TextNoteEvent; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; -import nostr.util.NostrException; -import org.junit.jupiter.api.Test; - -import java.net.MalformedURLException; -import java.net.URI; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - -public class NIP01Test { - - @Test - public void testGenerateSignValidateAndConvertTextNote() throws NostrException { - // Step 1: Prepare - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - - // Step 2: Generate a text note as a GenericEvent - String content = "This is a test text note."; - GenericEvent genericEvent = nip01.createTextNoteEvent(content).sign().getEvent(); - - // Step 3: Convert the GenericEvent to a TextNoteEvent - TextNoteEvent textNoteEvent = GenericEvent.convert(genericEvent, TextNoteEvent.class); - - // Step 4: Validate the signed event - assertDoesNotThrow( - () -> textNoteEvent.validate(), - "The textNoteEvent validation should not throw an AssertionError."); - - // Step 5: Assert the conversion and content - assertInstanceOf( - TextNoteEvent.class, textNoteEvent, "The converted event should be a TextNoteEvent."); - assertEquals( - content, - textNoteEvent.getContent(), - "The content of the TextNoteEvent should match the original content."); - assertEquals( - sender.getPublicKey(), - textNoteEvent.getPubKey(), - "The public key of the TextNoteEvent should match the sender's public key."); - } - - @Test - public void testGenerateSignValidateAndConvertTextNoteWithRecipient() throws NostrException { - // Step 1: Prepare - Identity sender = Identity.generateRandomIdentity(); - Identity recipient = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - - // Step 2: Generate a text note with a recipient as a GenericEvent - String content = "This is a test text note with a recipient."; - BaseTag recipientTag = NIP01.createPubKeyTag(recipient.getPublicKey()); - GenericEvent genericEvent = - nip01.createTextNoteEvent(List.of(recipientTag), content).sign().getEvent(); - - // Step 3: Convert the GenericEvent to a TextNoteEvent - TextNoteEvent textNoteEvent = GenericEvent.convert(genericEvent, TextNoteEvent.class); - - // Step 4: Validate the signed event - assertDoesNotThrow( - () -> textNoteEvent.validate(), - "The textNoteEvent validation should not throw an AssertionError."); - - // Step 5: Assert the conversion, content, and recipient - assertInstanceOf( - TextNoteEvent.class, textNoteEvent, "The converted event should be a TextNoteEvent."); - assertEquals( - content, - textNoteEvent.getContent(), - "The content of the TextNoteEvent should match the original content."); - assertEquals( - sender.getPublicKey(), - textNoteEvent.getPubKey(), - "The public key of the TextNoteEvent should match the sender's public key."); - assertEquals( - 1, - textNoteEvent.getRecipients().size(), - "The TextNoteEvent should have exactly one recipient."); - assertEquals( - recipient.getPublicKey(), - textNoteEvent.getRecipients().get(0), - "The recipient's public key should match the one in the GenericEvent."); - } - - @Test - public void testCreateTextNoteEventWithRecipientListParameter() throws NostrException { - Identity sender = Identity.generateRandomIdentity(); - Identity recipient = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - - PubKeyTag recipientTag = new PubKeyTag(recipient.getPublicKey()); - GenericEvent genericEvent = - nip01.createTextNoteEvent("Generic", List.of(recipientTag)).sign().getEvent(); - - TextNoteEvent textNoteEvent = GenericEvent.convert(genericEvent, TextNoteEvent.class); - - assertEquals(1, textNoteEvent.getRecipients().size()); - assertEquals(recipient.getPublicKey(), textNoteEvent.getRecipients().get(0)); - } - - @Test - public void testGenerateSignValidateAndConvertMetadataEvent() - throws MalformedURLException, NostrException { - // Step 1: Prepare - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - - // Step 2: Generate a metadata event - UserProfile userProfile = - UserProfile.builder() - .nip05("testuser@nos.tr") - .name("test user") - .about("This is a test user profile.") - .picture(URI.create("https://example.com/profile.jpg").toURL()) - .build(); - GenericEvent genericEvent = nip01.createMetadataEvent(userProfile).sign().getEvent(); - - InternetIdentifierMetadataEvent metadataEvent = - GenericEvent.convert(genericEvent, InternetIdentifierMetadataEvent.class); - - // Step 3: Validate the signed event - assertDoesNotThrow( - () -> metadataEvent.validate(), - "The metadata event validation should not throw an AssertionError."); - - // Step 4: Assert the sender - assertEquals( - sender.getPublicKey(), - genericEvent.getPubKey(), - "The public key of the metadata event should match the sender's public key."); - } - - @Test - public void testGenerateSignValidateAndConvertReplaceableEvent() { - // Step 1: Prepare - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - - // Step 2: Generate a replaceable event - int kind = 10001; - String content = "This is a replaceable event."; - GenericEvent genericEvent = nip01.createReplaceableEvent(kind, content).sign().getEvent(); - - // Step 3: Validate the signed event - // assertDoesNotThrow(() -> ((ReplaceableEvent) genericEvent).validate(), "The replaceable event - // validation should not throw an AssertionError."); - - // Step 4: Assert the kind, content, and sender - assertEquals( - kind, - genericEvent.getKind(), - "The kind of the replaceable event should match the specified kind."); - assertEquals( - content, - genericEvent.getContent(), - "The content of the replaceable event should match the original content."); - assertEquals( - sender.getPublicKey(), - genericEvent.getPubKey(), - "The public key of the replaceable event should match the sender's public key."); - } - - @Test - public void testGenerateSignValidateAndConvertEphemeralEvent() { - // Step 1: Prepare - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - - // Step 2: Generate an ephemeral event - int kind = 20000; - String content = "This is an ephemeral event."; - GenericEvent genericEvent = nip01.createEphemeralEvent(kind, content).sign().getEvent(); - - // Step 3: Validate the signed event - // assertDoesNotThrow(() -> ((EphemeralEvent) genericEvent).validate(), "The ephemeral event - // validation should not throw an AssertionError."); - - // Step 4: Assert the kind, content, and sender - assertEquals( - kind, - genericEvent.getKind(), - "The kind of the ephemeral event should match the specified kind."); - assertEquals( - content, - genericEvent.getContent(), - "The content of the ephemeral event should match the original content."); - assertEquals( - sender.getPublicKey(), - genericEvent.getPubKey(), - "The public key of the ephemeral event should match the sender's public key."); - } - - @Test - public void testGenerateSignValidateAndConvertAddressableEvent() { - // Step 1: Prepare - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - - // Step 2: Generate a parameterized replaceable event - int kind = 30000; - String content = "This is a parameterized replaceable event."; - GenericEvent genericEvent = nip01.createAddressableEvent(kind, content).sign().getEvent(); - - // Step 3: Validate the signed event - // assertDoesNotThrow(() -> ((AddressableEvent) genericEvent).validate(), "The parameterized - // replaceable event validation should not throw an AssertionError."); - - // Step 4: Assert the kind, content, and sender - assertEquals( - kind, - genericEvent.getKind(), - "The kind of the parameterized replaceable event should match the specified kind."); - assertEquals( - content, - genericEvent.getContent(), - "The content of the parameterized replaceable event should match the original content."); - assertEquals( - sender.getPublicKey(), - genericEvent.getPubKey(), - "The public key of the parameterized replaceable event should match the sender's public" - + " key."); - } - - @Test - public void testCreateAddressableEventWithTagList() { - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - - GenericTag tag = new GenericTag("test"); - GenericEvent event = - nip01.createAddressableEvent(List.of(tag), 30001, "addr").sign().getEvent(); - - assertEquals(1, event.getTags().size()); - assertEquals("addr", event.getContent()); - } - - @Test - public void testCreateEventTag() { - String eventId = "test-event-id"; - BaseTag genericTag = NIP01.createEventTag(eventId); - - assertInstanceOf(EventTag.class, genericTag, "The created tag should be a EventTag."); - assertEquals("e", genericTag.getCode(), "The tag code should be 'e' for event tags."); - assertEquals( - eventId, - ((EventTag) genericTag).getIdEvent(), - "The event ID should match the provided value."); - } - - @Test - public void testCreatePubKeyTag() { - Identity identity = Identity.generateRandomIdentity(); - PubKeyTag pubKeyTag = (PubKeyTag) NIP01.createPubKeyTag(identity.getPublicKey()); - - assertInstanceOf(PubKeyTag.class, pubKeyTag, "The created tag should be a PubKeyTag."); - assertEquals("p", pubKeyTag.getCode(), "The tag code should be 'p' for pubkey tags."); - assertEquals( - identity.getPublicKey(), - pubKeyTag.getPublicKey(), - "The public key should match the provided value."); - } - - @Test - public void testCreateIdentifierTag() { - String identifier = "test-identifier"; - IdentifierTag identifierTag = (IdentifierTag) NIP01.createIdentifierTag(identifier); - - assertInstanceOf( - IdentifierTag.class, identifierTag, "The created tag should be an IdentifierTag."); - assertEquals("d", identifierTag.getCode(), "The tag code should be 'd' for identifier tags."); - assertEquals( - identifier, identifierTag.getUuid(), "The identifier should match the provided value."); - } - - @Test - public void testCreateAddressTag() { - Integer kind = 1; - Identity identity = Identity.generateRandomIdentity(); - String identifier = "test-identifier"; - AddressTag addressTag = - (AddressTag) NIP01.createAddressTag(kind, identity.getPublicKey(), identifier); - - assertInstanceOf(AddressTag.class, addressTag, "The created tag should be an AddressTag."); - assertEquals("a", addressTag.getCode(), "The tag code should be 'a' for address tags."); - assertEquals(kind, addressTag.getKind(), "The kind should match the provided value."); - assertEquals( - identity.getPublicKey(), - addressTag.getPublicKey(), - "The public key should match the provided value."); - assertEquals( - identifier, - addressTag.getIdentifierTag().getUuid(), - "The identifier should match the provided value."); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP02Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP02Test.java deleted file mode 100644 index ad6f62fea..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP02Test.java +++ /dev/null @@ -1,71 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP02; -import nostr.base.Kind; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP02Test { - - private Identity sender; - private NIP02 nip02; - - @BeforeEach - void setUp() { - sender = Identity.generateRandomIdentity(); - nip02 = new NIP02(sender); - } - - @Test - void testCreateContactListEvent() { - List tags = new ArrayList<>(); - tags.add(new PubKeyTag(sender.getPublicKey())); - nip02.createContactListEvent(new ArrayList<>(tags)); - assertNotNull(nip02.getEvent(), "Event should be created"); - assertEquals( - Kind.CONTACT_LIST.getValue(), nip02.getEvent().getKind(), "Kind should be CONTACT_LIST"); - } - - @Test - void testAddContactTag() { - BaseTag pTag = new PubKeyTag(sender.getPublicKey()); - nip02.createContactListEvent(new ArrayList<>()); - nip02.addContactTag(pTag); - assertTrue( - nip02.getEvent().getTags().stream() - .anyMatch(t -> t.getCode().equals(Constants.Tag.PUBKEY_CODE)), - "Contact tag should be added"); - } - - @Test - void testAddContactTagWithPublicKey() { - nip02.createContactListEvent(new ArrayList<>()); - nip02.addContactTag(sender.getPublicKey()); - assertTrue( - nip02.getEvent().getTags().stream() - .anyMatch(t -> t.getCode().equals(Constants.Tag.PUBKEY_CODE)), - "Contact tag from public key should be added"); - } - - @Test - void testAddContactTagThrowsException() { - nip02.createContactListEvent(new ArrayList<>()); - BaseTag invalidTag = BaseTag.create("x", "invalid"); - assertThrows( - IllegalArgumentException.class, - () -> nip02.addContactTag(invalidTag), - "Should throw if added tag is not a 'p' tag"); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP03Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP03Test.java deleted file mode 100644 index 6bae548ba..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP03Test.java +++ /dev/null @@ -1,31 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP01; -import nostr.api.NIP03; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.EventTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP03Test { - - @Test - public void testCreateOtsEvent() { - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - GenericEvent ref = nip01.createTextNoteEvent("test").sign().getEvent(); - - NIP03 nip03 = new NIP03(sender); - nip03.createOtsEvent(ref, "ots", "alt"); - GenericEvent event = nip03.getEvent(); - - assertNotNull(event); - assertEquals(1040, event.getKind()); // Constants.Kind.OTS_ATTESTATION - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals("alt"))); - assertTrue(event.getTags().stream().anyMatch(t -> t instanceof EventTag)); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP04Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP04Test.java deleted file mode 100644 index 076396e54..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP04Test.java +++ /dev/null @@ -1,194 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP04; -import nostr.base.ElementAttribute; -import nostr.base.Kind; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.List; - -/** - * Unit tests for NIP-04 (Encrypted Direct Messages). - * - *

These tests verify: - *

    - *
  • Encryption/decryption round-trip correctness
  • - *
  • Error handling for invalid inputs
  • - *
  • Edge cases (empty messages, special characters, large content)
  • - *
  • Event structure validation
  • - *
- */ -public class NIP04Test { - - private Identity sender; - private Identity recipient; - - @BeforeEach - void setup() { - sender = Identity.generateRandomIdentity(); - recipient = Identity.generateRandomIdentity(); - } - - @Test - public void testCreateAndDecryptDirectMessage() { - String content = "hello"; - - NIP04 nip04 = new NIP04(sender, recipient.getPublicKey()); - nip04.createDirectMessageEvent(content); - - GenericEvent event = nip04.getEvent(); - assertEquals(Kind.ENCRYPTED_DIRECT_MESSAGE.getValue(), event.getKind()); - assertTrue(event.getTags().stream().anyMatch(t -> t instanceof PubKeyTag)); - - String decrypted = NIP04.decrypt(recipient, event); - assertEquals(content, decrypted); - } - - @Test - public void testEncryptDecryptRoundtrip() { - String originalMessage = "This is a secret message!"; - - // Encrypt the message - String encrypted = NIP04.encrypt(sender, originalMessage, recipient.getPublicKey()); - - // Verify it's encrypted (not plaintext) - assertNotNull(encrypted); - assertNotEquals(originalMessage, encrypted); - assertTrue(encrypted.contains("?iv="), "Encrypted message should contain IV separator"); - - // Decrypt and verify - String decrypted = NIP04.decrypt(recipient, encrypted, sender.getPublicKey()); - assertEquals(originalMessage, decrypted); - } - - @Test - public void testSenderCanDecryptOwnMessage() { - String content = "Message from sender"; - - NIP04 nip04 = new NIP04(sender, recipient.getPublicKey()); - nip04.createDirectMessageEvent(content); - - GenericEvent event = nip04.getEvent(); - - // Sender should be able to decrypt their own message - String decryptedBySender = NIP04.decrypt(sender, event); - assertEquals(content, decryptedBySender); - - // Recipient should also be able to decrypt - String decryptedByRecipient = NIP04.decrypt(recipient, event); - assertEquals(content, decryptedByRecipient); - } - - @Test - public void testDecryptWithWrongRecipientFails() { - String content = "Secret message"; - - NIP04 nip04 = new NIP04(sender, recipient.getPublicKey()); - nip04.createDirectMessageEvent(content); - - GenericEvent event = nip04.getEvent(); - - // Create unrelated third party - Identity thirdParty = Identity.generateRandomIdentity(); - - // Third party attempting to decrypt should fail - assertThrows(RuntimeException.class, () -> NIP04.decrypt(thirdParty, event), - "Unrelated party should not be able to decrypt"); - } - - @Test - public void testEncryptEmptyMessage() { - String emptyContent = ""; - - NIP04 nip04 = new NIP04(sender, recipient.getPublicKey()); - nip04.createDirectMessageEvent(emptyContent); - - GenericEvent event = nip04.getEvent(); - - // Should successfully encrypt and decrypt empty string - String decrypted = NIP04.decrypt(recipient, event); - assertEquals(emptyContent, decrypted); - } - - @Test - public void testEncryptLargeMessage() { - // Create a large message (10KB) - StringBuilder largeContent = new StringBuilder(); - for (int i = 0; i < 1000; i++) { - largeContent.append("This is line ").append(i).append(" of a very long message.\n"); - } - String content = largeContent.toString(); - - NIP04 nip04 = new NIP04(sender, recipient.getPublicKey()); - nip04.createDirectMessageEvent(content); - - GenericEvent event = nip04.getEvent(); - - // Should handle large messages - String decrypted = NIP04.decrypt(recipient, event); - assertEquals(content, decrypted); - assertTrue(decrypted.length() > 10000, "Decrypted message should preserve length"); - } - - @Test - public void testEncryptSpecialCharacters() { - // Test with Unicode, emojis, and special characters - String content = "Hello 世界! 🔐 Encrypted: \"quotes\" 'apostrophes' & symbols €£¥"; - - NIP04 nip04 = new NIP04(sender, recipient.getPublicKey()); - nip04.createDirectMessageEvent(content); - - GenericEvent event = nip04.getEvent(); - - // Should preserve all special characters - String decrypted = NIP04.decrypt(recipient, event); - assertEquals(content, decrypted); - } - - @Test - // Ensures decrypt can resolve generic p-tags when determining the recipient. - public void testDecryptWithGenericPubKeyTagFallback() { - String content = "Generic tag ciphertext"; - - String encrypted = NIP04.encrypt(sender, content, recipient.getPublicKey()); - - GenericTag genericPTag = - new GenericTag( - "p", - List.of(new ElementAttribute("param0", recipient.getPublicKey().toString()))); - - GenericEvent event = - GenericEvent.builder() - .pubKey(sender.getPublicKey()) - .kind(Kind.ENCRYPTED_DIRECT_MESSAGE) - .tags(List.of(genericPTag)) - .content(encrypted) - .build(); - - String decrypted = NIP04.decrypt(recipient, event); - assertEquals(content, decrypted); - } - - @Test - public void testDecryptInvalidEventKindThrowsException() { - // Create a non-DM event - Identity identity = Identity.generateRandomIdentity(); - GenericEvent invalidEvent = new GenericEvent(identity.getPublicKey(), Kind.TEXT_NOTE); - invalidEvent.setContent("Not encrypted"); - - // Attempting to decrypt wrong kind should fail - assertThrows(IllegalArgumentException.class, () -> NIP04.decrypt(sender, invalidEvent), - "Should throw IllegalArgumentException for non-kind-4 event"); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP05Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP05Test.java deleted file mode 100644 index 21fb30d06..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP05Test.java +++ /dev/null @@ -1,37 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP05; -import nostr.event.entities.UserProfile; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.net.URI; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP05Test { - - @Test - public void testCreateInternetIdentifierMetadataEvent() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - NIP05 nip05 = new NIP05(sender); - UserProfile profile = - UserProfile.builder() - .name("tester") - .nip05("tester@example.com") - .publicKey(sender.getPublicKey()) - .picture(URI.create("https://example.com/pic").toURL()) - .about("about") - .build(); - - nip05.createInternetIdentifierMetadataEvent(profile); - GenericEvent event = nip05.getEvent(); - - assertNotNull(event); - assertEquals(0, event.getKind().intValue()); // Constants.Kind.USER_METADATA but 0 - assertTrue(event.getContent().contains("tester@example.com")); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP09Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP09Test.java deleted file mode 100644 index b2954af40..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP09Test.java +++ /dev/null @@ -1,30 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP01; -import nostr.api.NIP09; -import nostr.base.Kind; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP09Test { - - @Test - public void testCreateDeletionEvent() { - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - GenericEvent note = nip01.createTextNoteEvent("del me").getEvent(); - - NIP09 nip09 = new NIP09(sender); - nip09.createDeletionEvent(List.of(note)); - GenericEvent event = nip09.getEvent(); - - assertEquals(Kind.DELETION.getValue(), event.getKind()); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals("e"))); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP12Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP12Test.java deleted file mode 100644 index 5e471af1c..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP12Test.java +++ /dev/null @@ -1,31 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP12; -import nostr.event.BaseTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.ReferenceTag; -import org.junit.jupiter.api.Test; - -import java.net.URL; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class NIP12Test { - - @Test - public void testCreateTags() throws Exception { - BaseTag hTag = NIP12.createHashtagTag("nostr"); - assertEquals("t", hTag.getCode()); - assertEquals("nostr", ((HashtagTag) hTag).getHashTag()); - - URL url = new URL("https://example.com"); - BaseTag rTag = NIP12.createReferenceTag(url); - assertEquals("r", rTag.getCode()); - assertEquals(url.toString(), ((ReferenceTag) rTag).getUri().toString()); - - BaseTag gTag = NIP12.createGeohashTag("loc"); - assertEquals("g", gTag.getCode()); - assertEquals("loc", ((GeohashTag) gTag).getLocation()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP14Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP14Test.java deleted file mode 100644 index ef2c2aaa4..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP14Test.java +++ /dev/null @@ -1,18 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP14; -import nostr.event.BaseTag; -import nostr.event.tag.SubjectTag; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class NIP14Test { - - @Test - public void testCreateSubjectTag() { - BaseTag tag = NIP14.createSubjectTag("subj"); - assertEquals("subject", tag.getCode()); - assertEquals("subj", ((SubjectTag) tag).getSubject()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP15Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP15Test.java deleted file mode 100644 index 54c940a2c..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP15Test.java +++ /dev/null @@ -1,33 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP15; -import nostr.event.entities.Product; -import nostr.event.entities.Stall; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.IdentifierTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - -public class NIP15Test { - - @Test - public void testCreateProductEvent() { - Identity sender = Identity.generateRandomIdentity(); - NIP15 nip15 = new NIP15(sender); - Product product = new Product(); - product.setName("item"); - product.setStall(new Stall()); - product.setCurrency("USD"); - product.setPrice(1f); - product.setQuantity(1); - nip15.createCreateOrUpdateProductEvent(product, List.of("tag")); - GenericEvent event = nip15.getEvent(); - assertNotNull(event.getId()); - IdentifierTag idTag = (IdentifierTag) event.getTags().get(0); - assertNotNull(idTag.getUuid()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP20Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP20Test.java deleted file mode 100644 index 57a26cf91..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP20Test.java +++ /dev/null @@ -1,25 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP01; -import nostr.api.NIP20; -import nostr.event.impl.GenericEvent; -import nostr.event.message.OkMessage; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP20Test { - - @Test - public void testCreateOkMessage() { - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - GenericEvent event = nip01.createTextNoteEvent("msg").getEvent(); - OkMessage ok = NIP20.createOkMessage(event, true, "ok"); - assertEquals(event.getId(), ok.getEventId()); - assertTrue(ok.getFlag()); - assertEquals("ok", ok.getMessage()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP23Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP23Test.java deleted file mode 100644 index 6d1326e10..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP23Test.java +++ /dev/null @@ -1,29 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP23; -import nostr.base.Kind; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.net.URL; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP23Test { - - @Test - public void testCreateLongFormTextNoteEvent() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - NIP23 nip23 = new NIP23(sender); - nip23.creatLongFormTextNoteEvent("long"); - nip23.addTitleTag("title"); - nip23.addImageTag(new URL("https://example.com")); - GenericEvent event = nip23.getEvent(); - - assertEquals(Kind.LONG_FORM_TEXT_NOTE.getValue(), event.getKind()); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals("title"))); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals("image"))); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP25Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP25Test.java deleted file mode 100644 index da7fcd900..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP25Test.java +++ /dev/null @@ -1,29 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP01; -import nostr.api.NIP25; -import nostr.event.entities.Reaction; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.EventTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP25Test { - - @Test - public void testCreateReactionEvent() { - Identity sender = Identity.generateRandomIdentity(); - NIP01 nip01 = new NIP01(sender); - GenericEvent note = nip01.createTextNoteEvent("hi").getEvent(); - - NIP25 nip25 = new NIP25(sender); - nip25.createReactionEvent(note, Reaction.LIKE, null); - GenericEvent event = nip25.getEvent(); - - assertEquals("+", event.getContent()); - assertTrue(event.getTags().stream().anyMatch(t -> t instanceof EventTag)); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP28Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP28Test.java deleted file mode 100644 index af79f7954..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP28Test.java +++ /dev/null @@ -1,47 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP28; -import nostr.base.Kind; -import nostr.event.entities.ChannelProfile; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP28Test { - - @Test - public void testCreateChannelCreateEvent() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - NIP28 nip28 = new NIP28(sender); - ChannelProfile profile = - new ChannelProfile("channel", "about", new java.net.URL("https://example.com")); - nip28.createChannelCreateEvent(profile); - GenericEvent event = nip28.getEvent(); - - assertEquals(Kind.CHANNEL_CREATE.getValue(), event.getKind()); - assertTrue(event.getContent().contains("channel")); - } - - @Test - public void testUpdateChannelMetadataEvent() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - NIP28 nip28 = new NIP28(sender); - ChannelProfile profile = - new ChannelProfile("channel", "about", new java.net.URL("https://example.com")); - nip28.createChannelCreateEvent(profile); - GenericEvent channelCreate = nip28.getEvent(); - - ChannelProfile updated = - new ChannelProfile("updated", "changed", new java.net.URL("https://example.com/2")); - nip28.updateChannelMetadataEvent(channelCreate, updated, null); - GenericEvent metadataEvent = nip28.getEvent(); - - assertEquals(Kind.CHANNEL_METADATA.getValue(), metadataEvent.getKind()); - assertTrue(metadataEvent.getContent().contains("updated")); - assertFalse(metadataEvent.getTags().isEmpty()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP30Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP30Test.java deleted file mode 100644 index e26933196..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP30Test.java +++ /dev/null @@ -1,19 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP30; -import nostr.event.BaseTag; -import nostr.event.tag.EmojiTag; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class NIP30Test { - - @Test - public void testCreateEmojiTag() { - BaseTag tag = NIP30.createEmojiTag("smile", "https://img"); - assertEquals("emoji", tag.getCode()); - assertEquals("smile", ((EmojiTag) tag).getShortcode()); - assertEquals("https://img", ((EmojiTag) tag).getUrl()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP31Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP31Test.java deleted file mode 100644 index 7352c042e..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP31Test.java +++ /dev/null @@ -1,18 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP31; -import nostr.event.BaseTag; -import nostr.event.tag.GenericTag; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class NIP31Test { - - @Test - public void testCreateAltTag() { - BaseTag tag = NIP31.createAltTag("desc"); - assertEquals("alt", tag.getCode()); - assertEquals("desc", ((GenericTag) tag).getAttributes().get(0).value()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP32Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP32Test.java deleted file mode 100644 index f176b14de..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP32Test.java +++ /dev/null @@ -1,24 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP32; -import nostr.event.BaseTag; -import nostr.event.tag.LabelNamespaceTag; -import nostr.event.tag.LabelTag; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class NIP32Test { - - @Test - public void testCreateTags() { - BaseTag ns = NIP32.createNameSpaceTag("ns"); - assertEquals("L", ns.getCode()); - assertEquals("ns", ((LabelNamespaceTag) ns).getNameSpace()); - - BaseTag label = NIP32.createLabelTag("label", "ns"); - assertEquals("l", label.getCode()); - assertEquals("label", ((LabelTag) label).getLabel()); - assertEquals("ns", ((LabelTag) label).getNameSpace()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP40Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP40Test.java deleted file mode 100644 index 2cda66ae2..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP40Test.java +++ /dev/null @@ -1,18 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP40; -import nostr.event.BaseTag; -import nostr.event.tag.ExpirationTag; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class NIP40Test { - - @Test - public void testCreateExpirationTag() { - BaseTag tag = NIP40.createExpirationTag(10); - assertEquals("expiration", tag.getCode()); - assertEquals(10, ((ExpirationTag) tag).getExpiration().intValue()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP42Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP42Test.java deleted file mode 100644 index ab74b2506..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP42Test.java +++ /dev/null @@ -1,65 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP42; -import nostr.base.Kind; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.impl.CanonicalAuthenticationEvent; -import nostr.event.impl.GenericEvent; -import nostr.event.message.CanonicalAuthenticationMessage; -import nostr.event.tag.GenericTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP42Test { - - @Test - public void testCreateTags() { - Relay relay = new Relay("wss://relay"); - BaseTag rTag = NIP42.createRelayTag(relay); - assertEquals("relay", rTag.getCode()); - assertEquals(relay.getUri(), ((GenericTag) rTag).getAttributes().get(0).value()); - - BaseTag cTag = NIP42.createChallengeTag("abc"); - assertEquals("challenge", cTag.getCode()); - assertEquals("abc", ((GenericTag) cTag).getAttributes().get(0).value()); - } - - @Test - // Build a canonical auth event and client AUTH message; verify kind and required tags. - public void testCanonicalAuthEventAndMessage() throws Exception { - Identity sender = Identity.generateRandomIdentity(); - Relay relay = new Relay("wss://relay.example.com"); - NIP42 nip42 = new NIP42(); - nip42.setSender(sender); - - GenericEvent ev = nip42.createCanonicalAuthenticationEvent("token-123", relay).sign().getEvent(); - - assertEquals(Kind.CLIENT_AUTH.getValue(), ev.getKind()); - assertTrue(ev.getTags().stream().anyMatch(t -> t.getCode().equals("relay"))); - assertTrue(ev.getTags().stream().anyMatch(t -> t.getCode().equals("challenge"))); - - CanonicalAuthenticationEvent authEvent = GenericEvent.convert(ev, CanonicalAuthenticationEvent.class); - assertDoesNotThrow(authEvent::validate); - - CanonicalAuthenticationMessage msg = NIP42.createClientAuthenticationMessage(authEvent); - String json = msg.encode(); - assertTrue(json.contains("\"AUTH\"")); - // Encoded AUTH message should embed the full event JSON including tags - assertTrue(json.contains("\"tags\"")); - assertTrue(json.contains("relay")); - assertTrue(json.contains("challenge")); - } - - @Test - // Relay AUTH message includes challenge string. - public void testRelayAuthMessage() throws Exception { - String json = NIP42.createRelayAuthenticationMessage("c-1").encode(); - assertTrue(json.contains("\"AUTH\"")); - assertTrue(json.contains("\"c-1\"")); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP44Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP44Test.java deleted file mode 100644 index b72c65b56..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP44Test.java +++ /dev/null @@ -1,181 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP44; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Unit tests for NIP-44 (Encrypted Payloads - Versioned Encrypted Messages). - * - *

These tests verify: - *

    - *
  • XChaCha20-Poly1305 AEAD encryption/decryption
  • - *
  • Version byte handling (0x02)
  • - *
  • Padding correctness
  • - *
  • HMAC authentication
  • - *
  • Error handling and edge cases
  • - *
- */ -public class NIP44Test { - - private Identity sender; - private Identity recipient; - - @BeforeEach - void setup() { - sender = Identity.generateRandomIdentity(); - recipient = Identity.generateRandomIdentity(); - } - - @Test - public void testEncryptDecrypt() { - String message = "hello"; - - String encrypted = NIP44.encrypt(sender, message, recipient.getPublicKey()); - String decrypted = NIP44.decrypt(recipient, encrypted, sender.getPublicKey()); - assertEquals(message, decrypted); - } - - @Test - public void testDecryptEvent() { - String content = "msg"; - String enc = NIP44.encrypt(sender, content, recipient.getPublicKey()); - GenericEvent event = - new GenericEvent( - sender.getPublicKey(), 1050, List.of(new PubKeyTag(recipient.getPublicKey())), enc); - - String dec = NIP44.decrypt(recipient, event); - assertEquals(content, dec); - } - - @Test - public void testVersionBytePresent() { - String message = "Test message for NIP-44"; - - String encrypted = NIP44.encrypt(sender, message, recipient.getPublicKey()); - - // NIP-44 encrypted payloads should be base64 encoded with version byte - assertNotNull(encrypted); - assertTrue(encrypted.length() > 0, "Encrypted payload should not be empty"); - - // Decrypt to verify it works - String decrypted = NIP44.decrypt(recipient, encrypted, sender.getPublicKey()); - assertEquals(message, decrypted); - } - - @Test - public void testPaddingCorrectness() { - // NIP-44 uses power-of-2 padding. Test that padding doesn't affect decryption. - String shortMsg = "Hi"; - String mediumMsg = "This is a medium length message with more content to ensure padding"; - String longMsg = "This is a much longer message that should be padded to a different size " + - "according to NIP-44 padding scheme which uses power-of-2 boundaries. " + - "We add extra text here to make sure we cross padding boundaries and " + - "test that decryption still works correctly regardless of padding."; - - String encShort = NIP44.encrypt(sender, shortMsg, recipient.getPublicKey()); - String encMedium = NIP44.encrypt(sender, mediumMsg, recipient.getPublicKey()); - String encLong = NIP44.encrypt(sender, longMsg, recipient.getPublicKey()); - - // The key test: all messages decrypt correctly despite padding - assertEquals(shortMsg, NIP44.decrypt(recipient, encShort, sender.getPublicKey())); - assertEquals(mediumMsg, NIP44.decrypt(recipient, encMedium, sender.getPublicKey())); - assertEquals(longMsg, NIP44.decrypt(recipient, encLong, sender.getPublicKey())); - - // Verify encryption produces output - assertNotNull(encShort); - assertNotNull(encMedium); - assertNotNull(encLong); - } - - @Test - public void testAuthenticationDetectsTampering() { - String message = "Authenticated message"; - - String encrypted = NIP44.encrypt(sender, message, recipient.getPublicKey()); - - // Tamper with the encrypted payload by modifying a character - String tampered; - if (encrypted.endsWith("A")) { - tampered = encrypted.substring(0, encrypted.length() - 1) + "B"; - } else { - tampered = encrypted.substring(0, encrypted.length() - 1) + "A"; - } - - // Decryption should fail due to AEAD authentication - assertThrows(RuntimeException.class, () -> - NIP44.decrypt(recipient, tampered, sender.getPublicKey()), - "Tampered ciphertext should fail AEAD authentication"); - } - - @Test - public void testEncryptMinimalMessage() { - // NIP-44 requires minimum 1 byte plaintext - String minimalMsg = "a"; - - String encrypted = NIP44.encrypt(sender, minimalMsg, recipient.getPublicKey()); - String decrypted = NIP44.decrypt(recipient, encrypted, sender.getPublicKey()); - - assertEquals(minimalMsg, decrypted, "Minimal message should encrypt and decrypt correctly"); - } - - @Test - public void testEncryptSpecialCharacters() { - // Test with Unicode, emojis, and special characters - String message = "Hello 世界! 🔒 Encrypted with NIP-44: \"quotes\" 'apostrophes' & symbols €£¥ 中文"; - - String encrypted = NIP44.encrypt(sender, message, recipient.getPublicKey()); - String decrypted = NIP44.decrypt(recipient, encrypted, sender.getPublicKey()); - - assertEquals(message, decrypted, "All special characters should be preserved"); - } - - @Test - public void testEncryptLargeMessage() { - // NIP-44 supports up to 65535 bytes. Create a large message (~60KB) - StringBuilder largeMsg = new StringBuilder(); - for (int i = 0; i < 1000; i++) { - largeMsg.append("Line ").append(i).append(": NIP-44 handles large messages.\n"); - } - String message = largeMsg.toString(); - - // Verify message is within NIP-44 limits (≤ 65535 bytes) - assertTrue(message.getBytes().length <= 65535, "Message must be within NIP-44 limit"); - - String encrypted = NIP44.encrypt(sender, message, recipient.getPublicKey()); - String decrypted = NIP44.decrypt(recipient, encrypted, sender.getPublicKey()); - - assertEquals(message, decrypted); - assertTrue(decrypted.length() > 10000, "Large message should be preserved"); - } - - @Test - public void testConversationKeyConsistency() { - String message1 = "First message"; - String message2 = "Second message"; - - // Multiple encryptions with same key pair should work - String enc1 = NIP44.encrypt(sender, message1, recipient.getPublicKey()); - String enc2 = NIP44.encrypt(sender, message2, recipient.getPublicKey()); - - String dec1 = NIP44.decrypt(recipient, enc1, sender.getPublicKey()); - String dec2 = NIP44.decrypt(recipient, enc2, sender.getPublicKey()); - - assertEquals(message1, dec1); - assertEquals(message2, dec2); - - // Even though same keys, nonces should differ (different ciphertext) - assertNotEquals(enc1, enc2, "Same plaintext should produce different ciphertext (due to random nonce)"); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP46Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP46Test.java deleted file mode 100644 index e10750d11..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP46Test.java +++ /dev/null @@ -1,90 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP46; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP46Test { - - @Test - public void testRequestAndResponseSerialization() { - NIP46.Request req = new NIP46.Request(); - req.setId("1"); - req.setMethod("do"); - req.addParam("a"); - String json = req.toString(); - NIP46.Request parsed = NIP46.Request.fromString(json); - assertEquals(req.getId(), parsed.getId()); - assertEquals(req.getMethod(), parsed.getMethod()); - assertTrue(parsed.getParams().contains("a")); - - NIP46.Response resp = new NIP46.Response("1", null, "ok"); - String js = resp.toString(); - NIP46.Response parsedResp = NIP46.Response.fromString(js); - assertEquals("ok", parsedResp.getResult()); - } - - @Test - public void testCreateRequestEvent() { - Identity sender = Identity.generateRandomIdentity(); - Identity signer = Identity.generateRandomIdentity(); - NIP46 nip46 = new NIP46(sender); - NIP46.Request req = new NIP46.Request("1", "do", null); - nip46.createRequestEvent(req, signer.getPublicKey()); - assertNotNull(nip46.getEvent()); - } - - @Test - // Request event should be kind NOSTR_CONNECT, include p-tag of signer, and have encrypted content. - public void testRequestEventCompliance() { - Identity app = Identity.generateRandomIdentity(); - Identity signer = Identity.generateRandomIdentity(); - NIP46 nip46 = new NIP46(app); - NIP46.Request req = new NIP46.Request("42", "get_public_key", null); - var event = nip46.createRequestEvent(req, signer.getPublicKey()).sign().getEvent(); - - assertEquals(nostr.base.Kind.NOSTR_CONNECT.getValue(), event.getKind()); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals("p")), "p-tag must be present"); - assertNotNull(event.getContent()); - assertFalse(event.getContent().isEmpty()); - } - - @Test - // Response event should also be kind NOSTR_CONNECT and include app p-tag. - public void testResponseEventCompliance() { - Identity signer = Identity.generateRandomIdentity(); - Identity app = Identity.generateRandomIdentity(); - NIP46 nip46 = new NIP46(signer); - NIP46.Response resp = new NIP46.Response("42", null, "ok"); - var event = nip46.createResponseEvent(resp, app.getPublicKey()).sign().getEvent(); - assertEquals(nostr.base.Kind.NOSTR_CONNECT.getValue(), event.getKind()); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals("p"))); - } - - @Test - // Multi-parameter request should serialize deterministically and decrypt to original payload. - public void testMultiParamRequestRoundTrip() { - Identity app = Identity.generateRandomIdentity(); - Identity signer = Identity.generateRandomIdentity(); - NIP46 nip46 = new NIP46(app); - - NIP46.Request req = new NIP46.Request("7", "sign_event", null); - req.addParam("kind=1"); - req.addParam("tag=p:abcd"); - - var ev = nip46.createRequestEvent(req, signer.getPublicKey()).sign().getEvent(); - assertEquals(nostr.base.Kind.NOSTR_CONNECT.getValue(), ev.getKind()); - - String decrypted = nostr.api.NIP44.decrypt(signer, ev); - NIP46.Request parsed = NIP46.Request.fromString(decrypted); - assertEquals("7", parsed.getId()); - assertEquals("sign_event", parsed.getMethod()); - assertTrue(parsed.getParams().contains("kind=1")); - assertTrue(parsed.getParams().contains("tag=p:abcd")); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP52ImplTest.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP52ImplTest.java deleted file mode 100644 index 0d4717a57..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP52ImplTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP52; -import nostr.base.PublicKey; -import nostr.event.BaseTag; -import nostr.event.entities.CalendarContent; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.SubjectTag; -import nostr.id.Identity; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class NIP52ImplTest { - public static final String TIME_BASED_EVENT_CONTENT = "CalendarTimeBasedEvent unit test content"; - public static final String TIME_BASED_TITLE = "CalendarTimeBasedEvent title"; - public static final String CALENDAR_TIME_BASED_EVENT_SUMMARY = - "Calendar Time-Based Event listing summary"; - public static final String CALENDAR_TIME_BASED_EVENT_START_TZID = "1687765220"; - public static final Long START = 1716513986268L; - public static CalendarContent timeBasedCalendarContent; - public static Identity timeBasedSender; - public static NIP52 nip52; - public static final String CALENDAR_TIME_BASED_EVENT_LOCATION = - "Calendar Time-Based Event location"; - - // optional fields - public static final String PTAG_1_HEX = - "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76985"; - public static final PubKeyTag P_1_TAG = new PubKeyTag(new PublicKey(PTAG_1_HEX), null, "ISSUER"); - public static final String PTAG_2_HEX = - "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4347"; - public static final PubKeyTag P_2_TAG = - new PubKeyTag(new PublicKey(PTAG_2_HEX), null, "COUNTERPARTY"); - - public static final String SUBJECT = "Calendar Time-Based Event Test Subject Tag"; - public static final SubjectTag SUBJECT_TAG = new SubjectTag(SUBJECT); - public static final GeohashTag G_TAG = - new GeohashTag("Calendar Time-Based Event Test Geohash Tag"); - public static final HashtagTag T_TAG = - new HashtagTag("Calendar Time-Based Event Test Hashtag Tag"); - - public static final IdentifierTag identifierTag = - new IdentifierTag("UUID-CalendarTimeBasedEventTest"); - - @BeforeAll - static void setup() { - timeBasedCalendarContent = new CalendarContent<>(identifierTag, TIME_BASED_TITLE, START); - - timeBasedCalendarContent.addParticipantPubKeyTags(List.of(P_1_TAG, P_2_TAG)); - timeBasedCalendarContent.setGeohashTag(G_TAG); - timeBasedCalendarContent.addHashtagTags(List.of(T_TAG)); - timeBasedCalendarContent.setStartTzid(CALENDAR_TIME_BASED_EVENT_START_TZID); - timeBasedCalendarContent.setEndTzid(START.toString()); - Long l = START + 100L; - timeBasedCalendarContent.setEndTzid(l.toString()); - timeBasedCalendarContent.setSummary(CALENDAR_TIME_BASED_EVENT_SUMMARY); - timeBasedCalendarContent.setLocation(CALENDAR_TIME_BASED_EVENT_LOCATION); - timeBasedSender = Identity.generateRandomIdentity(); - nip52 = new NIP52(timeBasedSender); - } - - @Test - void testNIP52CreateTimeBasedCalendarCalendarEventWithAllOptionalParameters() { - List tags = new ArrayList<>(); - tags.add(SUBJECT_TAG); - GenericEvent calendarTimeBasedEvent = - nip52 - .createCalendarTimeBasedEvent(tags, TIME_BASED_EVENT_CONTENT, timeBasedCalendarContent) - .getEvent(); - calendarTimeBasedEvent.update(); - - // Test required fields - assertNotNull(calendarTimeBasedEvent.getId()); - assertTrue( - calendarTimeBasedEvent.getTags().contains(containsGeneric("title", TIME_BASED_TITLE))); - assertTrue( - calendarTimeBasedEvent.getTags().contains(containsGeneric("start", START.toString()))); - assertTrue(calendarTimeBasedEvent.getTags().contains(identifierTag)); - - // Test optional fields that were actually set - assertTrue(calendarTimeBasedEvent.getTags().contains(SUBJECT_TAG)); - assertTrue( - calendarTimeBasedEvent - .getTags() - .contains(containsGeneric("summary", CALENDAR_TIME_BASED_EVENT_SUMMARY))); - assertTrue(calendarTimeBasedEvent.getTags().contains(P_1_TAG)); - assertTrue(calendarTimeBasedEvent.getTags().contains(P_2_TAG)); - assertTrue( - calendarTimeBasedEvent - .getTags() - .contains(containsGeneric("location", CALENDAR_TIME_BASED_EVENT_LOCATION))); - - // Remove assertions for G_TAG and T_TAG since they weren't set in setup - - // Test equality with minimal required fields - CalendarContent calendarContent = - new CalendarContent<>(identifierTag, TIME_BASED_TITLE, START); - - calendarContent.setLocation(CALENDAR_TIME_BASED_EVENT_LOCATION); - GenericEvent instance2 = - nip52 - .createCalendarTimeBasedEvent(tags, TIME_BASED_EVENT_CONTENT, timeBasedCalendarContent) - .getEvent(); - - // calendarTimeBasedEvent.update(); - - // NOTE: TODO - Compare all attributes except id, createdAt, and serializedEventCache. - // assertEquals(calendarTimeBasedEvent, instance2); - // Test required fields - assertNotNull(instance2.getId()); - assertTrue(instance2.getTags().contains(containsGeneric("title", TIME_BASED_TITLE))); - assertTrue(instance2.getTags().contains(containsGeneric("start", START.toString()))); - assertTrue(instance2.getTags().contains(identifierTag)); - - // Test optional fields that were actually set - assertTrue(instance2.getTags().contains(SUBJECT_TAG)); - assertTrue( - instance2 - .getTags() - .contains(containsGeneric("summary", CALENDAR_TIME_BASED_EVENT_SUMMARY))); - assertTrue(instance2.getTags().contains(P_1_TAG)); - assertTrue(instance2.getTags().contains(P_2_TAG)); - assertTrue( - instance2 - .getTags() - .contains(containsGeneric("location", CALENDAR_TIME_BASED_EVENT_LOCATION))); - } - - private BaseTag containsGeneric(String key, String value) { - return BaseTag.create(key, value); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP57ImplTest.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP57ImplTest.java deleted file mode 100644 index 643f6cf2c..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP57ImplTest.java +++ /dev/null @@ -1,367 +0,0 @@ -package nostr.api.unit; - -import lombok.extern.slf4j.Slf4j; -import nostr.api.NIP57; -import nostr.api.nip57.ZapRequestParameters; -import nostr.base.Kind; -import nostr.base.PrivateKey; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.event.impl.ZapRequestEvent; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; -import nostr.util.NostrException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Unit tests for NIP-57 (Zaps - Lightning Payment Protocol). - * - *

These tests verify: - *

    - *
  • Zap request creation with amounts and LNURLs
  • - *
  • Zap receipt validation and field verification
  • - *
  • Relay list handling in zap requests
  • - *
  • Anonymous zap support
  • - *
  • Amount validation
  • - *
  • Description hash computation (SHA256)
  • - *
- */ -@Slf4j -public class NIP57ImplTest { - - private Identity sender; - private Identity zapRecipient; - private NIP57 nip57; - - @BeforeEach - void setup() { - sender = Identity.generateRandomIdentity(); - zapRecipient = Identity.generateRandomIdentity(); - nip57 = new NIP57(sender); - } - - @Test - // Verifies the legacy overload still constructs zap requests with explicit parameters. - void testNIP57CreateZapRequestEventFactory() throws NostrException { - - PublicKey recipient = zapRecipient.getPublicKey(); - final String ZAP_REQUEST_CONTENT = "zap request content"; - final Long AMOUNT = 1232456L; - final String LNURL = "lnUrl"; - final String RELAYS_URL = "ws://localhost:5555"; - - GenericEvent genericEvent = - nip57 - .createZapRequestEvent( - AMOUNT, - LNURL, - BaseTag.create("relays", RELAYS_URL), - ZAP_REQUEST_CONTENT, - recipient, - null, - null) - .getEvent(); - - ZapRequestEvent zapRequestEvent = GenericEvent.convert(genericEvent, ZapRequestEvent.class); - - assertNotNull(zapRequestEvent.getId()); - assertNotNull(zapRequestEvent.getTags()); - assertNotNull(zapRequestEvent.getContent()); - assertNotNull(zapRequestEvent.getZapRequest()); - assertNotNull(zapRequestEvent.getRecipientKey()); - - assertTrue( - zapRequestEvent.getRelays().stream().anyMatch(relay -> relay.getUri().equals(RELAYS_URL))); - assertEquals(ZAP_REQUEST_CONTENT, genericEvent.getContent()); - assertEquals(LNURL, zapRequestEvent.getLnUrl()); - assertEquals(AMOUNT, zapRequestEvent.getAmount()); - } - - @Test - // Ensures the ZapRequestParameters builder produces zap requests with relay lists. - void shouldBuildZapRequestEventFromParametersObject() throws NostrException { - - PublicKey recipient = zapRecipient.getPublicKey(); - Relay relay = new Relay("ws://localhost:6001"); - final String CONTENT = "parameter object zap"; - final Long AMOUNT = 42_000L; - final String LNURL = "lnurl1param"; - - ZapRequestParameters parameters = - ZapRequestParameters.builder() - .amount(AMOUNT) - .lnUrl(LNURL) - .relay(relay) - .content(CONTENT) - .recipientPubKey(recipient) - .build(); - - GenericEvent genericEvent = nip57.createZapRequestEvent(parameters).getEvent(); - - ZapRequestEvent zapRequestEvent = GenericEvent.convert(genericEvent, ZapRequestEvent.class); - - assertNotNull(zapRequestEvent.getId()); - assertNotNull(zapRequestEvent.getTags()); - assertEquals(CONTENT, genericEvent.getContent()); - assertEquals(LNURL, zapRequestEvent.getLnUrl()); - assertEquals(AMOUNT, zapRequestEvent.getAmount()); - assertTrue( - zapRequestEvent.getRelays().stream().anyMatch(existing -> existing.getUri().equals(relay.getUri()))); - } - - @Test - void testZapRequestWithMultipleRelays() throws NostrException { - PublicKey recipient = zapRecipient.getPublicKey(); - List relays = List.of( - new Relay("wss://relay1.example.com"), - new Relay("wss://relay2.example.com"), - new Relay("wss://relay3.example.com") - ); - - ZapRequestParameters parameters = - ZapRequestParameters.builder() - .amount(100_000L) - .lnUrl("lnurl123") - .relays(relays) - .content("Multi-relay zap") - .recipientPubKey(recipient) - .build(); - - GenericEvent event = nip57.createZapRequestEvent(parameters).getEvent(); - ZapRequestEvent zapRequest = GenericEvent.convert(event, ZapRequestEvent.class); - - // Verify all relays are included - assertEquals(3, zapRequest.getRelays().size()); - assertTrue(zapRequest.getRelays().stream() - .anyMatch(r -> r.getUri().equals("wss://relay1.example.com"))); - assertTrue(zapRequest.getRelays().stream() - .anyMatch(r -> r.getUri().equals("wss://relay2.example.com"))); - assertTrue(zapRequest.getRelays().stream() - .anyMatch(r -> r.getUri().equals("wss://relay3.example.com"))); - } - - @Test - void testZapRequestEventKindIsCorrect() throws NostrException { - ZapRequestParameters parameters = - ZapRequestParameters.builder() - .amount(50_000L) - .lnUrl("lnurl_test") - .relay(new Relay("wss://relay.test")) - .content("Zap!") - .recipientPubKey(zapRecipient.getPublicKey()) - .build(); - - GenericEvent event = nip57.createZapRequestEvent(parameters).getEvent(); - - // NIP-57 zap requests are kind 9734 - assertEquals(Kind.ZAP_REQUEST.getValue(), event.getKind(), - "Zap request should be kind 9734"); - } - - @Test - void testZapRequestRequiredTags() throws NostrException { - PublicKey recipient = zapRecipient.getPublicKey(); - - ZapRequestParameters parameters = - ZapRequestParameters.builder() - .amount(25_000L) - .lnUrl("lnurl_required_tags") - .relay(new Relay("wss://relay.test")) - .content("Testing required tags") - .recipientPubKey(recipient) - .build(); - - GenericEvent event = nip57.createZapRequestEvent(parameters).getEvent(); - ZapRequestEvent zapRequest = GenericEvent.convert(event, ZapRequestEvent.class); - - // Verify p-tag (recipient) is present - boolean hasPTag = event.getTags().stream() - .anyMatch(tag -> tag instanceof PubKeyTag && - ((PubKeyTag) tag).getPublicKey().equals(recipient)); - assertTrue(hasPTag, "Zap request must have p-tag with recipient public key"); - - // Verify relays tag is present - assertNotNull(zapRequest.getRelays()); - assertFalse(zapRequest.getRelays().isEmpty(), "Zap request must have at least one relay"); - } - - @Test - void testZapAmountValidation() throws NostrException { - // Test with zero amount - ZapRequestParameters zeroAmount = - ZapRequestParameters.builder() - .amount(0L) - .lnUrl("lnurl_zero") - .relay(new Relay("wss://relay.test")) - .content("Zero amount zap") - .recipientPubKey(zapRecipient.getPublicKey()) - .build(); - - GenericEvent event = nip57.createZapRequestEvent(zeroAmount).getEvent(); - ZapRequestEvent zapRequest = GenericEvent.convert(event, ZapRequestEvent.class); - - assertEquals(0L, zapRequest.getAmount(), - "Zap request should allow zero amount (optional tip)"); - } - - @Test - void testZapReceiptCreation() throws NostrException { - // Create a zap request first - ZapRequestParameters requestParams = - ZapRequestParameters.builder() - .amount(100_000L) - .lnUrl("lnurl_receipt_test") - .relay(new Relay("wss://relay.test")) - .content("Original zap request") - .recipientPubKey(zapRecipient.getPublicKey()) - .build(); - - GenericEvent zapRequest = nip57.createZapRequestEvent(requestParams).getEvent(); - - // Create zap receipt (typically done by Lightning service provider) - String bolt11Invoice = "lnbc1u0p3qwertyuiopasd"; // Mock invoice (1u = 100,000 msat) - String preimage = "0123456789abcdef"; // Mock preimage - - NIP57 receiptBuilder = new NIP57(zapRecipient); - GenericEvent receipt = receiptBuilder.createZapReceiptEvent( - zapRequest, - bolt11Invoice, - preimage, - sender.getPublicKey() - ).getEvent(); - - // Verify receipt is kind 9735 - assertEquals(Kind.ZAP_RECEIPT.getValue(), receipt.getKind(), - "Zap receipt should be kind 9735"); - - // Verify receipt contains bolt11 tag - boolean hasBolt11 = receipt.getTags().stream() - .anyMatch(tag -> tag.getCode().equals("bolt11")); - assertTrue(hasBolt11, "Zap receipt must contain bolt11 tag"); - - // Verify receipt has description (zap request JSON) - boolean hasDescription = receipt.getTags().stream() - .anyMatch(tag -> tag.getCode().equals("description")); - assertTrue(hasDescription, "Zap receipt must contain description tag with zap request"); - } - - @Test - // Validates that the zap receipt bolt11 amount matches the zap request amount. - void testZapAmountMatchesInvoiceAmount() throws NostrException { - ZapRequestParameters requestParams = - ZapRequestParameters.builder() - .amount(5_000L) // 5000 msat - .lnUrl("lnurl_amount_match") - .relay(new Relay("wss://relay.example.com")) - .content("amount match") - .recipientPubKey(zapRecipient.getPublicKey()) - .build(); - - GenericEvent zapRequest = nip57.createZapRequestEvent(requestParams).getEvent(); - - // Mock invoice that would encode 5000 msat (50n = 50 nanoBTC) - String bolt11Invoice = "lnbc50n1pqwertyuiopasd"; - String preimage = "00cafebabe"; - NIP57 receiptBuilder = new NIP57(zapRecipient); - GenericEvent receipt = - receiptBuilder - .createZapReceiptEvent(zapRequest, bolt11Invoice, preimage, sender.getPublicKey()) - .getEvent(); - - assertNotNull(receipt); - } - - @Test - // Verifies description_hash equals SHA-256 of the description JSON for the zap request. - void testZapDescriptionHash() throws Exception { - // Use fixed identities to ensure consistent hashing - Identity fixedSender = Identity.create(new PrivateKey("0000000000000000000000000000000000000000000000000000000000000001")); - Identity fixedRecipient = Identity.create(new PrivateKey("0000000000000000000000000000000000000000000000000000000000000002")); - NIP57 fixedNip57 = new NIP57(fixedSender); - - ZapRequestParameters requestParams = - ZapRequestParameters.builder() - .amount(1_000L) - .lnUrl("lnurl_desc_hash") - .relay(new Relay("wss://relay.example.com")) - .content("hash me") - .recipientPubKey(fixedRecipient.getPublicKey()) - .build(); - - GenericEvent zapRequest = fixedNip57.createZapRequestEvent(requestParams).getEvent(); - // Reset created_at to ensure consistent hashing across test runs - zapRequest.setCreatedAt(1234567890L); - String bolt11 = "lnbc10n1pqwertyuiopasd"; - String preimage = "00112233"; - NIP57 receiptBuilder = new NIP57(fixedRecipient); - GenericEvent receipt = - receiptBuilder - .createZapReceiptEvent(zapRequest, bolt11, preimage, fixedSender.getPublicKey()) - .getEvent(); - - // Extract description_hash tag - var descriptionHashTagOpt = receipt.getTags().stream() - .filter(t -> t.getCode().equals("description_hash")) - .findFirst(); - assertTrue(descriptionHashTagOpt.isPresent()); - - // Calculate expected hash from the original zap request - String zapRequestJson = nostr.base.json.EventJsonMapper.mapper().writeValueAsString(zapRequest); - String expectedHash = nostr.util.NostrUtil.bytesToHex(nostr.util.NostrUtil.sha256(zapRequestJson.getBytes())); - - // Get actual hash from the tag - String actualHash = ((nostr.event.tag.GenericTag) descriptionHashTagOpt.get()).getAttributes().get(0).value().toString(); - - assertEquals(expectedHash, actualHash, "description_hash must equal SHA-256 of description JSON"); - } - - @Test - // Validates that creating a zap receipt with missing required fields fails fast. - void testInvalidZapReceiptMissingFields() throws NostrException { - ZapRequestParameters requestParams = - ZapRequestParameters.builder() - .amount(1_000L) - .lnUrl("lnurl_test_receipt") - .relay(new Relay("wss://relay.example.com")) - .content("zap") - .recipientPubKey(zapRecipient.getPublicKey()) - .build(); - - GenericEvent zapRequest = nip57.createZapRequestEvent(requestParams).getEvent(); - NIP57 receiptBuilder = new NIP57(zapRecipient); - - // Missing bolt11 - assertThrows( - NullPointerException.class, - () -> receiptBuilder.createZapReceiptEvent(zapRequest, null, "preimage", sender.getPublicKey())); - // Missing preimage - assertThrows( - NullPointerException.class, - () -> receiptBuilder.createZapReceiptEvent(zapRequest, "bolt11", null, sender.getPublicKey())); - } - - @Test - // Ensures a zap request without relays information is rejected. - void testZapRequestMissingRelaysThrows() { - // Build parameters without relaysTag or relays list - ZapRequestParameters.ZapRequestParametersBuilder builder = - ZapRequestParameters.builder() - .amount(123L) - .lnUrl("lnurl_no_relays") - .content("no relays") - .recipientPubKey(zapRecipient.getPublicKey()); - - assertThrows(IllegalStateException.class, () -> builder.build().determineRelaysTag()); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP60Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP60Test.java deleted file mode 100644 index ff5c2ed20..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP60Test.java +++ /dev/null @@ -1,277 +0,0 @@ -package nostr.api.unit; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.NonNull; -import nostr.api.NIP44; -import nostr.api.NIP60; -import nostr.base.Marker; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.entities.Amount; -import nostr.event.entities.CashuMint; -import nostr.event.entities.CashuProof; -import nostr.event.entities.CashuQuote; -import nostr.event.entities.CashuToken; -import nostr.event.entities.CashuWallet; -import nostr.event.entities.SpendingHistory; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.ExpirationTag; -import nostr.event.tag.GenericTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static nostr.base.json.EventJsonMapper.mapper; - -public class NIP60Test { - - @Test - public void createWalletEvent() throws JsonProcessingException { - - // Prepare - CashuMint mint1 = new CashuMint("https://mint1"); - mint1.setUnits(List.of("sat")); - - CashuMint mint2 = new CashuMint("https://mint2"); - mint2.setUnits(List.of("sat")); - - CashuMint mint3 = new CashuMint("https://mint3"); - mint3.setUnits(List.of("sat")); - - Relay relay1 = new Relay("wss://relay1"); - Relay relay2 = new Relay("wss://relay2"); - - CashuWallet wallet = new CashuWallet(); - wallet.setId("my-wallet"); - wallet.setName("my shitposting wallet"); - wallet.setDescription("a wallet for my day-to-day shitposting"); - wallet.setBalance(100); - wallet.setPrivateKey("hexkey"); - // wallet.setUnit("sat"); - wallet.addMint(mint1); - wallet.addMint(mint2); - wallet.addMint(mint3); - wallet.addRelay("sat", relay1); - wallet.addRelay("sat", relay2); - - Identity sender = Identity.generateRandomIdentity(); - NIP60 nip60 = new NIP60(sender); - - // Create - GenericEvent event = nip60.createWalletEvent(wallet).getEvent(); - List tags = event.getTags(); - - // Assert kind - Assertions.assertEquals(17375, event.getKind()); - - // Assert tags - Assertions.assertEquals(10, tags.size()); - - // Assert relay tags - List relayTags = tags.stream().filter(tag -> tag.getCode().equals("relay")).toList(); - - Assertions.assertEquals(2, relayTags.size()); - - // Assert mint tags - List mintTags = tags.stream().filter(tag -> tag.getCode().equals("mint")).toList(); - - Assertions.assertEquals(3, mintTags.size()); - - // Decrypt and verify content - String decryptedContent = NIP44.decrypt(sender, event.getContent(), sender.getPublicKey()); - GenericTag[] contentTags = mapper().readValue(decryptedContent, GenericTag[].class); - - // First tag should be balance - Assertions.assertEquals("balance", contentTags[0].getCode()); - Assertions.assertEquals("100", contentTags[0].getAttributes().get(0).value()); - Assertions.assertEquals("sat", contentTags[0].getAttributes().get(1).value()); - - // Second tag should be privkey - Assertions.assertEquals("privkey", contentTags[1].getCode()); - Assertions.assertEquals("hexkey", contentTags[1].getAttributes().get(0).value()); - } - - @Test - public void createTokenEvent() throws JsonProcessingException { - - // Prepare - CashuMint mint = new CashuMint("https://stablenut.umint.cash"); - mint.setUnits(List.of("sat")); - - CashuWallet wallet = new CashuWallet(); - wallet.setId("my-wallet"); - wallet.setName("my shitposting wallet"); - wallet.setDescription("a wallet for my day-to-day shitposting"); - wallet.setBalance(100); - wallet.setPrivateKey("hexkey"); - // wallet.setUnit("sat"); - wallet.addMint(mint); - - CashuProof proof = new CashuProof(); - proof.setId("005c2502034d4f12"); - proof.setAmount(1); - proof.setSecret("z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg="); - proof.setC("0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46"); - - CashuToken token = new CashuToken(); - token.setMint(mint); - token.setProofs(List.of(proof)); - - Identity sender = Identity.generateRandomIdentity(); - NIP60 nip60 = new NIP60(sender); - - // Create - GenericEvent event = nip60.createTokenEvent(token, wallet).getEvent(); - List tags = event.getTags(); - - // Assert kind - Assertions.assertEquals(7375, event.getKind().intValue()); - - // Assert tags - Assertions.assertEquals(1, tags.size()); - - // Assert a-tag - AddressTag aTag = (AddressTag) tags.get(0); - Assertions.assertEquals("a", aTag.getCode()); - // Assertions.assertEquals("", aTag.getPublicKey()); - Assertions.assertEquals("my-wallet", aTag.getIdentifierTag().getUuid()); - Assertions.assertEquals(17375, aTag.getKind().intValue()); - - // Decrypt and verify content - String decryptedContent = NIP44.decrypt(sender, event.getContent(), sender.getPublicKey()); - CashuToken contentToken = mapper().readValue(decryptedContent, CashuToken.class); - Assertions.assertEquals("https://stablenut.umint.cash", contentToken.getMint().getUrl()); - - CashuProof proofContent = contentToken.getProofs().get(0); - Assertions.assertEquals(proof.getId(), proofContent.getId()); - Assertions.assertEquals(proof.getAmount(), proofContent.getAmount()); - Assertions.assertEquals(proof.getSecret(), proofContent.getSecret()); - Assertions.assertEquals(proof.getC(), proofContent.getC()); - } - - @Test - public void createSpendingHistoryEvent() throws JsonProcessingException { - - Amount amount = new Amount(); - amount.setAmount(1); - amount.setUnit("sat"); - - EventTag eventTag = new EventTag(); - eventTag.setIdEvent(""); - eventTag.setRecommendedRelayUrl(""); - eventTag.setMarker(Marker.CREATED); - - SpendingHistory spendingHistory = new SpendingHistory(); - spendingHistory.setDirection(SpendingHistory.Direction.RECEIVED); - spendingHistory.setAmount(amount); - spendingHistory.setEventTags(List.of(eventTag)); - - Identity sender = Identity.generateRandomIdentity(); - NIP60 nip60 = new NIP60(sender); - - CashuWallet wallet = new CashuWallet(); - wallet.setId("my-wallet"); - wallet.setName("my shitposting wallet"); - wallet.setDescription("a wallet for my day-to-day shitposting"); - wallet.setBalance(100); - wallet.setPrivateKey("hexkey"); - // wallet.setUnit("sat"); - - GenericEvent event = nip60.createSpendingHistoryEvent(spendingHistory, wallet).getEvent(); - List tags = event.getTags(); - - // Assert tags - Assertions.assertEquals(1, tags.size()); - Assertions.assertEquals(7376, event.getKind().intValue()); - - // Assert a-tag - AddressTag aTag = (AddressTag) tags.get(0); - Assertions.assertEquals("my-wallet", aTag.getIdentifierTag().getUuid()); - Assertions.assertEquals(17375, aTag.getKind().intValue()); - - // Decrypt and verify content - String decryptedContent = NIP44.decrypt(sender, event.getContent(), sender.getPublicKey()); - BaseTag[] contentTags = mapper().readValue(decryptedContent, BaseTag[].class); - - // Assert direction - GenericTag directionTag = (GenericTag) contentTags[0]; - Assertions.assertEquals("direction", directionTag.getCode()); - Assertions.assertEquals("in", directionTag.getAttributes().get(0).value().toString()); - - // Assert amount - GenericTag amountTag = (GenericTag) contentTags[1]; - Assertions.assertEquals("amount", amountTag.getCode()); - Assertions.assertEquals("1", amountTag.getAttributes().get(0).value()); - Assertions.assertEquals("sat", amountTag.getAttributes().get(1).value()); - - // Assert event - EventTag eTag = (EventTag) contentTags[2]; - Assertions.assertEquals("e", eTag.getCode()); - Assertions.assertEquals("", eTag.getIdEvent()); - Assertions.assertEquals("", eTag.getRecommendedRelayUrl()); - Assertions.assertEquals("created", eTag.getMarker().getValue()); - } - - @Test - public void createRedemptionQuoteEvent() { - - CashuWallet wallet = new CashuWallet(); - wallet.setId("my-wallet"); - wallet.setName("my shitposting wallet"); - wallet.setDescription("a wallet for my day-to-day shitposting"); - wallet.setBalance(100); - wallet.setPrivateKey("hexkey"); - // wallet.setUnit("sat"); - - CashuQuote quote = new CashuQuote(); - quote.setId("quote-id"); - quote.setExpiration(1728883200L); - quote.setMint(new CashuMint("")); - quote.setWallet(wallet); - - Identity sender = Identity.generateRandomIdentity(); - NIP60 nip60 = new NIP60(sender); - - GenericEvent event = nip60.createRedemptionQuoteEvent(quote).getEvent(); - List tags = event.getTags(); - - // Assert kind - Assertions.assertEquals(7374, event.getKind().intValue()); - - // Assert tags - Assertions.assertEquals(3, tags.size()); - - // Assert Expiration tag - ExpirationTag expirationTag = (ExpirationTag) tags.get(0); - Assertions.assertEquals("expiration", expirationTag.getCode()); - Assertions.assertEquals(1728883200, expirationTag.getExpiration()); - - // Assert CashuMint tag - GenericTag mintTag = (GenericTag) tags.get(1); - Assertions.assertEquals("mint", mintTag.getCode()); - Assertions.assertEquals("", mintTag.getAttributes().get(0).value()); - - // Assert a-tag - AddressTag aTag = (AddressTag) tags.get(2); - Assertions.assertEquals("my-wallet", aTag.getIdentifierTag().getUuid()); - Assertions.assertEquals(17375, aTag.getKind().intValue()); - } - - private String getMintUrl(@NonNull BaseTag tag) { - if (tag instanceof GenericTag mintTag) { - return mintTag.getAttributes().get(0).value().toString(); - } - return null; - } - - private String getRelayUrl(@NonNull BaseTag tag) { - if (tag instanceof GenericTag relayTag) { - return relayTag.getAttributes().get(0).value().toString(); - } - return null; - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP61Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP61Test.java deleted file mode 100644 index c0fba66a5..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP61Test.java +++ /dev/null @@ -1,194 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP60; -import nostr.api.NIP61; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.entities.Amount; -import nostr.event.entities.CashuMint; -import nostr.event.entities.CashuProof; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.EventTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.UrlTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.net.MalformedURLException; -import java.net.URI; -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - -public class NIP61Test { - - @Test - // Verifies that informational Nutzap events include the expected relay, mint, and pubkey tags. - public void createNutzapInformationalEvent() { - // Prepare - Identity sender = Identity.generateRandomIdentity(); - NIP61 nip61 = new NIP61(sender); - - // Create test data - List pubkeys = Arrays.asList("pubkey1", "pubkey2"); - List relays = - Arrays.asList(new Relay("wss://relay1.example.com"), new Relay("wss://relay2.example.com")); - List mints = - Arrays.asList( - new CashuMint("https://mint1.example.com"), new CashuMint("https://mint2.example.com")); - - // Create event - GenericEvent event = nip61.createNutzapInformationalEvent(pubkeys, relays, mints).getEvent(); - List tags = event.getTags(); - - // Assert tags - Assertions.assertEquals(6, tags.size()); // 2 pubkeys + 2 relays + 2 mints - - // Verify pubkey tags - List pubkeyTags = - tags.stream() - .filter(tag -> tag.getCode().equals("pubkey")) - .map(tag -> (GenericTag) tag) - .toList(); - Assertions.assertEquals(2, pubkeyTags.size()); - Assertions.assertEquals("pubkey1", pubkeyTags.get(0).getAttributes().get(0).value()); - Assertions.assertEquals("pubkey2", pubkeyTags.get(1).getAttributes().get(0).value()); - - // Verify relay tags - List relayTags = - tags.stream() - .filter(tag -> tag.getCode().equals("relay")) - .map(tag -> (GenericTag) tag) - .toList(); - Assertions.assertEquals(2, relayTags.size()); - Assertions.assertEquals( - "wss://relay1.example.com", relayTags.get(0).getAttributes().get(0).value()); - Assertions.assertEquals( - "wss://relay2.example.com", relayTags.get(1).getAttributes().get(0).value()); - - // Verify mint tags - List mintTags = - tags.stream() - .filter(tag -> tag.getCode().equals("mint")) - .map(tag -> (GenericTag) tag) - .toList(); - Assertions.assertEquals(2, mintTags.size()); - Assertions.assertEquals( - "https://mint1.example.com", mintTags.get(0).getAttributes().get(0).value()); - Assertions.assertEquals( - "https://mint2.example.com", mintTags.get(1).getAttributes().get(0).value()); - } - - @Test - // Validates that Nutzap events include URL, amount, and pubkey tags when provided with data. - public void createNutzapEvent() { - // Prepare - Identity sender = Identity.generateRandomIdentity(); - NIP61 nip61 = new NIP61(sender); - - Identity recipientId = Identity.generateRandomIdentity(); - - // Create test data - Amount amount = new Amount(100, "sat"); - CashuMint mint = new CashuMint("https://mint.example.com"); - // PublicKey recipient = new PublicKey("recipient-pubkey"); - String content = "Test content"; - - // Optional proofs and events - CashuProof proof = new CashuProof(); - proof.setId("test-proof-id"); - List proofs = List.of(proof); - - EventTag eventTag = new EventTag(); - eventTag.setIdEvent("test-event-id"); - List events = List.of(eventTag); - - // Create event - GenericEvent event; - try { - event = - nip61 - .createNutzapEvent( - proofs, - URI.create(mint.getUrl()).toURL(), - events.get(0), - recipientId.getPublicKey(), - content) - .getEvent(); - // Add amount and unit tags explicitly via NIP60 helpers - event.addTag(NIP60.createAmountTag(amount)); - event.addTag(NIP60.createUnitTag(amount.getUnit())); - } catch (MalformedURLException ex) { - Assertions.fail("Mint URL should be valid in test data", ex); - return; - } - List tags = event.getTags(); - - // Assert tags - Assertions.assertEquals(6, tags.size()); // url + amount + unit + pubkey - - // Verify url tag - List urlTags = tags.stream().filter(tag -> tag.getCode().equals("u")).toList(); - assertInstanceOf(UrlTag.class, urlTags.get(0)); - Assertions.assertEquals(1, urlTags.size()); - Assertions.assertEquals("https://mint.example.com", ((UrlTag) urlTags.get(0)).getUrl()); - - // Verify amount tag - List amountTags = tags.stream().filter(tag -> tag.getCode().equals("amount")).toList(); - assertInstanceOf(GenericTag.class, amountTags.get(0)); - Assertions.assertEquals(1, amountTags.size()); - Assertions.assertEquals("100", ((GenericTag) amountTags.get(0)).getAttributes().get(0).value()); - - // Verify unit tag - List unitTags = tags.stream().filter(tag -> tag.getCode().equals("unit")).toList(); - assertInstanceOf(GenericTag.class, unitTags.get(0)); - Assertions.assertEquals(1, unitTags.size()); - Assertions.assertEquals("sat", ((GenericTag) unitTags.get(0)).getAttributes().get(0).value()); - - // Verify pubkey tag - List pubkeyTags = tags.stream().filter(tag -> tag.getCode().equals("p")).toList(); - assertInstanceOf(PubKeyTag.class, pubkeyTags.get(0)); - Assertions.assertEquals(1, pubkeyTags.size()); - Assertions.assertEquals( - recipientId.getPublicKey().toString(), - ((PubKeyTag) pubkeyTags.get(0)).getPublicKey().toString()); - - // Assert content - Assertions.assertEquals(content, event.getContent()); - } - - @Test - // Ensures convenience tag factory methods create correctly coded tags. - public void createTags() { - // Test P2PK tag creation - String pubkey = "test-pubkey"; - BaseTag p2pkTag = NIP61.createP2pkTag(pubkey); - assertInstanceOf(GenericTag.class, p2pkTag); - Assertions.assertEquals("pubkey", p2pkTag.getCode()); - Assertions.assertEquals(pubkey, ((GenericTag) p2pkTag).getAttributes().get(0).value()); - - // Test URL tag creation - String url = "https://example.com"; - BaseTag urlTag = NIP61.createUrlTag(url); - assertInstanceOf(UrlTag.class, urlTag); - Assertions.assertEquals("u", urlTag.getCode()); - Assertions.assertEquals(url, ((UrlTag) urlTag).getUrl()); - - // Test CashuProof tag creation - CashuProof proof = new CashuProof(); - proof.setId("test-proof-id"); - BaseTag proofTag = NIP61.createProofTag(proof); - assertInstanceOf(GenericTag.class, proofTag); - Assertions.assertEquals("proof", proofTag.getCode()); - Assertions.assertTrue( - ((GenericTag) proofTag) - .getAttributes() - .get(0) - .value() - .toString() - .contains("test-proof-id")); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP65Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP65Test.java deleted file mode 100644 index 84ae36e7d..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP65Test.java +++ /dev/null @@ -1,55 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP65; -import nostr.base.Marker; -import nostr.base.Relay; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NIP65Test { - - @Test - public void testCreateRelayListMetadataEvent() { - Identity sender = Identity.generateRandomIdentity(); - NIP65 nip65 = new NIP65(sender); - Relay relay = new Relay("wss://relay"); - nip65.createRelayListMetadataEvent(List.of(relay), Marker.READ); - GenericEvent event = nip65.getEvent(); - assertEquals("r", event.getTags().get(0).getCode()); - assertTrue(event.getTags().get(0).toString().toUpperCase().contains(Marker.READ.name())); - } - - @Test - public void testCreateRelayListMetadataEventMapVariant() { - Identity sender = Identity.generateRandomIdentity(); - NIP65 nip65 = new NIP65(sender); - Relay r1 = new Relay("wss://relay1"); - Relay r2 = new Relay("wss://relay2"); - nip65.createRelayListMetadataEvent(Map.of(r1, Marker.READ, r2, Marker.WRITE)); - GenericEvent event = nip65.getEvent(); - assertEquals(nostr.base.Kind.RELAY_LIST_METADATA.getValue(), event.getKind()); - assertTrue(event.getTags().stream().anyMatch(t -> t.toString().contains("relay1"))); - assertTrue(event.getTags().stream().anyMatch(t -> t.toString().toUpperCase().contains(Marker.WRITE.name()))); - } - - @Test - public void testRelayTagOrderPreserved() { - Identity sender = Identity.generateRandomIdentity(); - NIP65 nip65 = new NIP65(sender); - Relay r1 = new Relay("wss://r1"); - Relay r2 = new Relay("wss://r2"); - nip65.createRelayListMetadataEvent(List.of(r1, r2)); - GenericEvent event = nip65.getEvent(); - String t0 = event.getTags().get(0).toString(); - String t1 = event.getTags().get(1).toString(); - assertTrue(t0.contains("wss://r1")); - assertTrue(t1.contains("wss://r2")); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP99ImplTest.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP99ImplTest.java deleted file mode 100644 index 4f24a8178..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP99ImplTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP99; -import nostr.event.BaseTag; -import nostr.event.entities.ClassifiedListing; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.PriceTag; -import nostr.id.Identity; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class NIP99ImplTest { - public static final String CONTENT = "ClassifiedListingEvent unit test content"; - public static final String UNIT_TEST_TITLE = "unit test title"; - public static final String UNIT_TEST_SUMMARY = "unit test summary"; - public static final String CURRENCY = "BTC"; - public static final String MONTH = "MONTH"; - public static final String LOCATION = "pangea"; - public static final PriceTag PRICE_TAG = new PriceTag(BigDecimal.valueOf(11111), CURRENCY, MONTH); - public static final Long PUBLISHED_AT = 1716513986268L; - static ClassifiedListing classifiedListing; - static Identity sender; - static NIP99 nip99; - - @BeforeAll - static void setup() { - classifiedListing = - ClassifiedListing.builder(UNIT_TEST_TITLE, UNIT_TEST_SUMMARY, PRICE_TAG).build(); - classifiedListing.setLocation(LOCATION); - classifiedListing.setPublishedAt(PUBLISHED_AT); - sender = Identity.generateRandomIdentity(); - nip99 = new NIP99(sender); - } - - @Test - void testNIP99CreateClassifiedListingEventWithAllOptionalParameters() { - System.out.println("testNIP99CreateClassifiedListingEventWithAllOptionalParameters"); - - List baseTags = new ArrayList(); - GenericEvent instance = - nip99.createClassifiedListingEvent(baseTags, CONTENT, classifiedListing).getEvent(); - // instance.update(); - - assertNotNull(instance.getId()); - assertTrue(instance.getTags().contains(containsGeneric("title", UNIT_TEST_TITLE))); - assertTrue(instance.getTags().contains(containsGeneric("summary", UNIT_TEST_SUMMARY))); - assertTrue( - instance.getTags().contains(containsGeneric("published_at", PUBLISHED_AT.toString()))); - assertTrue(instance.getTags().contains(containsGeneric("location", LOCATION))); - assertTrue(instance.getTags().contains(PRICE_TAG)); - - ClassifiedListing classifiedListing2 = - ClassifiedListing.builder(UNIT_TEST_TITLE, UNIT_TEST_SUMMARY, PRICE_TAG).build(); - classifiedListing2.setLocation(LOCATION); - classifiedListing2.setPublishedAt(PUBLISHED_AT); - GenericEvent instance2 = - nip99.createClassifiedListingEvent(baseTags, CONTENT, classifiedListing).getEvent(); - // instance.update(); - - assertNotNull(instance2.getId()); - assertEquals(instance.getPubKey(), instance2.getPubKey()); - assertEquals(instance.getKind(), instance2.getKind()); - assertEquals(instance.getTags(), instance2.getTags()); - assertEquals(instance.getContent(), instance2.getContent()); - } - - @Test - void testNIP99CreateClassifiedListingEventWithoutOptionalParameters() { - System.out.println("testNIP99CreateClassifiedListingEventWithoutOptionalParameters"); - - List baseTags = new ArrayList(); - - GenericEvent instance = - nip99.createClassifiedListingEvent(baseTags, CONTENT, classifiedListing).getEvent(); - // instance.update(); - - assertNotNull(instance.getId()); - } - - @Test - void testNIP99CreateClassifiedListingEventWithDuplicateParameters() { - System.out.println("testNIP99CreateClassifiedListingEventWithDuplicateParameters"); - - List baseTags = new ArrayList(); - - var nip99 = new NIP99(sender); - - classifiedListing.setLocation(LOCATION); - classifiedListing.setPublishedAt(PUBLISHED_AT); - - baseTags.add(BaseTag.create("published_at", String.valueOf(PUBLISHED_AT))); - GenericEvent instance = - nip99.createClassifiedListingEvent(baseTags, CONTENT, classifiedListing).getEvent(); - // instance.update(); - - assertNotNull(instance.getId()); - assertTrue(instance.getTags().contains(containsGeneric("location", LOCATION))); - assertTrue( - instance.getTags().contains(containsGeneric("published_at", PUBLISHED_AT.toString()))); - } - - @Test - void testNIP99CreateClassifiedListingEventNullParameters() { - System.out.println("testNIP99CreateClassifiedListingEventNullParameters"); - assertThrows( - NullPointerException.class, - () -> ClassifiedListing.builder(null, UNIT_TEST_SUMMARY, PRICE_TAG).build()); - assertThrows( - NullPointerException.class, - () -> ClassifiedListing.builder(UNIT_TEST_TITLE, null, PRICE_TAG).build()); - assertThrows( - NullPointerException.class, - () -> ClassifiedListing.builder(UNIT_TEST_TITLE, UNIT_TEST_SUMMARY, null).build()); - } - - private BaseTag containsGeneric(String key, String value) { - return BaseTag.create(key, value); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NIP99Test.java b/nostr-java-api/src/test/java/nostr/api/unit/NIP99Test.java deleted file mode 100644 index b8d8cb177..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NIP99Test.java +++ /dev/null @@ -1,89 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NIP99; -import nostr.base.Kind; -import nostr.config.Constants; -import nostr.event.BaseTag; -import nostr.event.entities.ClassifiedListing; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.PriceTag; -import nostr.id.Identity; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** Unit tests for NIP-99 classified listings (event building and required tags). */ -public class NIP99Test { - - @Test - // Builds a classified listing with title, summary, price and optional fields; verifies tags. - void createClassifiedListingEvent_withAllFields() throws MalformedURLException { - Identity sender = Identity.generateRandomIdentity(); - NIP99 nip99 = new NIP99(sender); - - PriceTag price = PriceTag.builder().number(new BigDecimal("19.99")).currency("USD").frequency("day").build(); - ClassifiedListing listing = - ClassifiedListing.builder("Desk", "Wooden desk", price) - .publishedAt(1700000000L) - .location("Seattle, WA") - .build(); - - BaseTag image = nostr.api.NIP23.createImageTag(new URL("https://example.com/image.jpg"), "800x600"); - List baseTags = List.of(image); - - GenericEvent event = - nip99.createClassifiedListingEvent(baseTags, "Solid oak.", listing).getEvent(); - - // Kind is classified listing - assertEquals(Kind.CLASSIFIED_LISTING.getValue(), event.getKind()); - - // Required NIP-23/NIP-99 tags present - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals(Constants.Tag.TITLE_CODE))); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals(Constants.Tag.SUMMARY_CODE))); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals(Constants.Tag.PRICE_CODE))); - - // Optional: published_at, location, image - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals(Constants.Tag.PUBLISHED_AT_CODE))); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals(Constants.Tag.LOCATION_CODE))); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals(Constants.Tag.IMAGE_CODE))); - - // Price content integrity - PriceTag priceTag = (PriceTag) event.getTags().stream() - .filter(t -> t instanceof PriceTag) - .findFirst() - .orElseThrow(); - assertEquals(new BigDecimal("19.99"), priceTag.getNumber()); - assertEquals("USD", priceTag.getCurrency()); - assertEquals("day", priceTag.getFrequency()); - } - - @Test - // Builds a minimal classified listing with title, summary, and price; verifies required tags only. - void createClassifiedListingEvent_minimal() { - Identity sender = Identity.generateRandomIdentity(); - NIP99 nip99 = new NIP99(sender); - - PriceTag price = PriceTag.builder().number(new BigDecimal("100")).currency("EUR").build(); - ClassifiedListing listing = ClassifiedListing.builder("Bike", "Used bike", price).build(); - - GenericEvent event = - nip99.createClassifiedListingEvent(List.of(), "Great condition", listing).getEvent(); - - // Kind - assertEquals(Kind.CLASSIFIED_LISTING.getValue(), event.getKind()); - // Required tags present - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals(Constants.Tag.TITLE_CODE))); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals(Constants.Tag.SUMMARY_CODE))); - assertTrue(event.getTags().stream().anyMatch(t -> t.getCode().equals(Constants.Tag.PRICE_CODE))); - // Optional tags absent - assertTrue(event.getTags().stream().noneMatch(t -> t.getCode().equals(Constants.Tag.PUBLISHED_AT_CODE))); - assertTrue(event.getTags().stream().noneMatch(t -> t.getCode().equals(Constants.Tag.LOCATION_CODE))); - } -} - diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientEventVerificationTest.java b/nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientEventVerificationTest.java deleted file mode 100644 index 365aa4b91..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientEventVerificationTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NostrSpringWebSocketClient; -import nostr.api.service.NoteService; -import nostr.base.ISignable; -import nostr.base.Kind; -import nostr.base.Signature; -import nostr.event.impl.GenericEvent; -import nostr.id.Identity; -import nostr.id.SigningException; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NostrSpringWebSocketClientEventVerificationTest { - - @Test - void sendEventThrowsWhenUnsigned() { - GenericEvent event = new GenericEvent(); - event.setPubKey(Identity.generateRandomIdentity().getPublicKey()); - event.setKind(Kind.TEXT_NOTE.getValue()); - event.setContent("test"); - - NoteService service = Mockito.mock(NoteService.class); - Mockito.when(service.send(Mockito.any(), Mockito.any())).thenReturn(List.of()); - NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(service); - assertThrows(IllegalStateException.class, () -> client.sendEvent(event)); - } - - @Test - void sendEventReturnsEmptyListWhenSigned() { - Identity identity = Identity.generateRandomIdentity(); - GenericEvent event = new GenericEvent(identity.getPublicKey(), Kind.TEXT_NOTE.getValue()); - event.setContent("signed"); - identity.sign(event); - - NoteService service = Mockito.mock(NoteService.class); - Mockito.when(service.send(Mockito.any(), Mockito.any())).thenReturn(List.of()); - NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(service); - List responses = client.sendEvent(event); - assertTrue(responses.isEmpty()); - } - - @Test - // Verifies that SigningException bubbles up from the client when signing fails - void signPropagatesSigningException() { - String invalidPriv = "0000000000000000000000000000000000000000000000000000000000000000"; - Identity identity = Identity.create(invalidPriv); - - ISignable signable = - new ISignable() { - private Signature signature; - - @Override - public Signature getSignature() { - return signature; - } - - @Override - public void setSignature(Signature signature) { - this.signature = signature; - } - - @Override - public Consumer getSignatureConsumer() { - return this::setSignature; - } - - @Override - public Supplier getByteArraySupplier() { - return () -> ByteBuffer.wrap("msg".getBytes(StandardCharsets.UTF_8)); - } - }; - - NoteService service = Mockito.mock(NoteService.class); - NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(service); - - assertThrows(SigningException.class, () -> client.sign(identity, signable)); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientTest.java b/nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientTest.java deleted file mode 100644 index 051d04619..000000000 --- a/nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package nostr.api.unit; - -import nostr.api.NostrSpringWebSocketClient; -import nostr.api.WebSocketClientHandler; -import org.junit.jupiter.api.Test; -import sun.misc.Unsafe; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; - -public class NostrSpringWebSocketClientTest { - - private static class TestClient extends NostrSpringWebSocketClient { - @Override - protected WebSocketClientHandler newWebSocketClientHandler(String relayName, nostr.base.RelayUri relayUri) { - try { - return createHandler(relayName, relayUri.toString()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - private static WebSocketClientHandler createHandler(String name, String uri) throws Exception { - Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); - theUnsafe.setAccessible(true); - Unsafe unsafe = (Unsafe) theUnsafe.get(null); - WebSocketClientHandler handler = - (WebSocketClientHandler) unsafe.allocateInstance(WebSocketClientHandler.class); - - Field relayName = WebSocketClientHandler.class.getDeclaredField("relayName"); - relayName.setAccessible(true); - relayName.set(handler, name); - - Field relayUri = WebSocketClientHandler.class.getDeclaredField("relayUri"); - relayUri.setAccessible(true); - relayUri.set(handler, new nostr.base.RelayUri(uri)); - - Field eventClient = WebSocketClientHandler.class.getDeclaredField("eventClient"); - eventClient.setAccessible(true); - eventClient.set(handler, null); - - Field requestClientMap = WebSocketClientHandler.class.getDeclaredField("requestClientMap"); - requestClientMap.setAccessible(true); - requestClientMap.set(handler, new ConcurrentHashMap<>()); - - return handler; - } - - @Test - void testMultipleSubscriptionsDoNotOverwriteHandlers() throws Exception { - NostrSpringWebSocketClient client = new TestClient(); - - Field registryField = NostrSpringWebSocketClient.class.getDeclaredField("relayRegistry"); - registryField.setAccessible(true); - nostr.api.client.NostrRelayRegistry registry = - (nostr.api.client.NostrRelayRegistry) registryField.get(client); - - @SuppressWarnings("unchecked") - Map map = registry.getClientMap(); - - map.put("relayA", createHandler("relayA", "ws://a")); - map.put("relayB", createHandler("relayB", "ws://b")); - - Method method = - nostr.api.client.NostrRelayRegistry.class.getDeclaredMethod( - "ensureRequestClients", nostr.base.SubscriptionId.class); - method.setAccessible(true); - - method.invoke(registry, nostr.base.SubscriptionId.of("sub1")); - assertEquals(4, map.size()); - WebSocketClientHandler handlerA1 = map.get("relayA:sub1"); - WebSocketClientHandler handlerB1 = map.get("relayB:sub1"); - assertNotNull(handlerA1); - assertNotNull(handlerB1); - - method.invoke(registry, nostr.base.SubscriptionId.of("sub2")); - assertEquals(6, map.size()); - assertSame(handlerA1, map.get("relayA:sub1")); - assertSame(handlerB1, map.get("relayB:sub1")); - assertNotNull(map.get("relayA:sub2")); - assertNotNull(map.get("relayB:sub2")); - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/util/CommonTestObjectsFactory.java b/nostr-java-api/src/test/java/nostr/api/util/CommonTestObjectsFactory.java deleted file mode 100644 index 1490bb01a..000000000 --- a/nostr-java-api/src/test/java/nostr/api/util/CommonTestObjectsFactory.java +++ /dev/null @@ -1,151 +0,0 @@ -package nostr.api.util; - -import lombok.Getter; -import nostr.api.NIP01; -import nostr.api.NIP99; -import nostr.event.BaseTag; -import nostr.event.entities.ClassifiedListing; -import nostr.event.impl.GenericEvent; -import nostr.event.impl.TextNoteEvent; -import nostr.event.tag.EventTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.PriceTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.SubjectTag; -import nostr.id.Identity; -import nostr.util.NostrException; -import org.apache.commons.lang3.RandomStringUtils; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Random; -import java.util.UUID; - -public class CommonTestObjectsFactory { - - public static Identity createNewIdentity() { - return Identity.generateRandomIdentity(); - } - - public static T createTextNoteEvent( - Identity identity, List tags, String content) throws NostrException { - NIP01 nip01 = new NIP01(identity); - GenericEvent genericEvent = nip01.createTextNoteEvent(tags, content).getEvent(); - TextNoteEvent textNoteEvent = GenericEvent.convert(genericEvent, TextNoteEvent.class); - return (T) textNoteEvent; - } - - public static T createClassifiedListingEvent( - Identity identity, List tags, String content, ClassifiedListing cl) { - - NIP99 nip99 = new NIP99(identity); - return (T) nip99.createClassifiedListingEvent(tags, content, cl).getEvent(); - } - - public static GenericEvent createGenericEvent() { - String concat = generateRandomHex64String(); - return new GenericEvent(concat.substring(0, 64)); - } - - public static SubjectTag createSubjectTag(Class clazz) { - return new SubjectTag(clazz.getName() + " Subject Tag"); - } - - public static PubKeyTag createPubKeyTag(Identity identity) { - return new PubKeyTag(identity.getPublicKey()); - } - - public static GeohashTag createGeohashTag(Class clazz) { - return new GeohashTag(clazz.getName() + " Geohash Tag"); - } - - public static HashtagTag createHashtagTag(Class clazz) { - return new HashtagTag(clazz.getName() + " Hashtag Tag"); - } - - public static EventTag createEventTag(Class clazz) { - return new EventTag(createGenericEvent().getId()); - } - - public static PriceTag createPriceTag() { - PriceComposite pc = new PriceComposite(); - BigDecimal NUMBER = pc.getPrice(); - String CURRENCY = pc.getCurrency(); - String FREQUENCY = pc.getFrequency(); - return new PriceTag(NUMBER, CURRENCY, FREQUENCY); - } - - public static ClassifiedListing createClassifiedListing(String title, String summary) { - return new ClassifiedListingComposite(title, summary, createPriceTag()).getClassifiedListing(); - } - - public static String lorumIpsum() { - return lorumIpsum(CommonTestObjectsFactory.class); - } - - public static String lorumIpsum(Class clazz) { - return lorumIpsum(clazz, 64); - } - - public static String lorumIpsum(Class clazz, int length) { - return lorumIpsum(clazz.getSimpleName(), length); - } - - public static String lorumIpsum(String s, int length) { - boolean useLetters = false; - boolean useNumbers = true; - return cullStringLength( - String.join("-", s, generateRandomAlphaNumericString(length, useLetters, useNumbers)), 64); - } - - public static String lnUrl() { - // lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp - // match lnUrl string length of 84 - return cullStringLength("lnurl" + generateRandomHex64String(), 84); - } - - private static String cullStringLength(String s, int x) { - return s.length() > x ? s.substring(0, x) : s; - } - - private static String generateRandomAlphaNumericString( - int length, boolean useLetters, boolean useNumbers) { - return RandomStringUtils.random(length, useLetters, useNumbers); - } - - public static String generateRandomHex64String() { - return UUID.randomUUID() - .toString() - .concat(UUID.randomUUID().toString()) - .replaceAll("[^A-Za-z0-9]", ""); - } - - public static BigDecimal createRandomBigDecimal() { - Random rand = new Random(); - int max = 100, min = 50; - int i = rand.nextInt(max - min + 1) + min; - int j = (rand.nextInt(max - min + 1) + min); - return new BigDecimal(String.valueOf(i) + '.' + j); - } - - @Getter - public static class PriceComposite { - private final String currency = "BTC"; - private final String frequency = "nanosecond"; - private final BigDecimal price; - - private PriceComposite() { - price = createRandomBigDecimal(); - } - } - - @Getter - public static class ClassifiedListingComposite { - private final ClassifiedListing classifiedListing; - - private ClassifiedListingComposite(String title, String summary, PriceTag priceTag) { - this.classifiedListing = ClassifiedListing.builder(title, summary, priceTag).build(); - } - } -} diff --git a/nostr-java-api/src/test/java/nostr/api/util/JsonComparator.java b/nostr-java-api/src/test/java/nostr/api/util/JsonComparator.java deleted file mode 100644 index fb92beede..000000000 --- a/nostr-java-api/src/test/java/nostr/api/util/JsonComparator.java +++ /dev/null @@ -1,117 +0,0 @@ -package nostr.api.util; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.JsonNodeType; -import com.google.common.collect.Sets; - -import java.util.Collection; -import java.util.Comparator; -import java.util.Optional; -import java.util.Spliterator; - -import static java.util.Spliterators.spliteratorUnknownSize; -import static java.util.stream.StreamSupport.stream; - -public class JsonComparator implements Comparator> { - - private boolean ignoreElementOrderInArrays = true; - - public static boolean isEquivalentJson(JsonNode target, JsonNode other) { - return new JsonComparator().compare(target, other) == 0; - } - - @Override - public int compare(Iterable o1, Iterable o2) { - if (o1 == null || o2 == null) { - return -1; - } - if (o1 == o2) { - return 0; - } - if (o1 instanceof JsonNode && o2 instanceof JsonNode) { - return compareJsonNodes((JsonNode) o1, (JsonNode) o2); - } - return -1; - } - - private int compareJsonNodes(JsonNode o1, JsonNode o2) { - if (o1 == null || o2 == null) { - return -1; - } - if (o1 == o2) { - return 0; - } - if (!o1.getNodeType().equals(o2.getNodeType())) { - return -1; - } - switch (o1.getNodeType()) { - case NULL: - return o2.isNull() ? 0 : -1; - case BOOLEAN: - return o1.asBoolean() == o2.asBoolean() ? 0 : -1; - case STRING: - return o1.asText().equals(o2.asText()) ? 0 : -1; - case NUMBER: - double double1 = o1.asDouble(); - double double2 = o2.asDouble(); - return Math.abs(double1 - double2) / Math.max(double1, double2) < 0.999 ? 0 : -1; - case OBJECT: - // ignores fields with null value that are missing at other JSON - var missingNotNullFields = - Sets.symmetricDifference( - Sets.newHashSet(o1.fieldNames()), Sets.newHashSet(o2.fieldNames())) - .stream() - .filter(missingField -> isNotNull(o1, missingField) || isNotNull(o2, missingField)) - .toList(); - if (!missingNotNullFields.isEmpty()) { - return -1; - } - Integer reduce1 = - stream(spliteratorUnknownSize(o1.fieldNames(), Spliterator.ORDERED), false) - .map(key -> compareJsonNodes(o1.get(key), o2.get(key))) - .reduce(0, (a, b) -> a == -1 || b == -1 ? -1 : 0); - return reduce1; - case ARRAY: - if (o1.size() != o2.size()) { - return -1; - } - if (o1.isEmpty()) { - return 0; - } - var o1Iterator = o1.elements(); - var o2Iterator = o2.elements(); - var o2Elements = Sets.newHashSet(o2.elements()); - Integer reduce = - stream(spliteratorUnknownSize(o1Iterator, Spliterator.ORDERED), false) - .map( - o1Next -> - ignoreElementOrderInArrays - ? lookForMatchingElement(o1Next, o2Elements) - : compareJsonNodes(o1Next, o2Iterator.next())) - .reduce(0, (a, b) -> a == -1 || b == -1 ? -1 : 0); - return reduce; - case MISSING: - case BINARY: - case POJO: - default: - return -1; - } - } - - private int lookForMatchingElement( - JsonNode elementToLookFor, Collection collectionOfElements) { - // Note: O(n^2) complexity - return collectionOfElements.stream() - .filter(o2Element -> compareJsonNodes(elementToLookFor, o2Element) == 0) - .findFirst() - .map(o2Element -> 0) - .orElse(-1); - } - - private static boolean isNotNull(JsonNode jsonObject, String fieldName) { - return Optional.ofNullable(jsonObject.get(fieldName)) - .map(JsonNode::getNodeType) - .filter(nodeType -> nodeType != JsonNodeType.NULL) - .isPresent(); - } -} diff --git a/nostr-java-api/src/test/resources/application-test.properties b/nostr-java-api/src/test/resources/application-test.properties deleted file mode 100644 index 5c1a515fb..000000000 --- a/nostr-java-api/src/test/resources/application-test.properties +++ /dev/null @@ -1,4 +0,0 @@ -spring.threads.virtual.enabled=true - -logging.level.nostr.api=INFO -logging.pattern.console=%msg%n diff --git a/nostr-java-api/src/test/resources/junit-platform.properties b/nostr-java-api/src/test/resources/junit-platform.properties deleted file mode 100644 index beae0cf29..000000000 --- a/nostr-java-api/src/test/resources/junit-platform.properties +++ /dev/null @@ -1,9 +0,0 @@ -# junit-platform.properties - -junit.jupiter.execution.parallel.enabled=true -junit.jupiter.execution.parallel.config.strategy=dynamic -junit.jupiter.execution.parallel.mode.default=same_thread -#junit.jupiter.execution.parallel.mode.default=concurrent -#junit.jupiter.execution.parallel.mode.classes.default=concurrent - -#junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$ClassName diff --git a/nostr-java-api/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/nostr-java-api/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index fdbd0b157..000000000 --- a/nostr-java-api/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-subclass diff --git a/nostr-java-api/src/test/resources/relay-container.properties b/nostr-java-api/src/test/resources/relay-container.properties deleted file mode 100644 index 93627a11b..000000000 --- a/nostr-java-api/src/test/resources/relay-container.properties +++ /dev/null @@ -1,2 +0,0 @@ -relay.container.image=dockurr/strfry:latest -relay.container.port=7777 \ No newline at end of file diff --git a/nostr-java-api/src/test/resources/strfry.conf b/nostr-java-api/src/test/resources/strfry.conf deleted file mode 100644 index 8657ddbd7..000000000 --- a/nostr-java-api/src/test/resources/strfry.conf +++ /dev/null @@ -1,75 +0,0 @@ -## -## strfry config for integration testing (no whitelist) -## - -db = "./strfry-db/" - -dbParams { - maxreaders = 256 - mapsize = 10995116277760 - noReadAhead = false -} - -events { - maxEventSize = 65536 - rejectEventsNewerThanSeconds = 900 - rejectEventsOlderThanSeconds = 94608000 - rejectEphemeralEventsOlderThanSeconds = 60 - ephemeralEventsLifetimeSeconds = 300 - maxNumTags = 2000 - maxTagValSize = 1024 -} - -relay { - bind = "0.0.0.0" - port = 7777 - nofiles = 1000000 - realIpHeader = "" - - info { - name = "nostr-java test relay" - description = "strfry relay for nostr-java integration tests" - pubkey = "" - contact = "" - icon = "" - nips = "" - } - - maxWebsocketPayloadSize = 131072 - maxReqFilterSize = 200 - autoPingSeconds = 55 - enableTcpKeepalive = false - queryTimesliceBudgetMicroseconds = 10000 - maxFilterLimit = 500 - maxSubsPerConnection = 20 - - writePolicy { - # No write policy plugin - accept all events - plugin = "" - } - - compression { - enabled = true - slidingWindow = true - } - - logging { - dumpInAll = false - dumpInEvents = false - dumpInReqs = false - dbScanPerf = false - invalidEvents = true - } - - numThreads { - ingester = 3 - reqWorker = 3 - reqMonitor = 3 - negentropy = 2 - } - - negentropy { - enabled = true - maxSyncEvents = 1000000 - } -} diff --git a/nostr-java-base/src/main/java/nostr/base/ElementAttribute.java b/nostr-java-base/src/main/java/nostr/base/ElementAttribute.java deleted file mode 100644 index fac7dc1d0..000000000 --- a/nostr-java-base/src/main/java/nostr/base/ElementAttribute.java +++ /dev/null @@ -1,11 +0,0 @@ -package nostr.base; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * @author squirrel - */ -public record ElementAttribute( - @JsonProperty @JsonInclude(JsonInclude.Include.NON_NULL) String name, - @JsonProperty Object value) {} diff --git a/nostr-java-base/src/main/java/nostr/base/GenericTagQuery.java b/nostr-java-base/src/main/java/nostr/base/GenericTagQuery.java deleted file mode 100644 index 6f88c960f..000000000 --- a/nostr-java-base/src/main/java/nostr/base/GenericTagQuery.java +++ /dev/null @@ -1,6 +0,0 @@ -package nostr.base; - -/** - * @author squirrel - */ -public record GenericTagQuery(String tagName, String value) implements IElement {} diff --git a/nostr-java-base/src/main/java/nostr/base/IBech32Encodable.java b/nostr-java-base/src/main/java/nostr/base/IBech32Encodable.java deleted file mode 100644 index b5597449e..000000000 --- a/nostr-java-base/src/main/java/nostr/base/IBech32Encodable.java +++ /dev/null @@ -1,9 +0,0 @@ -package nostr.base; - -/** - * @author squirrel - */ -public interface IBech32Encodable { - - String toBech32(); -} diff --git a/nostr-java-base/src/main/java/nostr/base/IElement.java b/nostr-java-base/src/main/java/nostr/base/IElement.java deleted file mode 100644 index 30ada9277..000000000 --- a/nostr-java-base/src/main/java/nostr/base/IElement.java +++ /dev/null @@ -1,11 +0,0 @@ -package nostr.base; - -/** - * @author squirrel - */ -public interface IElement { - - default String getNip() { - return "1"; - } -} diff --git a/nostr-java-base/src/main/java/nostr/base/IEvent.java b/nostr-java-base/src/main/java/nostr/base/IEvent.java deleted file mode 100644 index 23d3f8f6b..000000000 --- a/nostr-java-base/src/main/java/nostr/base/IEvent.java +++ /dev/null @@ -1,8 +0,0 @@ -package nostr.base; - -/** - * @author squirrel - */ -public interface IEvent extends IElement, IBech32Encodable { - String getId(); -} diff --git a/nostr-java-base/src/main/java/nostr/base/IGenericElement.java b/nostr-java-base/src/main/java/nostr/base/IGenericElement.java deleted file mode 100644 index 5a734d03f..000000000 --- a/nostr-java-base/src/main/java/nostr/base/IGenericElement.java +++ /dev/null @@ -1,11 +0,0 @@ -package nostr.base; - -import java.util.List; - -public interface IGenericElement extends IElement { - List getAttributes(); - - void addAttribute(ElementAttribute... attribute); - - void addAttributes(List attributes); -} diff --git a/nostr-java-base/src/main/java/nostr/base/IKey.java b/nostr-java-base/src/main/java/nostr/base/IKey.java deleted file mode 100644 index 1db0f1984..000000000 --- a/nostr-java-base/src/main/java/nostr/base/IKey.java +++ /dev/null @@ -1,13 +0,0 @@ -package nostr.base; - -import java.io.Serializable; - -/** - * @author squirrel - */ -public interface IKey extends Serializable { - - byte[] getRawData(); - - String toBech32String(); -} diff --git a/nostr-java-base/src/main/java/nostr/base/ITag.java b/nostr-java-base/src/main/java/nostr/base/ITag.java deleted file mode 100644 index d2097e2ae..000000000 --- a/nostr-java-base/src/main/java/nostr/base/ITag.java +++ /dev/null @@ -1,11 +0,0 @@ -package nostr.base; - -/** - * @author squirrel - */ -public interface ITag extends IElement { - - void setParent(IEvent event); - - String getCode(); -} diff --git a/nostr-java-base/src/main/java/nostr/base/Kind.java b/nostr-java-base/src/main/java/nostr/base/Kind.java deleted file mode 100644 index 67c2ab7ba..000000000 --- a/nostr-java-base/src/main/java/nostr/base/Kind.java +++ /dev/null @@ -1,135 +0,0 @@ -package nostr.base; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.time.temporal.ValueRange; -import java.util.Optional; - -/** - * @author squirrel - */ -@AllArgsConstructor -@Getter -public enum Kind { - SET_METADATA(0, "set_metadata"), - TEXT_NOTE(1, "text_note"), - RECOMMEND_SERVER(2, "recommend_server"), - COINJOIN_POOL(2022, "coinjoin_pool"), - REACTION_TO_WEBSITE(17, "reaction_to_website"), - CONTACT_LIST(3, "contact_list"), - ENCRYPTED_DIRECT_MESSAGE(4, "encrypted_direct_message"), - DELETION(5, "deletion"), - REPOST(6, "repost"), - REACTION(7, "reaction"), - REPORT(1984, "report"), - CHANNEL_CREATE(40, "channel_create"), - CHANNEL_METADATA(41, "channel_metadata"), - CHANNEL_MESSAGE(42, "channel_message"), - HIDE_MESSAGE(43, "hide_message"), - MUTE_USER(44, "mute_user"), - OTS_EVENT(1040, "ots_event"), - RESERVED_CASHU_WALLET_TOKENS(7_374, "reserved_cashu_wallet_tokens"), - WALLET(17_375, "wallet"), - WALLET_UNSPENT_PROOF(7_375, "wallet_unspent_proof"), - WALLET_TX_HISTORY(7_376, "wallet_tx_history"), - ZAP_REQUEST(9734, "zap_request"), - ZAP_RECEIPT(9735, "zap_receipt"), - BADGE_DEFINITION(30_008, "badge_definition"), - BADGE_AWARD(30_009, "badge_award"), - REPLACEABLE_EVENT(10_000, "replaceable_event"), - EPHEMEREAL_EVENT(20_000, "ephemereal_event"), - ADDRESSABLE_EVENT(30_000, "addressable_event"), - PIN_LIST(10_001, "pin_list"), - CLIENT_AUTH(22_242, "authentication_of_clients_to_relays"), - STALL_CREATE_OR_UPDATE(30_017, "create_or_update_stall"), - PRODUCT_CREATE_OR_UPDATE(30_018, "create_or_update_product"), - LONG_FORM_TEXT_NOTE(30_023, "long_form_text_note"), - LONG_FORM_DRAFT(30_024, "long_form_draft"), - APPLICATION_SPECIFIC_DATA(30_078, "application_specific_data"), - CLASSIFIED_LISTING(30_402, "classified_listing_active"), - CLASSIFIED_LISTING_INACTIVE(30_403, "classified_listing_inactive"), - CLASSIFIED_LISTING_DRAFT(30_403, "classified_listing_draft"), - CALENDAR_DATE_BASED_EVENT(31_922, "calendar_date_based_event"), - CALENDAR_TIME_BASED_EVENT(31_923, "calendar_time_based_event"), - CALENDAR_EVENT(31_924, "calendar_event"), - CALENDAR_RSVP_EVENT(31_925, "calendar_rsvp_event"), - NUTZAP_INFORMATIONAL(10_019, "nutzap_informational"), - NUTZAP(9_321, "nutzap"), - RELAY_LIST_METADATA(10_002, "relay_list_metadata"), - NOSTR_CONNECT(24_133, "nostr_connect"); - - @JsonValue private final int value; - - private final String name; - - /** - * Returns the Kind enum constant for the given integer value. - * - *

This method is used by Jackson for JSON deserialization. For unknown kind values - * (valid range but not defined in this enum), it returns {@code null} to allow handling - * of custom or future NIP kinds that aren't yet defined in this library. - * - *

For strict validation that throws on unknown kinds, use {@link #valueOfStrict(int)}. - * For a safer Optional-based lookup, use {@link #findByValue(int)}. - * - * @param value the kind integer value (must be between 0 and 65535) - * @return the Kind enum constant, or {@code null} if the value is valid but not defined - * @throws IllegalArgumentException if the value is outside the valid range (0-65535) - */ - @JsonCreator - public static Kind valueOf(int value) { - if (!ValueRange.of(0, 65_535).isValidIntValue(value)) { - throw new IllegalArgumentException( - String.format("Kind must be between 0 and 65535 but was [%d]", value)); - } - for (Kind k : values()) { - if (k.getValue() == value) { - return k; - } - } - return null; - } - - /** - * Returns the Kind enum constant for the given integer value, throwing if not found. - * - *

Use this method when you require a known Kind and want to fail fast on unknown values. - * For lenient handling of custom kinds, use {@link #valueOf(int)} or {@link #findByValue(int)}. - * - * @param value the kind integer value (must be between 0 and 65535) - * @return the Kind enum constant - * @throws IllegalArgumentException if the value is outside the valid range or unknown - */ - public static Kind valueOfStrict(int value) { - Kind kind = valueOf(value); - if (kind == null) { - throw new IllegalArgumentException( - String.format("Unknown kind value: %d. Use valueOf() for lenient handling or add it to the Kind enum.", value)); - } - return kind; - } - - /** - * Safely looks up a Kind by its integer value, returning an Optional. - * - *

This is the recommended method for handling potentially unknown kinds, as it makes - * the possibility of unknown values explicit in the API. - * - * @param value the kind integer value - * @return an Optional containing the Kind if found, or empty if unknown or out of range - */ - public static Optional findByValue(int value) { - if (!ValueRange.of(0, 65_535).isValidIntValue(value)) { - return Optional.empty(); - } - return Optional.ofNullable(valueOf(value)); - } - - @Override - public String toString() { - return Integer.toString(value); - } -} diff --git a/nostr-java-base/src/main/java/nostr/base/Marker.java b/nostr-java-base/src/main/java/nostr/base/Marker.java deleted file mode 100644 index 0786826dd..000000000 --- a/nostr-java-base/src/main/java/nostr/base/Marker.java +++ /dev/null @@ -1,29 +0,0 @@ -package nostr.base; - -import com.fasterxml.jackson.annotation.JsonValue; - -/** - * @author squirrel - */ -public enum Marker { - ROOT("root"), - REPLY("reply"), - MENTION("mention"), - FORK("fork"), - CREATED("created"), - DESTROYED("destroyed"), - REDEEMED("redeemed"), - READ("read"), - WRITE("write"); - - private final String value; - - Marker(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } -} diff --git a/nostr-java-base/src/main/java/nostr/base/RelayUri.java b/nostr-java-base/src/main/java/nostr/base/RelayUri.java deleted file mode 100644 index 167b21139..000000000 --- a/nostr-java-base/src/main/java/nostr/base/RelayUri.java +++ /dev/null @@ -1,41 +0,0 @@ -package nostr.base; - -import lombok.EqualsAndHashCode; -import lombok.NonNull; - -import java.net.URI; - -/** - * Value object that encapsulates validation of relay URIs. - */ -@EqualsAndHashCode -public final class RelayUri { - - private final String value; - - public RelayUri(@NonNull String value) { - try { - URI uri = URI.create(value); - String scheme = uri.getScheme(); - if (!("ws".equalsIgnoreCase(scheme) || "wss".equalsIgnoreCase(scheme))) { - throw new IllegalArgumentException("Relay URI must use ws or wss scheme"); - } - } catch (IllegalArgumentException ex) { - throw new IllegalArgumentException("Invalid relay URI: " + value, ex); - } - this.value = value; - } - - public String value() { - return value; - } - - public URI toUri() { - return URI.create(value); - } - - @Override - public String toString() { - return value; - } -} diff --git a/nostr-java-base/src/main/java/nostr/base/annotation/Event.java b/nostr-java-base/src/main/java/nostr/base/annotation/Event.java deleted file mode 100644 index ec0671c2d..000000000 --- a/nostr-java-base/src/main/java/nostr/base/annotation/Event.java +++ /dev/null @@ -1,18 +0,0 @@ -package nostr.base.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author squirrel - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface Event { - - String name(); - - int nip() default 1; -} diff --git a/nostr-java-base/src/main/java/nostr/base/annotation/Key.java b/nostr-java-base/src/main/java/nostr/base/annotation/Key.java deleted file mode 100644 index 232d94f5e..000000000 --- a/nostr-java-base/src/main/java/nostr/base/annotation/Key.java +++ /dev/null @@ -1,17 +0,0 @@ -package nostr.base.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author squirrel - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Key { - String name() default ""; - - int nip() default 1; -} diff --git a/nostr-java-base/src/main/java/nostr/base/annotation/Tag.java b/nostr-java-base/src/main/java/nostr/base/annotation/Tag.java deleted file mode 100644 index c83d586f6..000000000 --- a/nostr-java-base/src/main/java/nostr/base/annotation/Tag.java +++ /dev/null @@ -1,20 +0,0 @@ -package nostr.base.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author squirrel - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface Tag { - - String code(); - - String name() default ""; - - int nip() default 1; -} diff --git a/nostr-java-base/src/test/java/nostr/base/KindTest.java b/nostr-java-base/src/test/java/nostr/base/KindTest.java deleted file mode 100644 index 78d808e95..000000000 --- a/nostr-java-base/src/test/java/nostr/base/KindTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package nostr.base; - -import org.junit.jupiter.api.Test; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class KindTest { - - @Test - void testValueOfValid() { - Kind kind = Kind.valueOf(Kind.TEXT_NOTE.getValue()); - assertEquals(Kind.TEXT_NOTE, kind); - assertEquals(Integer.toString(Kind.TEXT_NOTE.getValue()), kind.toString()); - } - - @Test - void testValueOfUnknownReturnsNull() { - Kind kind = Kind.valueOf(999); - assertNull(kind, "Unknown kind values should return null for lenient handling"); - } - - @Test - void testValueOfInvalidRange() { - assertThrows(IllegalArgumentException.class, () -> Kind.valueOf(70_000)); - } - - @Test - void testValueOfStrictValid() { - Kind kind = Kind.valueOfStrict(Kind.REACTION.getValue()); - assertEquals(Kind.REACTION, kind); - } - - @Test - void testValueOfStrictUnknownThrows() { - IllegalArgumentException ex = assertThrows( - IllegalArgumentException.class, - () -> Kind.valueOfStrict(999) - ); - assertTrue(ex.getMessage().contains("999")); - } - - @Test - void testValueOfStrictInvalidRangeThrows() { - assertThrows(IllegalArgumentException.class, () -> Kind.valueOfStrict(70_000)); - } - - @Test - void testFindByValueValid() { - Optional kind = Kind.findByValue(Kind.ZAP_RECEIPT.getValue()); - assertTrue(kind.isPresent()); - assertEquals(Kind.ZAP_RECEIPT, kind.get()); - } - - @Test - void testFindByValueUnknownReturnsEmpty() { - Optional kind = Kind.findByValue(999); - assertTrue(kind.isEmpty(), "Unknown kind should return empty Optional"); - } - - @Test - void testFindByValueInvalidRangeReturnsEmpty() { - Optional kind = Kind.findByValue(70_000); - assertTrue(kind.isEmpty(), "Out of range kind should return empty Optional"); - } - - @Test - void testEnumValues() { - for (Kind k : Kind.values()) { - assertNotNull(k); - } - } -} diff --git a/nostr-java-base/src/test/java/nostr/base/MarkerTest.java b/nostr-java-base/src/test/java/nostr/base/MarkerTest.java deleted file mode 100644 index 9ff07177c..000000000 --- a/nostr-java-base/src/test/java/nostr/base/MarkerTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package nostr.base; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -class MarkerTest { - - @Test - void testGetValue() { - for (Marker m : Marker.values()) { - assertNotNull(m.getValue()); - assertFalse(m.getValue().isEmpty()); - } - } -} diff --git a/nostr-java-base/src/test/java/nostr/base/RelayUriTest.java b/nostr-java-base/src/test/java/nostr/base/RelayUriTest.java deleted file mode 100644 index e78c55e0a..000000000 --- a/nostr-java-base/src/test/java/nostr/base/RelayUriTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package nostr.base; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class RelayUriTest { - // Accept only ws/wss schemes. - @Test - void validSchemes() { - assertDoesNotThrow(() -> new RelayUri("ws://example")); - assertDoesNotThrow(() -> new RelayUri("wss://example")); - } - - // Reject non-websocket schemes. - @Test - void invalidScheme() { - assertThrows(IllegalArgumentException.class, () -> new RelayUri("http://example")); - } -} - diff --git a/nostr-java-client/pom.xml b/nostr-java-client/pom.xml index c877ad422..143ef9ba1 100644 --- a/nostr-java-client/pom.xml +++ b/nostr-java-client/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 1.3.0 + 2.0.0 ../pom.xml @@ -27,7 +27,7 @@ ${project.groupId} - nostr-java-id + nostr-java-identity diff --git a/nostr-java-client/src/main/java/nostr/client/WebSocketClientFactory.java b/nostr-java-client/src/main/java/nostr/client/WebSocketClientFactory.java deleted file mode 100644 index e82fc7cda..000000000 --- a/nostr-java-client/src/main/java/nostr/client/WebSocketClientFactory.java +++ /dev/null @@ -1,15 +0,0 @@ -package nostr.client; - -import nostr.base.RelayUri; -import nostr.client.springwebsocket.WebSocketClientIF; - -import java.util.concurrent.ExecutionException; - -/** - * Abstraction for creating WebSocket clients for relay URIs. - */ -@FunctionalInterface -public interface WebSocketClientFactory { - - WebSocketClientIF create(RelayUri relayUri) throws ExecutionException, InterruptedException; -} diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/ConnectionState.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/ConnectionState.java new file mode 100644 index 000000000..458c2fdbb --- /dev/null +++ b/nostr-java-client/src/main/java/nostr/client/springwebsocket/ConnectionState.java @@ -0,0 +1,11 @@ +package nostr.client.springwebsocket; + +/** + * Connection states for a WebSocket relay connection. + */ +public enum ConnectionState { + CONNECTING, + CONNECTED, + RECONNECTING, + CLOSED +} diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java new file mode 100644 index 000000000..45a1c7150 --- /dev/null +++ b/nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java @@ -0,0 +1,633 @@ +package nostr.client.springwebsocket; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import nostr.event.BaseMessage; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Scope; +import org.springframework.retry.annotation.Recover; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketHttpHeaders; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.WebSocketContainer; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; + +/** + * WebSocket client for Nostr relay communication. + * + *

This client uses {@link CompletableFuture} for response waiting, providing instant + * notification when responses arrive instead of polling. Send and subscribe operations + * are retried automatically on transient {@link IOException} failures. + */ +@Component +@Scope(BeanDefinition.SCOPE_PROTOTYPE) +@Slf4j +public class NostrRelayClient extends TextWebSocketHandler implements AutoCloseable { + private static final long DEFAULT_AWAIT_TIMEOUT_MS = 60000L; + private static final long DEFAULT_MAX_IDLE_TIMEOUT_MS = 3600000L; + private static final int DEFAULT_MAX_TEXT_MESSAGE_BUFFER_SIZE = 1048576; + private static final int DEFAULT_MAX_BINARY_MESSAGE_BUFFER_SIZE = 1048576; + private static final int DEFAULT_MAX_EVENTS_PER_REQUEST = 10_000; + private static final ThreadFactory RELAY_IO_THREAD_FACTORY = + Thread.ofVirtual().name("nostr-relay-io-", 0).factory(); + private static final ThreadFactory LISTENER_THREAD_FACTORY = + Thread.ofVirtual().name("nostr-relay-listener-", 0).factory(); + private static final Executor RELAY_IO_EXECUTOR = + command -> RELAY_IO_THREAD_FACTORY.newThread(command).start(); + private static final Executor LISTENER_EXECUTOR = + command -> LISTENER_THREAD_FACTORY.newThread(command).start(); + + @Value("${nostr.websocket.await-timeout-ms:60000}") + private long awaitTimeoutMs; + + @Value("${nostr.websocket.max-idle-timeout-ms:3600000}") + private long maxIdleTimeoutMs; + + @Value("${nostr.websocket.max-events-per-request:10000}") + private int maxEventsPerRequest = DEFAULT_MAX_EVENTS_PER_REQUEST; + + private final WebSocketSession clientSession; + private final ReentrantLock sendLock = new ReentrantLock(); + private PendingRequest pendingRequest; + private final Map listeners = new ConcurrentHashMap<>(); + private final AtomicReference connectionState = + new AtomicReference<>(ConnectionState.CONNECTING); + + private static final class PendingRequest { + private final CompletableFuture> future = new CompletableFuture<>(); + private final List events = new ArrayList<>(); + private final int maxEvents; + + PendingRequest(int maxEvents) { + this.maxEvents = maxEvents; + } + + void addEvent(String event) { + if (events.size() < maxEvents) { + events.add(event); + } + } + + void complete() { + future.complete(List.copyOf(events)); + } + + void completeExceptionally(Throwable ex) { + future.completeExceptionally(ex); + } + + boolean isDone() { + return future.isDone(); + } + + CompletableFuture> getFuture() { + return future; + } + + int eventCount() { + return events.size(); + } + } + + public NostrRelayClient(@Value("${nostr.relay.uri}") String relayUri) + throws java.util.concurrent.ExecutionException, InterruptedException { + this.clientSession = connectSession(relayUri); + connectionState.set(ConnectionState.CONNECTED); + } + + public NostrRelayClient(String relayUri, long awaitTimeoutMs) + throws java.util.concurrent.ExecutionException, InterruptedException { + if (awaitTimeoutMs <= 0) { + throw new IllegalArgumentException("awaitTimeoutMs must be positive"); + } + this.awaitTimeoutMs = awaitTimeoutMs; + log.info("NostrRelayClient created for {} with awaitTimeoutMs={}", relayUri, awaitTimeoutMs); + this.clientSession = connectSession(relayUri); + connectionState.set(ConnectionState.CONNECTED); + } + + NostrRelayClient(WebSocketSession clientSession, long awaitTimeoutMs) { + if (clientSession == null) { + throw new NullPointerException("clientSession must not be null"); + } + if (awaitTimeoutMs <= 0) { + throw new IllegalArgumentException("awaitTimeoutMs must be positive"); + } + this.clientSession = clientSession; + this.awaitTimeoutMs = awaitTimeoutMs; + connectionState.set(ConnectionState.CONNECTED); + } + + /** + * Connect to a relay asynchronously on a Virtual Thread. + * + * @param relayUri relay WebSocket URI + * @return future that completes with a connected client + */ + public static CompletableFuture connectAsync(@NonNull String relayUri) { + return connectAsync(relayUri, DEFAULT_AWAIT_TIMEOUT_MS); + } + + /** + * Connect to a relay asynchronously on a Virtual Thread using a custom send timeout. + * + * @param relayUri relay WebSocket URI + * @param awaitTimeoutMs timeout for blocking send operations + * @return future that completes with a connected client + */ + public static CompletableFuture connectAsync( + @NonNull String relayUri, long awaitTimeoutMs) { + Objects.requireNonNull(relayUri, "relayUri"); + if (awaitTimeoutMs <= 0) { + throw new IllegalArgumentException("awaitTimeoutMs must be positive"); + } + return CompletableFuture.supplyAsync( + () -> { + try { + return new NostrRelayClient(relayUri, awaitTimeoutMs); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new CompletionException( + new IOException("Interrupted while connecting to relay " + relayUri, ex)); + } catch (ExecutionException ex) { + Throwable cause = ex.getCause() != null ? ex.getCause() : ex; + throw new CompletionException( + new IOException("Failed to connect to relay " + relayUri, cause)); + } + }, + RELAY_IO_EXECUTOR); + } + + public ConnectionState getConnectionState() { + return connectionState.get(); + } + + @Override + protected void handleTextMessage(@NonNull WebSocketSession session, TextMessage message) { + log.debug("Relay payload received: {}", message.getPayload()); + dispatchMessage(message.getPayload()); + sendLock.lock(); + try { + if (pendingRequest != null && !pendingRequest.isDone()) { + pendingRequest.addEvent(message.getPayload()); + if (isTerminationMessage(message.getPayload())) { + pendingRequest.complete(); + log.debug("Response future completed with {} events", pendingRequest.eventCount()); + } + } + } finally { + sendLock.unlock(); + } + } + + private boolean isTerminationMessage(String payload) { + if (payload == null || payload.length() < 2) { + return false; + } + return payload.startsWith("[\"EOSE\"") + || payload.startsWith("[\"OK\"") + || payload.startsWith("[\"NOTICE\"") + || payload.startsWith("[\"CLOSED\""); + } + + @Override + public void handleTransportError(@NonNull WebSocketSession session, @NonNull Throwable exception) { + log.warn("Transport error on WebSocket session", exception); + notifyError(exception); + sendLock.lock(); + try { + if (pendingRequest != null && !pendingRequest.isDone()) { + pendingRequest.completeExceptionally(exception); + } + } finally { + sendLock.unlock(); + } + } + + @Override + public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) + throws Exception { + super.afterConnectionClosed(session, status); + connectionState.set(ConnectionState.CLOSED); + notifyClose(); + sendLock.lock(); + try { + if (pendingRequest != null && !pendingRequest.isDone()) { + pendingRequest.completeExceptionally( + new IOException("WebSocket connection closed: " + status)); + } + } finally { + sendLock.unlock(); + } + } + + @NostrRetryable + public List send(T eventMessage) throws IOException { + String json = eventMessage.encode(); + log.debug("Sending {} to relay {} (size={} bytes)", + eventMessage.getCommand(), clientSession.getUri(), json.length()); + return send(json); + } + + @NostrRetryable + public List send(String json) throws IOException { + PendingRequest request; + + sendLock.lock(); + try { + if (pendingRequest != null && !pendingRequest.isDone()) { + throw new IllegalStateException( + "A request is already in flight. Concurrent send() calls are not supported."); + } + request = new PendingRequest(maxEventsPerRequest); + pendingRequest = request; + log.info("Sending request to relay {}: {}", clientSession.getUri(), json); + clientSession.sendMessage(new TextMessage(json)); + } finally { + sendLock.unlock(); + } + + long timeout = awaitTimeoutMs > 0 ? awaitTimeoutMs : DEFAULT_AWAIT_TIMEOUT_MS; + log.debug("Waiting for relay response with timeout={}ms", timeout); + + try { + List result = request.getFuture().get(timeout, TimeUnit.MILLISECONDS); + log.info("Received {} relay events via {}", result.size(), clientSession.getUri()); + return result; + } catch (TimeoutException e) { + log.error("Timed out waiting for relay response after {}ms", timeout); + sendLock.lock(); + try { + if (pendingRequest == request) { + pendingRequest = null; + } + } finally { + sendLock.unlock(); + } + try { + clientSession.close(); + } catch (IOException closeEx) { + log.warn("Error closing session after timeout", closeEx); + } + throw new RelayTimeoutException(timeout); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for relay response", e); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + throw new IOException("Error waiting for relay response", cause); + } finally { + sendLock.lock(); + try { + if (pendingRequest == request) { + pendingRequest = null; + } + } finally { + sendLock.unlock(); + } + } + } + + /** + * Send an encoded relay message on a Virtual Thread. + * + * @param json encoded relay message + * @return future that completes with relay responses + */ + public CompletableFuture> sendAsync(@NonNull String json) { + Objects.requireNonNull(json, "json"); + return executeAsyncWithRetry(() -> send(json)); + } + + /** + * Send a relay message on a Virtual Thread. + * + * @param eventMessage relay message object + * @return future that completes with relay responses + */ + public CompletableFuture> sendAsync(@NonNull T eventMessage) { + Objects.requireNonNull(eventMessage, "eventMessage"); + return executeAsyncWithRetry(() -> send(eventMessage)); + } + + @NostrRetryable + public AutoCloseable subscribe( + @NonNull T requestMessage, + @NonNull Consumer messageListener, + @NonNull Consumer errorListener, + Runnable closeListener) + throws IOException { + String json = requestMessage.encode(); + log.debug("Subscribing with {} on relay {} (size={} bytes)", + requestMessage.getCommand(), clientSession.getUri(), json.length()); + return subscribe(json, messageListener, errorListener, closeListener); + } + + @NostrRetryable + public AutoCloseable subscribe( + String requestJson, + Consumer messageListener, + Consumer errorListener, + Runnable closeListener) + throws IOException { + Objects.requireNonNull(requestJson, "requestJson"); + Objects.requireNonNull(messageListener, "messageListener"); + Objects.requireNonNull(errorListener, "errorListener"); + if (!clientSession.isOpen()) { + throw new IOException("WebSocket session is closed"); + } + + String listenerId = UUID.randomUUID().toString(); + listeners.put( + listenerId, + new ListenerRegistration(messageListener, errorListener, closeListener)); + + try { + clientSession.sendMessage(new TextMessage(requestJson)); + } catch (IOException e) { + listeners.remove(listenerId); + throw e; + } catch (RuntimeException e) { + listeners.remove(listenerId); + throw new IOException("Failed to send subscription payload", e); + } + + return () -> listeners.remove(listenerId); + } + + /** + * Register a subscription asynchronously on a Virtual Thread. + * + * @param requestJson encoded REQ message + * @param messageListener callback for inbound relay payloads + * @param errorListener callback for relay transport errors + * @param closeListener callback when connection closes + * @return future that completes with the subscription handle + */ + public CompletableFuture subscribeAsync( + String requestJson, + Consumer messageListener, + Consumer errorListener, + Runnable closeListener) { + return executeAsyncWithRetry( + () -> subscribe(requestJson, messageListener, errorListener, closeListener)); + } + + /** + * Register a subscription asynchronously on a Virtual Thread. + * + * @param requestMessage message object that encodes to a REQ command + * @param messageListener callback for inbound relay payloads + * @param errorListener callback for relay transport errors + * @param closeListener callback when connection closes + * @return future that completes with the subscription handle + */ + public CompletableFuture subscribeAsync( + @NonNull T requestMessage, + @NonNull Consumer messageListener, + @NonNull Consumer errorListener, + Runnable closeListener) { + return executeAsyncWithRetry( + () -> subscribe(requestMessage, messageListener, errorListener, closeListener)); + } + + @Recover + public List recover(IOException ex, String json) throws IOException { + log.error("Failed to send message to relay {} after retries (size={} bytes)", + clientSession.getUri(), json.length(), ex); + throw ex; + } + + @Recover + public List recover(IOException ex, BaseMessage eventMessage) throws IOException { + String json = eventMessage.encode(); + log.error("Failed to send {} to relay {} after retries (size={} bytes)", + eventMessage.getCommand(), clientSession.getUri(), json.length(), ex); + throw ex; + } + + @Recover + public AutoCloseable recoverSubscription( + IOException ex, + String json, + Consumer messageListener, + Consumer errorListener, + Runnable closeListener) + throws IOException { + log.error("Failed to subscribe on relay {} after retries (size={} bytes)", + clientSession.getUri(), json.length(), ex); + throw ex; + } + + @Recover + public AutoCloseable recoverSubscription( + IOException ex, + BaseMessage requestMessage, + Consumer messageListener, + Consumer errorListener, + Runnable closeListener) + throws IOException { + String json = requestMessage.encode(); + log.error("Failed to subscribe with {} on relay {} after retries (size={} bytes)", + requestMessage.getCommand(), clientSession.getUri(), json.length(), ex); + throw ex; + } + + @Override + public void close() throws IOException { + if (clientSession != null) { + boolean open = false; + try { + open = clientSession.isOpen(); + } catch (Exception e) { + log.warn("Exception while checking if clientSession is open during close()", e); + } + if (open) { + clientSession.close(); + connectionState.set(ConnectionState.CLOSED); + notifyClose(); + } + } + } + + private static StandardWebSocketClient createSpringClient() { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + + long idleTimeout = getLongProperty("nostr.websocket.max-idle-timeout-ms", DEFAULT_MAX_IDLE_TIMEOUT_MS); + int textBufferSize = getIntProperty("nostr.websocket.max-text-message-buffer-size", DEFAULT_MAX_TEXT_MESSAGE_BUFFER_SIZE); + int binaryBufferSize = getIntProperty("nostr.websocket.max-binary-message-buffer-size", DEFAULT_MAX_BINARY_MESSAGE_BUFFER_SIZE); + + container.setDefaultMaxSessionIdleTimeout(idleTimeout); + container.setDefaultMaxTextMessageBufferSize(textBufferSize); + container.setDefaultMaxBinaryMessageBufferSize(binaryBufferSize); + + log.info("websocket_container_configured max_idle_timeout_ms={} max_text_buffer={} max_binary_buffer={}", + idleTimeout, textBufferSize, binaryBufferSize); + return new StandardWebSocketClient(container); + } + + private WebSocketSession connectSession(String relayUri) + throws ExecutionException, InterruptedException { + return createSpringClient().execute(this, new WebSocketHttpHeaders(), URI.create(relayUri)) + .get(); + } + + private static long getLongProperty(String key, long defaultValue) { + String value = System.getProperty(key); + if (value != null && !value.isEmpty()) { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + log.warn("Invalid value for property {}: {}, using default: {}", key, value, defaultValue); + } + } + return defaultValue; + } + + private static int getIntProperty(String key, int defaultValue) { + String value = System.getProperty(key); + if (value != null && !value.isEmpty()) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + log.warn("Invalid value for property {}: {}, using default: {}", key, value, defaultValue); + } + } + return defaultValue; + } + + private void dispatchMessage(String payload) { + List activeListeners = List.copyOf(listeners.values()); + activeListeners.forEach( + listener -> + LISTENER_EXECUTOR.execute( + () -> safelyInvoke(listener.messageListener(), payload, listener))); + } + + private void notifyError(Throwable throwable) { + List activeListeners = List.copyOf(listeners.values()); + activeListeners.forEach( + listener -> + LISTENER_EXECUTOR.execute( + () -> safelyInvoke(listener.errorListener(), throwable, listener))); + } + + private void notifyClose() { + List activeListeners = List.copyOf(listeners.values()); + activeListeners.forEach( + listener -> + LISTENER_EXECUTOR.execute(() -> safelyInvoke(listener.closeListener(), listener))); + listeners.clear(); + } + + private void safelyInvoke(Consumer consumer, String payload, ListenerRegistration listener) { + if (consumer == null) return; + try { + consumer.accept(payload); + } catch (Exception e) { + log.warn("Listener threw exception while handling message", e); + safelyInvoke(listener.errorListener(), e, listener); + } + } + + private void safelyInvoke(Consumer consumer, Throwable throwable, ListenerRegistration ignored) { + if (consumer == null) return; + try { + consumer.accept(throwable); + } catch (Exception e) { + log.warn("Listener error callback threw exception", e); + } + } + + private void safelyInvoke(Runnable runnable, ListenerRegistration listener) { + if (runnable == null) return; + try { + runnable.run(); + } catch (Exception e) { + log.warn("Listener close callback threw exception", e); + safelyInvoke(listener.errorListener(), e, listener); + } + } + + private CompletableFuture executeAsyncWithRetry(IoSupplier operation) { + return CompletableFuture.supplyAsync( + () -> { + try { + return executeWithRetry(operation); + } catch (IOException ex) { + throw new CompletionException(ex); + } + }, + RELAY_IO_EXECUTOR); + } + + private T executeWithRetry(IoSupplier operation) throws IOException { + long retryDelayMs = NostrRetryable.DELAY; + IOException lastFailure = null; + + for (int attempt = 1; attempt <= NostrRetryable.MAX_ATTEMPTS; attempt++) { + try { + return operation.get(); + } catch (IOException ex) { + lastFailure = ex; + if (attempt == NostrRetryable.MAX_ATTEMPTS) { + break; + } + sleepForRetry(retryDelayMs); + retryDelayMs = Math.max(1L, Math.round(retryDelayMs * NostrRetryable.MULTIPLIER)); + } + } + + throw lastFailure == null + ? new IOException("Relay operation failed without exception details") + : lastFailure; + } + + private void sleepForRetry(long retryDelayMs) throws IOException { + try { + Thread.sleep(retryDelayMs); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting to retry relay operation", ex); + } + } + + private record ListenerRegistration( + Consumer messageListener, + Consumer errorListener, + Runnable closeListener) {} + + @FunctionalInterface + private interface IoSupplier { + T get() throws IOException; + } + +} diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/RelayTimeoutException.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/RelayTimeoutException.java new file mode 100644 index 000000000..cd1d6aaba --- /dev/null +++ b/nostr-java-client/src/main/java/nostr/client/springwebsocket/RelayTimeoutException.java @@ -0,0 +1,20 @@ +package nostr.client.springwebsocket; + +import java.io.IOException; + +/** + * Thrown when a relay does not respond within the configured timeout. + */ +public class RelayTimeoutException extends IOException { + + private final long timeoutMs; + + public RelayTimeoutException(long timeoutMs) { + super("Timed out waiting for relay response after " + timeoutMs + "ms"); + this.timeoutMs = timeoutMs; + } + + public long getTimeoutMs() { + return timeoutMs; + } +} diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClient.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClient.java deleted file mode 100644 index 0ff091725..000000000 --- a/nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClient.java +++ /dev/null @@ -1,199 +0,0 @@ -package nostr.client.springwebsocket; - -import lombok.Getter; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import nostr.event.BaseMessage; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.retry.annotation.Recover; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - -@Component -@Slf4j -public class SpringWebSocketClient implements AutoCloseable { - private final WebSocketClientIF webSocketClientIF; - - @Getter private final String relayUrl; - - public SpringWebSocketClient( - @NonNull WebSocketClientIF webSocketClientIF, @Value("${nostr.relay.uri}") String relayUrl) { - this.webSocketClientIF = webSocketClientIF; - this.relayUrl = relayUrl; - } - - /** - * Sends the provided {@link BaseMessage} over the WebSocket connection. - * - * @param eventMessage the message to send - * @return the list of responses from the relay - * @throws IOException if an I/O error occurs while sending the message - */ - @NostrRetryable - public List send(@NonNull BaseMessage eventMessage) throws IOException { - String json = eventMessage.encode(); - log.debug( - "Sending {} to relay {} (size={} bytes)", - eventMessage.getCommand(), - relayUrl, - json.length()); - List responses = webSocketClientIF.send(json); - log.debug( - "Sent {} to relay {} with {} responses", - eventMessage.getCommand(), - relayUrl, - responses.size()); - return responses; - } - - @NostrRetryable - public List send(@NonNull String json) throws IOException { - log.debug("Sending message to relay {} (size={} bytes)", relayUrl, json.length()); - List responses = webSocketClientIF.send(json); - log.debug("Sent message to relay {} with {} responses", relayUrl, responses.size()); - return responses; - } - - @NostrRetryable - public AutoCloseable subscribe( - @NonNull BaseMessage requestMessage, - @NonNull Consumer messageListener, - @NonNull Consumer errorListener, - Runnable closeListener) - throws IOException { - Objects.requireNonNull(messageListener, "messageListener"); - Objects.requireNonNull(errorListener, "errorListener"); - String json = requestMessage.encode(); - log.debug( - "Subscribing with {} on relay {} (size={} bytes)", - requestMessage.getCommand(), - relayUrl, - json.length()); - AutoCloseable handle = - webSocketClientIF.subscribe(json, messageListener, errorListener, closeListener); - log.debug( - "Subscription established with {} on relay {}", - requestMessage.getCommand(), - relayUrl); - return handle; - } - - @NostrRetryable - public AutoCloseable subscribe( - @NonNull String json, - @NonNull Consumer messageListener, - @NonNull Consumer errorListener, - Runnable closeListener) - throws IOException { - Objects.requireNonNull(messageListener, "messageListener"); - Objects.requireNonNull(errorListener, "errorListener"); - log.debug( - "Subscribing with raw message to relay {} (size={} bytes)", relayUrl, json.length()); - AutoCloseable handle = - webSocketClientIF.subscribe(json, messageListener, errorListener, closeListener); - log.debug("Subscription established on relay {}", relayUrl); - return handle; - } - - /** - * Logs a recovery failure with operation context. - * - * @param operation the operation that failed (e.g., "send message", "subscribe") - * @param size the size of the message in bytes - * @param ex the exception that caused the failure - */ - private void logRecoveryFailure(String operation, int size, IOException ex) { - log.error( - "Failed to {} to relay {} after retries (size={} bytes)", - operation, - relayUrl, - size, - ex); - } - - /** - * Logs a recovery failure with operation and command context. - * - * @param operation the operation that failed (e.g., "send", "subscribe with") - * @param command the command type from the message - * @param size the size of the message in bytes - * @param ex the exception that caused the failure - */ - private void logRecoveryFailure(String operation, String command, int size, IOException ex) { - log.error( - "Failed to {} {} to relay {} after retries (size={} bytes)", - operation, - command, - relayUrl, - size, - ex); - } - - /** - * This method is invoked by Spring Retry after all retry attempts for the {@link #send(String)} - * method are exhausted. It logs the failure and rethrows the exception. - * - * @param ex the IOException that caused the retries to fail - * @param json the JSON message that failed to send - * @return nothing; always throws the exception - * @throws IOException always thrown to propagate the failure - */ - @Recover - public List recover(IOException ex, String json) throws IOException { - logRecoveryFailure("send message", json.length(), ex); - throw ex; - } - - @Recover - public AutoCloseable recoverSubscription( - IOException ex, - String json, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) - throws IOException { - logRecoveryFailure("subscribe with raw message", json.length(), ex); - throw ex; - } - - @Recover - public AutoCloseable recoverSubscription( - IOException ex, - BaseMessage requestMessage, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) - throws IOException { - String json = requestMessage.encode(); - logRecoveryFailure("subscribe with", requestMessage.getCommand(), json.length(), ex); - throw ex; - } - - /** - * This method is invoked by Spring Retry after all retry attempts for the {@link #send(BaseMessage)} - * method are exhausted. It logs the failure and rethrows the exception. - * - * @param ex the IOException that caused the retries to fail - * @param eventMessage the BaseMessage that failed to send - * @return nothing; always throws the exception - * @throws IOException always thrown to propagate the failure - */ - @Recover - public List recover(IOException ex, BaseMessage eventMessage) throws IOException { - String json = eventMessage.encode(); - logRecoveryFailure("send", eventMessage.getCommand(), json.length(), ex); - throw ex; - } - - @Override - public void close() throws IOException { - log.debug("Closing WebSocket client for relay {}", relayUrl); - webSocketClientIF.close(); - log.debug("WebSocket client closed for relay {}", relayUrl); - } - -} diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClientFactory.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClientFactory.java deleted file mode 100644 index 328710051..000000000 --- a/nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClientFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package nostr.client.springwebsocket; - -import nostr.base.RelayUri; -import nostr.client.WebSocketClientFactory; - -import java.util.concurrent.ExecutionException; - -/** - * Default factory creating Spring-based WebSocket clients. - */ -public class SpringWebSocketClientFactory implements WebSocketClientFactory { - - @Override - public WebSocketClientIF create(RelayUri relayUri) - throws ExecutionException, InterruptedException { - return new StandardWebSocketClient(relayUri.value()); - } -} diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/StandardWebSocketClient.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/StandardWebSocketClient.java deleted file mode 100644 index beed430b0..000000000 --- a/nostr-java-client/src/main/java/nostr/client/springwebsocket/StandardWebSocketClient.java +++ /dev/null @@ -1,447 +0,0 @@ -package nostr.client.springwebsocket; - -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import nostr.event.BaseMessage; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketHttpHeaders; -import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.handler.TextWebSocketHandler; - -import jakarta.websocket.ContainerProvider; -import jakarta.websocket.WebSocketContainer; - -import java.io.IOException; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; - -/** - * WebSocket client for Nostr relay communication. - * - *

This client uses {@link CompletableFuture} for response waiting, providing instant - * notification when responses arrive instead of polling. This eliminates race conditions - * that can occur with polling-based approaches where the response may arrive between - * poll intervals. - */ -@Component -@Scope(BeanDefinition.SCOPE_PROTOTYPE) -@Slf4j -public class StandardWebSocketClient extends TextWebSocketHandler implements WebSocketClientIF { - private static final long DEFAULT_AWAIT_TIMEOUT_MS = 60000L; - /** Default max idle timeout for WebSocket sessions (1 hour). Set to 0 for no timeout. */ - private static final long DEFAULT_MAX_IDLE_TIMEOUT_MS = 3600000L; - /** Default max text message buffer size (1MB). Large enough for NIP-60 wallet state events. */ - private static final int DEFAULT_MAX_TEXT_MESSAGE_BUFFER_SIZE = 1048576; - /** Default max binary message buffer size (1MB). */ - private static final int DEFAULT_MAX_BINARY_MESSAGE_BUFFER_SIZE = 1048576; - - @Value("${nostr.websocket.await-timeout-ms:60000}") - private long awaitTimeoutMs; - - @Value("${nostr.websocket.poll-interval-ms:500}") - private long pollIntervalMs; // Kept for API compatibility, no longer used for polling - - @Value("${nostr.websocket.max-idle-timeout-ms:3600000}") - private long maxIdleTimeoutMs; - - private final WebSocketSession clientSession; - private final Object sendLock = new Object(); - private PendingRequest pendingRequest; - private final Map listeners = new ConcurrentHashMap<>(); - private final AtomicBoolean connectionClosed = new AtomicBoolean(false); - - /** Encapsulates a pending request's future and its associated events list for thread isolation. */ - private static final class PendingRequest { - private final CompletableFuture> future = new CompletableFuture<>(); - private final List events = new ArrayList<>(); - - void addEvent(String event) { - events.add(event); - } - - void complete() { - future.complete(List.copyOf(events)); - } - - void completeExceptionally(Throwable ex) { - future.completeExceptionally(ex); - } - - boolean isDone() { - return future.isDone(); - } - - CompletableFuture> getFuture() { - return future; - } - - int eventCount() { - return events.size(); - } - } - - /** - * Creates a new {@code StandardWebSocketClient} connected to the provided relay URI. - * - * @param relayUri the URI of the relay to connect to - * @throws java.util.concurrent.ExecutionException if the WebSocket session fails to establish - * @throws InterruptedException if the current thread is interrupted while waiting for the - * WebSocket handshake to complete - */ - public StandardWebSocketClient(@Value("${nostr.relay.uri}") String relayUri) - throws java.util.concurrent.ExecutionException, InterruptedException { - this.clientSession = createSpringClient() - .execute(this, new WebSocketHttpHeaders(), URI.create(relayUri)) - .get(); - } - - /** - * Creates a new {@code StandardWebSocketClient} with custom timeout configuration. - * - *

This constructor allows explicit configuration of timeout values, which is useful - * when creating clients outside of Spring's dependency injection context or when - * programmatic timeout configuration is preferred over property-based configuration. - * - * @param relayUri the URI of the relay to connect to - * @param awaitTimeoutMs timeout in milliseconds for awaiting relay responses (must be positive) - * @param pollIntervalMs polling interval in milliseconds (kept for API compatibility, no longer used) - * @throws java.util.concurrent.ExecutionException if the WebSocket session fails to establish - * @throws InterruptedException if the current thread is interrupted while waiting for the - * WebSocket handshake to complete - * @throws IllegalArgumentException if awaitTimeoutMs or pollIntervalMs is not positive - */ - public StandardWebSocketClient(String relayUri, long awaitTimeoutMs, long pollIntervalMs) - throws java.util.concurrent.ExecutionException, InterruptedException { - if (awaitTimeoutMs <= 0) { - throw new IllegalArgumentException("awaitTimeoutMs must be positive"); - } - if (pollIntervalMs <= 0) { - throw new IllegalArgumentException("pollIntervalMs must be positive"); - } - this.awaitTimeoutMs = awaitTimeoutMs; - this.pollIntervalMs = pollIntervalMs; - log.info("StandardWebSocketClient created for {} with awaitTimeoutMs={}, pollIntervalMs={} (event-driven, no polling)", - relayUri, awaitTimeoutMs, pollIntervalMs); - this.clientSession = createSpringClient() - .execute(this, new WebSocketHttpHeaders(), URI.create(relayUri)) - .get(); - } - - StandardWebSocketClient( - WebSocketSession clientSession, long awaitTimeoutMs, long pollIntervalMs) { - if (clientSession == null) { - throw new NullPointerException("clientSession must not be null"); - } - if (awaitTimeoutMs <= 0) { - throw new IllegalArgumentException("awaitTimeoutMs must be positive"); - } - if (pollIntervalMs <= 0) { - throw new IllegalArgumentException("pollIntervalMs must be positive"); - } - this.clientSession = clientSession; - this.awaitTimeoutMs = awaitTimeoutMs; - this.pollIntervalMs = pollIntervalMs; - } - - @Override - protected void handleTextMessage(@NonNull WebSocketSession session, TextMessage message) { - log.debug("Relay payload received: {}", message.getPayload()); - dispatchMessage(message.getPayload()); - synchronized (sendLock) { - if (pendingRequest != null && !pendingRequest.isDone()) { - pendingRequest.addEvent(message.getPayload()); - // Complete on termination signals: EOSE (end of stored events) or OK (event acceptance) - if (isTerminationMessage(message.getPayload())) { - pendingRequest.complete(); - log.debug("Response future completed with {} events", pendingRequest.eventCount()); - } - } - } - } - - /** - * Checks if the message is a Nostr protocol termination signal. - * - *

Termination signals indicate the relay has finished sending responses: - *

    - *
  • EOSE - End of Stored Events, sent after all matching events for a REQ
  • - *
  • OK - Acknowledgment of an EVENT submission
  • - *
  • NOTICE - Server notice (often indicates errors)
  • - *
  • CLOSED - Subscription closed by relay
  • - *
- */ - private boolean isTerminationMessage(String payload) { - if (payload == null || payload.length() < 2) { - return false; - } - // Quick check for JSON array starting with known termination commands - // Format: ["EOSE", ...] or ["OK", ...] or ["NOTICE", ...] or ["CLOSED", ...] - return payload.startsWith("[\"EOSE\"") - || payload.startsWith("[\"OK\"") - || payload.startsWith("[\"NOTICE\"") - || payload.startsWith("[\"CLOSED\""); - } - - @Override - public void handleTransportError(@NonNull WebSocketSession session, @NonNull Throwable exception) { - log.warn("Transport error on WebSocket session", exception); - notifyError(exception); - synchronized (sendLock) { - if (pendingRequest != null && !pendingRequest.isDone()) { - pendingRequest.completeExceptionally(exception); - } - } - } - - @Override - public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) - throws Exception { - super.afterConnectionClosed(session, status); - if (connectionClosed.compareAndSet(false, true)) { - notifyClose(); - } - synchronized (sendLock) { - if (pendingRequest != null && !pendingRequest.isDone()) { - pendingRequest.completeExceptionally( - new IOException("WebSocket connection closed: " + status)); - } - } - } - - @Override - public List send(T eventMessage) throws IOException { - return send(eventMessage.encode()); - } - - @Override - public List send(String json) throws IOException { - PendingRequest request; - - synchronized (sendLock) { - if (pendingRequest != null && !pendingRequest.isDone()) { - throw new IllegalStateException( - "A request is already in flight. Concurrent send() calls are not supported. " - + "Wait for the current request to complete or use separate client instances."); - } - request = new PendingRequest(); - pendingRequest = request; - log.info("Sending request to relay {}: {}", clientSession.getUri(), json); - clientSession.sendMessage(new TextMessage(json)); - } - - long timeout = awaitTimeoutMs > 0 ? awaitTimeoutMs : DEFAULT_AWAIT_TIMEOUT_MS; - log.debug("Waiting for relay response with timeout={}ms (event-driven)", timeout); - - try { - List result = request.getFuture().get(timeout, TimeUnit.MILLISECONDS); - log.info("Received {} relay events via {}", result.size(), clientSession.getUri()); - return result; - } catch (TimeoutException e) { - log.error("Timed out waiting for relay response after {}ms", timeout); - synchronized (sendLock) { - if (pendingRequest == request) { - pendingRequest = null; - } - } - try { - clientSession.close(); - } catch (IOException closeEx) { - log.warn("Error closing session after timeout", closeEx); - } - return List.of(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Interrupted while waiting for relay response", e); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof IOException) { - throw (IOException) cause; - } - throw new IOException("Error waiting for relay response", cause); - } finally { - synchronized (sendLock) { - if (pendingRequest == request) { - pendingRequest = null; - } - } - } - } - - @Override - public AutoCloseable subscribe( - String requestJson, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) - throws IOException { - if (requestJson == null || messageListener == null || errorListener == null) { - throw new NullPointerException("Subscription parameters must not be null"); - } - if (!clientSession.isOpen()) { - throw new IOException("WebSocket session is closed"); - } - - String listenerId = UUID.randomUUID().toString(); - listeners.put( - listenerId, - new ListenerRegistration(messageListener, errorListener, closeListener)); - - try { - clientSession.sendMessage(new TextMessage(requestJson)); - } catch (IOException e) { - listeners.remove(listenerId); - throw e; - } catch (RuntimeException e) { - listeners.remove(listenerId); - throw new IOException("Failed to send subscription payload", e); - } - - return () -> listeners.remove(listenerId); - } - - @Override - public void close() throws IOException { - if (clientSession != null) { - boolean open = false; - try { - open = clientSession.isOpen(); - } catch (Exception e) { - log.warn("Exception while checking if clientSession is open during close()", e); - } - if (open) { - clientSession.close(); - if (connectionClosed.compareAndSet(false, true)) { - notifyClose(); - } - } - } - } - - /** - * Creates a Spring WebSocket client configured with extended timeout and buffer sizes. - * - *

The WebSocketContainer is configured with: - *

    - *
  • Max session idle timeout to prevent premature connection closures (important for - * Nostr relays that may have periods of inactivity between messages)
  • - *
  • Large text/binary message buffers (default 1MB) to handle NIP-60 wallet state events - * and other large Nostr events that can exceed default buffer sizes
  • - *
- * - *

Configuration via system properties: - *

    - *
  • {@code nostr.websocket.max-idle-timeout-ms} - Max session idle timeout (default: 3600000)
  • - *
  • {@code nostr.websocket.max-text-message-buffer-size} - Max text message buffer (default: 1048576)
  • - *
  • {@code nostr.websocket.max-binary-message-buffer-size} - Max binary message buffer (default: 1048576)
  • - *
- * - * @return a configured Spring StandardWebSocketClient - */ - private static org.springframework.web.socket.client.standard.StandardWebSocketClient createSpringClient() { - WebSocketContainer container = ContainerProvider.getWebSocketContainer(); - - long idleTimeout = getLongProperty("nostr.websocket.max-idle-timeout-ms", DEFAULT_MAX_IDLE_TIMEOUT_MS); - int textBufferSize = getIntProperty("nostr.websocket.max-text-message-buffer-size", DEFAULT_MAX_TEXT_MESSAGE_BUFFER_SIZE); - int binaryBufferSize = getIntProperty("nostr.websocket.max-binary-message-buffer-size", DEFAULT_MAX_BINARY_MESSAGE_BUFFER_SIZE); - - container.setDefaultMaxSessionIdleTimeout(idleTimeout); - container.setDefaultMaxTextMessageBufferSize(textBufferSize); - container.setDefaultMaxBinaryMessageBufferSize(binaryBufferSize); - - log.info("websocket_container_configured max_idle_timeout_ms={} max_text_buffer={} max_binary_buffer={}", - idleTimeout, textBufferSize, binaryBufferSize); - return new org.springframework.web.socket.client.standard.StandardWebSocketClient(container); - } - - private static long getLongProperty(String key, long defaultValue) { - String value = System.getProperty(key); - if (value != null && !value.isEmpty()) { - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - log.warn("Invalid value for property {}: {}, using default: {}", key, value, defaultValue); - } - } - return defaultValue; - } - - private static int getIntProperty(String key, int defaultValue) { - String value = System.getProperty(key); - if (value != null && !value.isEmpty()) { - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - log.warn("Invalid value for property {}: {}, using default: {}", key, value, defaultValue); - } - } - return defaultValue; - } - - private void dispatchMessage(String payload) { - listeners.values().forEach(listener -> safelyInvoke(listener.messageListener(), payload, listener)); - } - - private void notifyError(Throwable throwable) { - listeners.values().forEach(listener -> safelyInvoke(listener.errorListener(), throwable, listener)); - } - - private void notifyClose() { - listeners.values().forEach(listener -> safelyInvoke(listener.closeListener(), listener)); - listeners.clear(); - } - - private void safelyInvoke(Consumer consumer, String payload, ListenerRegistration listener) { - if (consumer == null) { - return; - } - try { - consumer.accept(payload); - } catch (Exception e) { - log.warn("Listener threw exception while handling message", e); - safelyInvoke(listener.errorListener(), e, listener); - } - } - - private void safelyInvoke(Consumer consumer, Throwable throwable, ListenerRegistration ignored) { - if (consumer == null) { - return; - } - try { - consumer.accept(throwable); - } catch (Exception e) { - log.warn("Listener error callback threw exception", e); - } - } - - private void safelyInvoke(Runnable runnable, ListenerRegistration listener) { - if (runnable == null) { - return; - } - try { - runnable.run(); - } catch (Exception e) { - log.warn("Listener close callback threw exception", e); - safelyInvoke(listener.errorListener(), e, listener); - } - } - - private record ListenerRegistration( - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) {} -} diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClientIF.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClientIF.java deleted file mode 100644 index d1da585ea..000000000 --- a/nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClientIF.java +++ /dev/null @@ -1,107 +0,0 @@ -package nostr.client.springwebsocket; - -import nostr.event.BaseMessage; - -import java.io.IOException; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - -/** - * Abstraction of a client-owned WebSocket connection to a Nostr relay. - * - *

Implementations typically maintain a single active connection and are not required to be - * thread-safe. Callers should serialize access and invoke {@link #close()} when the client is no - * longer needed. - */ -public interface WebSocketClientIF extends AutoCloseable { - - /** - * Sends the provided Nostr message over the current WebSocket connection. - * - *

The call blocks until the implementation considers the exchange complete (for example, after - * receiving a response or timing out). The method should be invoked by a single thread at a time - * as implementations are generally not thread-safe. - * - * @param eventMessage the message to encode and transmit - * @param the specific {@link nostr.event.BaseMessage} subtype - * @return a list of raw JSON payloads received in response; never {@code null}, but possibly - * empty - * @throws IOException if the message cannot be sent or the connection fails - */ - List send(T eventMessage) throws IOException; - - /** - * Sends a raw JSON string over the WebSocket connection. - * - *

Semantics match send(BaseMessage): the call is blocking and should not be invoked - * concurrently from multiple threads. - * - * @param json the JSON payload to transmit - * @return a list of raw JSON payloads received in response; never {@code null}, but possibly - * empty - * @throws IOException if the message cannot be sent or the connection fails - */ - List send(String json) throws IOException; - - /** - * Registers a listener for streaming messages while sending the provided JSON payload - * asynchronously. - * - *

The implementation MUST send {@code requestJson} immediately without blocking the caller - * for relay responses. Inbound messages received on the connection are dispatched to the provided - * {@code messageListener}. Transport errors should be forwarded to {@code errorListener}, and the - * optional {@code closeListener} should be invoked exactly once when the underlying connection is - * closed. - * - * @param requestJson the JSON payload to transmit to start the subscription - * @param messageListener callback invoked for each message received - * @param errorListener callback invoked when a transport error occurs - * @param closeListener optional callback invoked when the connection closes normally - * @return a handle that cancels the subscription when closed - * @throws IOException if the payload cannot be sent or the connection is unavailable - */ - AutoCloseable subscribe( - String requestJson, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) - throws IOException; - - /** - * Convenience overload that accepts a {@link nostr.event.BaseMessage} and delegates to - * {@link #subscribe(String, Consumer, Consumer, Runnable)}. - * - * @param eventMessage the message to encode and transmit - * @param messageListener callback invoked for each message received - * @param errorListener callback invoked when a transport error occurs - * @param closeListener optional callback invoked when the connection closes normally - * @return a handle that cancels the subscription when closed - * @throws IOException if encoding or transmission fails - */ - default AutoCloseable subscribe( - T eventMessage, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) - throws IOException { - Objects.requireNonNull(eventMessage, "eventMessage"); - return subscribe( - eventMessage.encode(), - Objects.requireNonNull(messageListener, "messageListener"), - Objects.requireNonNull(errorListener, "errorListener"), - closeListener); - } - - /** - * Closes the underlying WebSocket session and releases associated resources. - * - *

The caller that created this client is responsible for invoking this method when the - * connection is no longer required. After invocation, the client should not be used for further - * send operations. - * - * @throws IOException if an I/O error occurs while closing the connection - */ - @Override - void close() throws IOException; -} diff --git a/nostr-java-client/src/test/java/nostr/client/springwebsocket/StandardWebSocketClientMultiMessageTest.java b/nostr-java-client/src/test/java/nostr/client/springwebsocket/NostrRelayClientMultiMessageTest.java similarity index 73% rename from nostr-java-client/src/test/java/nostr/client/springwebsocket/StandardWebSocketClientMultiMessageTest.java rename to nostr-java-client/src/test/java/nostr/client/springwebsocket/NostrRelayClientMultiMessageTest.java index 73c5236a5..74aecd833 100644 --- a/nostr-java-client/src/test/java/nostr/client/springwebsocket/StandardWebSocketClientMultiMessageTest.java +++ b/nostr-java-client/src/test/java/nostr/client/springwebsocket/NostrRelayClientMultiMessageTest.java @@ -8,34 +8,34 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; /** - * Tests that StandardWebSocketClient correctly accumulates multiple messages + * Tests that NostrRelayClient correctly accumulates multiple messages * before completing when a termination message (EOSE, OK) is received. */ -class StandardWebSocketClientMultiMessageTest { +class NostrRelayClientMultiMessageTest { @Test void testAccumulatesMessagesUntilEose() throws Exception { WebSocketSession session = Mockito.mock(WebSocketSession.class); when(session.isOpen()).thenReturn(true); - StandardWebSocketClient client = new StandardWebSocketClient(session, 5000, 100); + NostrRelayClient client = new NostrRelayClient(session, 5000); - // Simulate relay responses when send is called CountDownLatch sendLatch = new CountDownLatch(1); doAnswer(invocation -> { sendLatch.countDown(); return null; }).when(session).sendMessage(any(TextMessage.class)); - // Start send in background thread Thread sendThread = new Thread(() -> { try { List result = client.send("[\"REQ\",\"sub1\",{}]"); @@ -48,20 +48,13 @@ void testAccumulatesMessagesUntilEose() throws Exception { }); sendThread.start(); - // Wait for send to start assertTrue(sendLatch.await(1, TimeUnit.SECONDS), "Send should have started"); - Thread.sleep(50); // Small delay for send() to set up pendingRequest + Thread.sleep(50); - // Simulate EVENT message (not termination - should NOT complete) client.handleTextMessage(session, new TextMessage("[\"EVENT\",\"sub1\",{\"id\":\"abc\"}]")); - - // Small delay to ensure processing Thread.sleep(50); - - // Simulate EOSE message (termination - should complete) client.handleTextMessage(session, new TextMessage("[\"EOSE\",\"sub1\"]")); - // Wait for send thread to complete sendThread.join(2000); } @@ -70,7 +63,7 @@ void testCompletesImmediatelyOnOk() throws Exception { WebSocketSession session = Mockito.mock(WebSocketSession.class); when(session.isOpen()).thenReturn(true); - StandardWebSocketClient client = new StandardWebSocketClient(session, 5000, 100); + NostrRelayClient client = new NostrRelayClient(session, 5000); CountDownLatch sendLatch = new CountDownLatch(1); doAnswer(invocation -> { @@ -92,19 +85,17 @@ void testCompletesImmediatelyOnOk() throws Exception { assertTrue(sendLatch.await(1, TimeUnit.SECONDS)); Thread.sleep(50); - // Simulate OK message (termination - should complete immediately) client.handleTextMessage(session, new TextMessage("[\"OK\",\"abc\",true,\"\"]")); sendThread.join(2000); } @Test - void testEventWithoutEoseTimesOut() throws Exception { + void testEventWithoutEoseThrowsRelayTimeout() throws Exception { WebSocketSession session = Mockito.mock(WebSocketSession.class); when(session.isOpen()).thenReturn(true); - // Short timeout for this test - StandardWebSocketClient client = new StandardWebSocketClient(session, 200, 50); + NostrRelayClient client = new NostrRelayClient(session, 200); CountDownLatch sendLatch = new CountDownLatch(1); doAnswer(invocation -> { @@ -112,13 +103,12 @@ void testEventWithoutEoseTimesOut() throws Exception { return null; }).when(session).sendMessage(any(TextMessage.class)); + AtomicReference caught = new AtomicReference<>(); Thread sendThread = new Thread(() -> { try { - List result = client.send("[\"REQ\",\"sub1\",{}]"); - // Without EOSE, should timeout and return empty - assertTrue(result.isEmpty(), "Should timeout and return empty list"); + client.send("[\"REQ\",\"sub1\",{}]"); } catch (Exception e) { - throw new RuntimeException(e); + caught.set(e); } }); sendThread.start(); @@ -126,10 +116,10 @@ void testEventWithoutEoseTimesOut() throws Exception { assertTrue(sendLatch.await(1, TimeUnit.SECONDS)); Thread.sleep(50); - // Simulate EVENT message but no EOSE client.handleTextMessage(session, new TextMessage("[\"EVENT\",\"sub1\",{\"id\":\"abc\"}]")); - // Don't send EOSE - should timeout sendThread.join(1000); + assertInstanceOf(RelayTimeoutException.class, caught.get(), + "Should throw RelayTimeoutException on timeout"); } } diff --git a/nostr-java-client/src/test/java/nostr/client/springwebsocket/StandardWebSocketClientSubscriptionTest.java b/nostr-java-client/src/test/java/nostr/client/springwebsocket/NostrRelayClientSubscriptionTest.java similarity index 79% rename from nostr-java-client/src/test/java/nostr/client/springwebsocket/StandardWebSocketClientSubscriptionTest.java rename to nostr-java-client/src/test/java/nostr/client/springwebsocket/NostrRelayClientSubscriptionTest.java index 0aab79b21..a41cd4b65 100644 --- a/nostr-java-client/src/test/java/nostr/client/springwebsocket/StandardWebSocketClientSubscriptionTest.java +++ b/nostr-java-client/src/test/java/nostr/client/springwebsocket/NostrRelayClientSubscriptionTest.java @@ -6,6 +6,8 @@ import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -13,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -class StandardWebSocketClientSubscriptionTest { +class NostrRelayClientSubscriptionTest { // Verifies that subscription listeners receive multiple messages without blocking the caller. @Test @@ -21,20 +23,25 @@ void subscribeDeliversMultipleMessagesWithoutBlocking() throws Exception { WebSocketSession session = Mockito.mock(WebSocketSession.class); Mockito.when(session.isOpen()).thenReturn(true); - try (StandardWebSocketClient client = new StandardWebSocketClient(session, 1_000, 50)) { + try (NostrRelayClient client = new NostrRelayClient(session, 1_000)) { AtomicInteger received = new AtomicInteger(); AtomicBoolean errorInvoked = new AtomicBoolean(false); + CountDownLatch receiveLatch = new CountDownLatch(2); AutoCloseable handle = client.subscribe( "[\"REQ\",\"sub\"]", - message -> received.incrementAndGet(), + message -> { + received.incrementAndGet(); + receiveLatch.countDown(); + }, throwable -> errorInvoked.set(true), null); client.handleTextMessage(session, new TextMessage("event-one")); client.handleTextMessage(session, new TextMessage("event-two")); + assertTrue(receiveLatch.await(1, TimeUnit.SECONDS)); assertEquals(2, received.get()); assertFalse(errorInvoked.get()); diff --git a/nostr-java-client/src/test/java/nostr/client/springwebsocket/StandardWebSocketClientTimeoutTest.java b/nostr-java-client/src/test/java/nostr/client/springwebsocket/NostrRelayClientTimeoutTest.java similarity index 51% rename from nostr-java-client/src/test/java/nostr/client/springwebsocket/StandardWebSocketClientTimeoutTest.java rename to nostr-java-client/src/test/java/nostr/client/springwebsocket/NostrRelayClientTimeoutTest.java index 014110e02..12f85d724 100644 --- a/nostr-java-client/src/test/java/nostr/client/springwebsocket/StandardWebSocketClientTimeoutTest.java +++ b/nostr-java-client/src/test/java/nostr/client/springwebsocket/NostrRelayClientTimeoutTest.java @@ -5,18 +5,15 @@ import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; -import java.util.List; +import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class StandardWebSocketClientTimeoutTest { +public class NostrRelayClientTimeoutTest { @Test - public void testTimeoutReturnsEmptyListAndClosesSession() throws Exception { + public void testTimeoutThrowsRelayTimeoutExceptionAndClosesSession() throws Exception { WebSocketSession session = Mockito.mock(WebSocketSession.class); - try (StandardWebSocketClient client = new StandardWebSocketClient(session, 100, 50)) { - List result = client.send("test"); - assertTrue(result.isEmpty()); + try (NostrRelayClient client = new NostrRelayClient(session, 100)) { + assertThrows(RelayTimeoutException.class, () -> client.send("test")); } Mockito.verify(session).sendMessage(Mockito.any(TextMessage.class)); Mockito.verify(session).close(); diff --git a/nostr-java-client/src/test/java/nostr/client/springwebsocket/SpringWebSocketClientSubscribeTest.java b/nostr-java-client/src/test/java/nostr/client/springwebsocket/SpringWebSocketClientSubscribeTest.java index 54a1ba885..3ed961a85 100644 --- a/nostr-java-client/src/test/java/nostr/client/springwebsocket/SpringWebSocketClientSubscribeTest.java +++ b/nostr-java-client/src/test/java/nostr/client/springwebsocket/SpringWebSocketClientSubscribeTest.java @@ -1,102 +1,93 @@ package nostr.client.springwebsocket; -import lombok.Getter; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.mockito.Mockito; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringJUnitConfig( - classes = { - RetryConfig.class, - SpringWebSocketClient.class, - SpringWebSocketClientSubscribeTest.TestConfig.class - }) -@TestPropertySource(properties = "nostr.relay.uri=wss://test") +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests that NostrRelayClient subscribe correctly dispatches messages, errors, + * and close callbacks to registered listeners. + */ class SpringWebSocketClientSubscribeTest { - @Configuration - static class TestConfig { - @Bean - EmitterWebSocketClient webSocketClientIF() { - return new EmitterWebSocketClient(); + // Verifies message and error callbacks execute for an active subscription. + @Test + void subscribeReceivesMessagesAndErrorAndClose() throws Exception { + WebSocketSession session = Mockito.mock(WebSocketSession.class); + when(session.isOpen()).thenReturn(true); + + try (NostrRelayClient client = new NostrRelayClient(session, 1_000)) { + AtomicInteger messages = new AtomicInteger(); + AtomicInteger errors = new AtomicInteger(); + AtomicInteger closes = new AtomicInteger(); + CountDownLatch messageLatch = new CountDownLatch(1); + CountDownLatch errorLatch = new CountDownLatch(1); + + AutoCloseable handle = + client.subscribe( + "[\"REQ\",\"sub-1\",{}]", + payload -> { + messages.incrementAndGet(); + messageLatch.countDown(); + }, + t -> { + errors.incrementAndGet(); + errorLatch.countDown(); + }, + closes::incrementAndGet); + + // Simulate relay messages + client.handleTextMessage(session, new TextMessage("EVENT")); + client.handleTransportError(session, new IOException("boom")); + handle.close(); + + // Close callback is only invoked on connection close, not on unsubscribe + // So we trigger it via afterConnectionClosed + client.afterConnectionClosed(session, new org.springframework.web.socket.CloseStatus(1000)); + + assertTrue(messageLatch.await(1, TimeUnit.SECONDS)); + assertTrue(errorLatch.await(1, TimeUnit.SECONDS)); + assertEquals(1, messages.get()); + assertEquals(1, errors.get()); + assertEquals(0, closes.get()); + // Close listener invoked during afterConnectionClosed + // (unsubscribe via handle.close() just removes the listener) + + verify(session).sendMessage(any(TextMessage.class)); } } - static class EmitterWebSocketClient implements WebSocketClientIF { - @Getter private String lastJson; - private Consumer messageListener; - private Consumer errorListener; - private Runnable closeListener; - - @Override - public java.util.List send(T eventMessage) - throws IOException { - return send(eventMessage.encode()); - } - - @Override - public java.util.List send(String json) throws IOException { - lastJson = json; - return java.util.List.of(); - } - - @Override - public AutoCloseable subscribe( - String requestJson, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) - throws IOException { - this.lastJson = requestJson; - this.messageListener = messageListener; - this.errorListener = errorListener; - this.closeListener = closeListener; - return () -> { - if (this.closeListener != null) this.closeListener.run(); - }; - } + // Verifies BaseMessage subscriptions encode and send a REQ payload. + @Test + void subscribeBaseMessageOverloadSendsEncodedJson() throws Exception { + WebSocketSession session = Mockito.mock(WebSocketSession.class); + when(session.isOpen()).thenReturn(true); - @Override - public void close() {} + try (NostrRelayClient client = new NostrRelayClient(session, 1_000)) { + var reqMessage = new nostr.event.message.ReqMessage( + "sub-1", nostr.event.filter.EventFilter.builder().kind(1).build()); - void emit(String payload) { if (messageListener != null) messageListener.accept(payload); } - void emitError(Throwable t) { if (errorListener != null) errorListener.accept(t); } - } + AutoCloseable handle = + client.subscribe(reqMessage, s -> {}, t -> {}, null); - @Autowired private SpringWebSocketClient client; - @Autowired private EmitterWebSocketClient webSocketClientIF; + org.mockito.ArgumentCaptor captor = + org.mockito.ArgumentCaptor.forClass(TextMessage.class); + verify(session).sendMessage(captor.capture()); + assertTrue(captor.getValue().getPayload().contains("REQ")); - @Test - void subscribeReceivesMessagesAndErrorAndClose() throws Exception { - AtomicInteger messages = new AtomicInteger(); - AtomicInteger errors = new AtomicInteger(); - AtomicInteger closes = new AtomicInteger(); - - AutoCloseable handle = - client.subscribe( - new nostr.event.message.ReqMessage("sub-1", new nostr.event.filter.Filters[] {}), - payload -> messages.incrementAndGet(), - t -> errors.incrementAndGet(), - closes::incrementAndGet - ); - - webSocketClientIF.emit("EVENT"); - webSocketClientIF.emitError(new IOException("boom")); - handle.close(); - - assertEquals(1, messages.get()); - assertEquals(1, errors.get()); - assertEquals(1, closes.get()); - assertTrue(webSocketClientIF.getLastJson().contains("\"REQ\"")); + handle.close(); + } } } diff --git a/nostr-java-client/src/test/java/nostr/client/springwebsocket/SpringWebSocketClientTest.java b/nostr-java-client/src/test/java/nostr/client/springwebsocket/SpringWebSocketClientTest.java index 4442186fa..4fe48ea1c 100644 --- a/nostr-java-client/src/test/java/nostr/client/springwebsocket/SpringWebSocketClientTest.java +++ b/nostr-java-client/src/test/java/nostr/client/springwebsocket/SpringWebSocketClientTest.java @@ -1,148 +1,165 @@ package nostr.client.springwebsocket; -import lombok.Getter; -import lombok.Setter; -import nostr.event.BaseMessage; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.mockito.Mockito; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; import java.io.IOException; import java.util.List; -import java.util.function.Consumer; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringJUnitConfig( - classes = { - RetryConfig.class, - SpringWebSocketClient.class, - SpringWebSocketClientTest.TestConfig.class - }) -@TestPropertySource(properties = "nostr.relay.uri=wss://test") +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +/** + * Tests for NostrRelayClient send, subscribe, and timeout behavior. + */ class SpringWebSocketClientTest { - @Configuration - static class TestConfig { - @Bean - TestWebSocketClient webSocketClientIF() { - return new TestWebSocketClient(); - } - } + // Verifies sendAsync completes after the relay emits a termination message. + @Test + void sendAsyncReturnsResponseOnTermination() throws Exception { + WebSocketSession session = Mockito.mock(WebSocketSession.class); + when(session.isOpen()).thenReturn(true); - static class TestWebSocketClient implements WebSocketClientIF { - @Getter @Setter private int attempts; - @Setter private int failuresBeforeSuccess; - @Getter @Setter private int subAttempts; - @Setter private int subFailuresBeforeSuccess; + NostrRelayClient client = new NostrRelayClient(session, 5000); - @Override - public List send(T eventMessage) throws IOException { - return send(eventMessage.encode()); - } + CountDownLatch sendLatch = new CountDownLatch(1); + doAnswer(invocation -> { + sendLatch.countDown(); + return null; + }).when(session).sendMessage(any(TextMessage.class)); - @Override - public List send(String json) throws IOException { - attempts++; - if (attempts <= failuresBeforeSuccess) { - throw new IOException("fail"); - } - return List.of("ok"); - } + CompletableFuture> resultFuture = client.sendAsync("[\"REQ\",\"sub1\",{}]"); - @Override - public AutoCloseable subscribe( - String requestJson, - Consumer messageListener, - Consumer errorListener, - Runnable closeListener) - throws IOException { - subAttempts++; - if (subAttempts <= subFailuresBeforeSuccess) { - throw new IOException("sub-fail"); - } - return () -> {}; - } + assertTrue(sendLatch.await(1, TimeUnit.SECONDS)); + client.handleTextMessage(session, new TextMessage("[\"EOSE\",\"sub1\"]")); - @Override - public void close() {} + List result = resultFuture.get(2, TimeUnit.SECONDS); + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.get(0).contains("EOSE")); } - @Autowired private SpringWebSocketClient client; - - @Autowired private TestWebSocketClient webSocketClientIF; + // Verifies blocking send times out and closes the underlying session. + @Test + void sendThrowsRelayTimeoutExceptionOnTimeout() throws Exception { + WebSocketSession session = Mockito.mock(WebSocketSession.class); + when(session.isOpen()).thenReturn(true); - @BeforeEach - void setup() { - webSocketClientIF.setFailuresBeforeSuccess(0); - webSocketClientIF.setAttempts(0); - // Reset subscription-related state to avoid test interference across methods - webSocketClientIF.setSubFailuresBeforeSuccess(0); - webSocketClientIF.setSubAttempts(0); + try (NostrRelayClient client = new NostrRelayClient(session, 100)) { + assertThrows(RelayTimeoutException.class, () -> client.send("test")); + } + Mockito.verify(session).sendMessage(any(TextMessage.class)); + Mockito.verify(session, Mockito.atLeastOnce()).close(); } - // Ensures retryable send eventually succeeds after configured transient failures. + // Verifies subscription callbacks are delivered asynchronously and stop after unsubscribe. @Test - void retriesUntilSuccess() throws IOException { - webSocketClientIF.setFailuresBeforeSuccess(2); - List result = client.send("payload"); - assertEquals(List.of("ok"), result); - assertEquals(3, webSocketClientIF.getAttempts()); + void subscribeDeliversMessages() throws Exception { + WebSocketSession session = Mockito.mock(WebSocketSession.class); + when(session.isOpen()).thenReturn(true); + + try (NostrRelayClient client = new NostrRelayClient(session, 1_000)) { + AtomicInteger received = new AtomicInteger(); + AtomicBoolean extraMessageReceived = new AtomicBoolean(false); + CountDownLatch receiveLatch = new CountDownLatch(2); + + AutoCloseable handle = + client.subscribe( + "[\"REQ\",\"sub\"]", + message -> { + int count = received.incrementAndGet(); + if (count > 2) { + extraMessageReceived.set(true); + return; + } + receiveLatch.countDown(); + }, + throwable -> {}, + null); + + client.handleTextMessage(session, new TextMessage("event-one")); + client.handleTextMessage(session, new TextMessage("event-two")); + + assertTrue(receiveLatch.await(1, TimeUnit.SECONDS)); + assertEquals(2, received.get()); + + handle.close(); + client.handleTextMessage(session, new TextMessage("event-three")); + Thread.sleep(100); + assertEquals(2, received.get()); + assertFalse(extraMessageReceived.get()); + } } - // Ensures the client surfaces the final IOException after exhausting retries. + // Verifies listener callbacks are executed on a Virtual Thread, not the caller thread. @Test - void recoverAfterMaxAttempts() { - webSocketClientIF.setFailuresBeforeSuccess(5); - assertThrows(IOException.class, () -> client.send("payload")); - assertEquals(3, webSocketClientIF.getAttempts()); + void subscribeDispatchesListenerOnVirtualThread() throws Exception { + WebSocketSession session = Mockito.mock(WebSocketSession.class); + when(session.isOpen()).thenReturn(true); + + try (NostrRelayClient client = new NostrRelayClient(session, 1_000)) { + long callerThreadId = Thread.currentThread().threadId(); + AtomicReference callbackThreadId = new AtomicReference<>(); + AtomicBoolean callbackWasVirtual = new AtomicBoolean(false); + CountDownLatch callbackLatch = new CountDownLatch(1); + + AutoCloseable handle = + client.subscribe( + "[\"REQ\",\"sub\"]", + message -> { + callbackThreadId.set(Thread.currentThread().threadId()); + callbackWasVirtual.set(Thread.currentThread().isVirtual()); + callbackLatch.countDown(); + }, + throwable -> {}, + null); + + client.handleTextMessage(session, new TextMessage("event-one")); + + assertTrue(callbackLatch.await(1, TimeUnit.SECONDS)); + assertTrue(callbackWasVirtual.get()); + assertNotEquals(callerThreadId, callbackThreadId.get()); + handle.close(); + } } - // Ensures retryable subscribe eventually succeeds after configured transient failures. + // Verifies subscribe rejects attempts when the session is already closed. @Test - void subscribeRetriesUntilSuccess() throws Exception { - webSocketClientIF.setSubFailuresBeforeSuccess(2); - AutoCloseable h = - client.subscribe( - new nostr.event.message.ReqMessage("sub-1", new nostr.event.filter.Filters[] {}), - s -> {}, - t -> {}, - () -> {}); - h.close(); - assertEquals(3, webSocketClientIF.getSubAttempts()); - } + void subscribeThrowsOnClosedSession() throws Exception { + WebSocketSession session = Mockito.mock(WebSocketSession.class); + when(session.isOpen()).thenReturn(false); - // Ensures subscribe surfaces final IOException after exhausting retries. - @Test - void subscribeRecoverAfterMaxAttempts() { - webSocketClientIF.setSubFailuresBeforeSuccess(5); - assertThrows( - IOException.class, - () -> - client.subscribe( - new nostr.event.message.ReqMessage("sub-2", new nostr.event.filter.Filters[] {}), - s -> {}, - t -> {}, - () -> {})); - assertEquals(3, webSocketClientIF.getSubAttempts()); + NostrRelayClient client = new NostrRelayClient(session, 1_000); + + assertThrows(IOException.class, () -> + client.subscribe("[\"REQ\",\"sub\"]", s -> {}, t -> {}, null)); } - // Ensures retry also applies to the raw String subscribe overload. + // Verifies send propagates transport failures from the WebSocket session. @Test - void subscribeRawRetriesUntilSuccess() throws Exception { - webSocketClientIF.setSubFailuresBeforeSuccess(1); - AutoCloseable h = - client.subscribe( - "[\"REQ\",\"sub-raw\",{}]", - s -> {}, - t -> {}, - () -> {}); - h.close(); - assertEquals(2, webSocketClientIF.getSubAttempts()); + void sendWithSendFailureThrowsIOException() throws Exception { + WebSocketSession session = Mockito.mock(WebSocketSession.class); + when(session.isOpen()).thenReturn(true); + Mockito.doThrow(new IOException("connection lost")) + .when(session).sendMessage(any(TextMessage.class)); + + NostrRelayClient client = new NostrRelayClient(session, 1_000); + + assertThrows(IOException.class, () -> client.send("payload")); } } diff --git a/nostr-java-base/pom.xml b/nostr-java-core/pom.xml similarity index 80% rename from nostr-java-base/pom.xml rename to nostr-java-core/pom.xml index 885c9a1ea..2305d2fd6 100644 --- a/nostr-java-base/pom.xml +++ b/nostr-java-core/pom.xml @@ -4,13 +4,14 @@ xyz.tcheeric nostr-java - 1.3.0 + 2.0.0 ../pom.xml - - nostr-java-base + + nostr-java-core jar - nostr-java-base + nostr-java-core + Foundation utilities and BIP340 Schnorr cryptography for the Nostr Java SDK @@ -24,16 +25,10 @@ - - - ${project.groupId} - nostr-java-util - - + - ${project.groupId} - nostr-java-crypto - + org.apache.commons + commons-lang3 @@ -44,14 +39,18 @@ com.fasterxml.jackson.module jackson-module-blackbird - + + + + + org.bouncycastle + bcprov-jdk18on org.projectlombok lombok - provided @@ -63,7 +62,6 @@ org.junit.jupiter junit-jupiter - test diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/Pair.java b/nostr-java-core/src/main/java/nostr/crypto/Pair.java similarity index 100% rename from nostr-java-crypto/src/main/java/nostr/crypto/Pair.java rename to nostr-java-core/src/main/java/nostr/crypto/Pair.java diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/Point.java b/nostr-java-core/src/main/java/nostr/crypto/Point.java similarity index 100% rename from nostr-java-crypto/src/main/java/nostr/crypto/Point.java rename to nostr-java-core/src/main/java/nostr/crypto/Point.java diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32.java b/nostr-java-core/src/main/java/nostr/crypto/bech32/Bech32.java similarity index 100% rename from nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32.java rename to nostr-java-core/src/main/java/nostr/crypto/bech32/Bech32.java diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32EncodingException.java b/nostr-java-core/src/main/java/nostr/crypto/bech32/Bech32EncodingException.java similarity index 100% rename from nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32EncodingException.java rename to nostr-java-core/src/main/java/nostr/crypto/bech32/Bech32EncodingException.java diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32Prefix.java b/nostr-java-core/src/main/java/nostr/crypto/bech32/Bech32Prefix.java similarity index 100% rename from nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32Prefix.java rename to nostr-java-core/src/main/java/nostr/crypto/bech32/Bech32Prefix.java diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/nip04/EncryptedDirectMessage.java b/nostr-java-core/src/main/java/nostr/crypto/nip04/EncryptedDirectMessage.java similarity index 100% rename from nostr-java-crypto/src/main/java/nostr/crypto/nip04/EncryptedDirectMessage.java rename to nostr-java-core/src/main/java/nostr/crypto/nip04/EncryptedDirectMessage.java diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/nip44/EncryptedPayloads.java b/nostr-java-core/src/main/java/nostr/crypto/nip44/EncryptedPayloads.java similarity index 100% rename from nostr-java-crypto/src/main/java/nostr/crypto/nip44/EncryptedPayloads.java rename to nostr-java-core/src/main/java/nostr/crypto/nip44/EncryptedPayloads.java diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/schnorr/Schnorr.java b/nostr-java-core/src/main/java/nostr/crypto/schnorr/Schnorr.java similarity index 97% rename from nostr-java-crypto/src/main/java/nostr/crypto/schnorr/Schnorr.java rename to nostr-java-core/src/main/java/nostr/crypto/schnorr/Schnorr.java index a761c66a5..8e1ebbc71 100644 --- a/nostr-java-crypto/src/main/java/nostr/crypto/schnorr/Schnorr.java +++ b/nostr-java-core/src/main/java/nostr/crypto/schnorr/Schnorr.java @@ -1,185 +1,185 @@ -package nostr.crypto.schnorr; - -import nostr.crypto.Point; -import nostr.util.NostrUtil; -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -import java.math.BigInteger; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SecureRandom; -import java.security.Security; -import java.security.interfaces.ECPrivateKey; -import java.security.spec.ECGenParameterSpec; -import java.util.Arrays; - -/** - * Utility methods for BIP-340 Schnorr signatures over secp256k1. - * - *

Implements signing, verification, and simple key derivation helpers used throughout the - * project. All methods operate on 32-byte inputs as mandated by the specification. - */ -public class Schnorr { - - /** - * Create a Schnorr signature for a 32-byte message. - * - * @param msg 32-byte message hash to sign - * @param secKey 32-byte secret key - * @param auxRand auxiliary 32 random bytes used for nonce derivation - * @return the 64-byte signature (R || s) - * @throws SchnorrException if inputs are invalid or signing fails - */ - public static byte[] sign(byte[] msg, byte[] secKey, byte[] auxRand) throws SchnorrException { - if (msg.length != 32) { - throw new SchnorrException("The message must be a 32-byte array."); - } - BigInteger secKey0 = NostrUtil.bigIntFromBytes(secKey); - - if (!(BigInteger.ONE.compareTo(secKey0) <= 0 - && secKey0.compareTo(Point.getn().subtract(BigInteger.ONE)) <= 0)) { - throw new SchnorrException("The secret key must be an integer in the range 1..n-1."); - } - Point P = Point.mul(Point.getG(), secKey0); - if (!P.hasEvenY()) { - secKey0 = Point.getn().subtract(secKey0); - } - int len = NostrUtil.bytesFromBigInteger(secKey0).length + P.toBytes().length + msg.length; - byte[] buf = new byte[len]; - byte[] t = - NostrUtil.xor( - NostrUtil.bytesFromBigInteger(secKey0), taggedHashUnchecked("BIP0340/aux", auxRand)); - - if (t == null) { - throw new RuntimeException("Unexpected error. Null array"); - } - - System.arraycopy(t, 0, buf, 0, t.length); - System.arraycopy(P.toBytes(), 0, buf, t.length, P.toBytes().length); - System.arraycopy(msg, 0, buf, t.length + P.toBytes().length, msg.length); - BigInteger k0 = - NostrUtil.bigIntFromBytes(taggedHashUnchecked("BIP0340/nonce", buf)).mod(Point.getn()); - if (k0.compareTo(BigInteger.ZERO) == 0) { - throw new SchnorrException("Failure. This happens only with negligible probability."); - } - Point R = Point.mul(Point.getG(), k0); - BigInteger k; - if (!R.hasEvenY()) { - k = Point.getn().subtract(k0); - } else { - k = k0; - } - len = R.toBytes().length + P.toBytes().length + msg.length; - buf = new byte[len]; - System.arraycopy(R.toBytes(), 0, buf, 0, R.toBytes().length); - System.arraycopy(P.toBytes(), 0, buf, R.toBytes().length, P.toBytes().length); - System.arraycopy(msg, 0, buf, R.toBytes().length + P.toBytes().length, msg.length); - BigInteger e = - NostrUtil.bigIntFromBytes(taggedHashUnchecked("BIP0340/challenge", buf)).mod(Point.getn()); - BigInteger kes = k.add(e.multiply(secKey0)).mod(Point.getn()); - len = R.toBytes().length + NostrUtil.bytesFromBigInteger(kes).length; - byte[] sig = new byte[len]; - System.arraycopy(R.toBytes(), 0, sig, 0, R.toBytes().length); - System.arraycopy( - NostrUtil.bytesFromBigInteger(kes), - 0, - sig, - R.toBytes().length, - NostrUtil.bytesFromBigInteger(kes).length); - if (!verify(msg, P.toBytes(), sig)) { - throw new SchnorrException("The signature does not pass verification."); - } - return sig; - } - - /** - * Verify a Schnorr signature for a 32-byte message. - * - * @param msg 32-byte message hash to verify - * @param pubkey 32-byte x-only public key - * @param sig 64-byte signature (R || s) - * @return true if the signature is valid; false otherwise - * @throws SchnorrException if inputs are invalid - */ - public static boolean verify(byte[] msg, byte[] pubkey, byte[] sig) throws SchnorrException { - - if (msg.length != 32) { - throw new SchnorrException("The message must be a 32-byte array."); - } - - if (pubkey.length != 32) { - throw new SchnorrException("The public key must be a 32-byte array."); - } - if (sig.length != 64) { - throw new SchnorrException("The signature must be a 64-byte array."); - } - - Point P = Point.liftX(pubkey); - if (P == null) { - return false; - } - BigInteger r = NostrUtil.bigIntFromBytes(Arrays.copyOfRange(sig, 0, 32)); - BigInteger s = NostrUtil.bigIntFromBytes(Arrays.copyOfRange(sig, 32, 64)); - if (r.compareTo(Point.getp()) >= 0 || s.compareTo(Point.getn()) >= 0) { - return false; - } - int len = 32 + pubkey.length + msg.length; - byte[] buf = new byte[len]; - System.arraycopy(sig, 0, buf, 0, 32); - System.arraycopy(pubkey, 0, buf, 32, pubkey.length); - System.arraycopy(msg, 0, buf, 32 + pubkey.length, msg.length); - BigInteger e = - NostrUtil.bigIntFromBytes(taggedHashUnchecked("BIP0340/challenge", buf)).mod(Point.getn()); - Point R = Point.add(Point.mul(Point.getG(), s), Point.mul(P, Point.getn().subtract(e))); - return R != null && R.hasEvenY() && R.getX().compareTo(r) == 0; - } - - /** - * Generate a random private key suitable for secp256k1. - * - * @return a 32-byte private key - */ - public static byte[] generatePrivateKey() { - try { - Security.addProvider(new BouncyCastleProvider()); - KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDSA", "BC"); - kpg.initialize(new ECGenParameterSpec("secp256k1"), SecureRandom.getInstanceStrong()); - KeyPair processorKeyPair = kpg.genKeyPair(); - - return NostrUtil.bytesFromBigInteger(((ECPrivateKey) processorKeyPair.getPrivate()).getS()); - - } catch (InvalidAlgorithmParameterException - | NoSuchAlgorithmException - | NoSuchProviderException e) { - throw new RuntimeException(e); - } - } - - /** - * Derive the x-only public key bytes for a given private key. - * - * @param secKey 32-byte secret key - * @return the 32-byte x-only public key - * @throws SchnorrException if the private key is out of range - */ - public static byte[] genPubKey(byte[] secKey) throws SchnorrException { - BigInteger x = NostrUtil.bigIntFromBytes(secKey); - if (!(BigInteger.ONE.compareTo(x) <= 0 - && x.compareTo(Point.getn().subtract(BigInteger.ONE)) <= 0)) { - throw new SchnorrException("The secret key must be an integer in the range 1..n-1."); - } - Point ret = Point.mul(Point.G, x); - return Point.bytesFromPoint(ret); - } - - private static byte[] taggedHashUnchecked(String tag, byte[] msg) { - try { - return Point.taggedHash(tag, msg); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } -} +package nostr.crypto.schnorr; + +import nostr.crypto.Point; +import nostr.util.NostrUtil; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; + +/** + * Utility methods for BIP-340 Schnorr signatures over secp256k1. + * + *

Implements signing, verification, and simple key derivation helpers used throughout the + * project. All methods operate on 32-byte inputs as mandated by the specification. + */ +public class Schnorr { + + /** + * Create a Schnorr signature for a 32-byte message. + * + * @param msg 32-byte message hash to sign + * @param secKey 32-byte secret key + * @param auxRand auxiliary 32 random bytes used for nonce derivation + * @return the 64-byte signature (R || s) + * @throws SchnorrException if inputs are invalid or signing fails + */ + public static byte[] sign(byte[] msg, byte[] secKey, byte[] auxRand) throws SchnorrException { + if (msg.length != 32) { + throw new SchnorrException("The message must be a 32-byte array."); + } + BigInteger secKey0 = NostrUtil.bigIntFromBytes(secKey); + + if (!(BigInteger.ONE.compareTo(secKey0) <= 0 + && secKey0.compareTo(Point.getn().subtract(BigInteger.ONE)) <= 0)) { + throw new SchnorrException("The secret key must be an integer in the range 1..n-1."); + } + Point P = Point.mul(Point.getG(), secKey0); + if (!P.hasEvenY()) { + secKey0 = Point.getn().subtract(secKey0); + } + int len = NostrUtil.bytesFromBigInteger(secKey0).length + P.toBytes().length + msg.length; + byte[] buf = new byte[len]; + byte[] t = + NostrUtil.xor( + NostrUtil.bytesFromBigInteger(secKey0), taggedHashUnchecked("BIP0340/aux", auxRand)); + + if (t == null) { + throw new RuntimeException("Unexpected error. Null array"); + } + + System.arraycopy(t, 0, buf, 0, t.length); + System.arraycopy(P.toBytes(), 0, buf, t.length, P.toBytes().length); + System.arraycopy(msg, 0, buf, t.length + P.toBytes().length, msg.length); + BigInteger k0 = + NostrUtil.bigIntFromBytes(taggedHashUnchecked("BIP0340/nonce", buf)).mod(Point.getn()); + if (k0.compareTo(BigInteger.ZERO) == 0) { + throw new SchnorrException("Failure. This happens only with negligible probability."); + } + Point R = Point.mul(Point.getG(), k0); + BigInteger k; + if (!R.hasEvenY()) { + k = Point.getn().subtract(k0); + } else { + k = k0; + } + len = R.toBytes().length + P.toBytes().length + msg.length; + buf = new byte[len]; + System.arraycopy(R.toBytes(), 0, buf, 0, R.toBytes().length); + System.arraycopy(P.toBytes(), 0, buf, R.toBytes().length, P.toBytes().length); + System.arraycopy(msg, 0, buf, R.toBytes().length + P.toBytes().length, msg.length); + BigInteger e = + NostrUtil.bigIntFromBytes(taggedHashUnchecked("BIP0340/challenge", buf)).mod(Point.getn()); + BigInteger kes = k.add(e.multiply(secKey0)).mod(Point.getn()); + len = R.toBytes().length + NostrUtil.bytesFromBigInteger(kes).length; + byte[] sig = new byte[len]; + System.arraycopy(R.toBytes(), 0, sig, 0, R.toBytes().length); + System.arraycopy( + NostrUtil.bytesFromBigInteger(kes), + 0, + sig, + R.toBytes().length, + NostrUtil.bytesFromBigInteger(kes).length); + if (!verify(msg, P.toBytes(), sig)) { + throw new SchnorrException("The signature does not pass verification."); + } + return sig; + } + + /** + * Verify a Schnorr signature for a 32-byte message. + * + * @param msg 32-byte message hash to verify + * @param pubkey 32-byte x-only public key + * @param sig 64-byte signature (R || s) + * @return true if the signature is valid; false otherwise + * @throws SchnorrException if inputs are invalid + */ + public static boolean verify(byte[] msg, byte[] pubkey, byte[] sig) throws SchnorrException { + + if (msg.length != 32) { + throw new SchnorrException("The message must be a 32-byte array."); + } + + if (pubkey.length != 32) { + throw new SchnorrException("The public key must be a 32-byte array."); + } + if (sig.length != 64) { + throw new SchnorrException("The signature must be a 64-byte array."); + } + + Point P = Point.liftX(pubkey); + if (P == null) { + return false; + } + BigInteger r = NostrUtil.bigIntFromBytes(Arrays.copyOfRange(sig, 0, 32)); + BigInteger s = NostrUtil.bigIntFromBytes(Arrays.copyOfRange(sig, 32, 64)); + if (r.compareTo(Point.getp()) >= 0 || s.compareTo(Point.getn()) >= 0) { + return false; + } + int len = 32 + pubkey.length + msg.length; + byte[] buf = new byte[len]; + System.arraycopy(sig, 0, buf, 0, 32); + System.arraycopy(pubkey, 0, buf, 32, pubkey.length); + System.arraycopy(msg, 0, buf, 32 + pubkey.length, msg.length); + BigInteger e = + NostrUtil.bigIntFromBytes(taggedHashUnchecked("BIP0340/challenge", buf)).mod(Point.getn()); + Point R = Point.add(Point.mul(Point.getG(), s), Point.mul(P, Point.getn().subtract(e))); + return R != null && R.hasEvenY() && R.getX().compareTo(r) == 0; + } + + /** + * Generate a random private key suitable for secp256k1. + * + * @return a 32-byte private key + */ + public static byte[] generatePrivateKey() { + try { + Security.addProvider(new BouncyCastleProvider()); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDSA", "BC"); + kpg.initialize(new ECGenParameterSpec("secp256k1"), SecureRandom.getInstanceStrong()); + KeyPair processorKeyPair = kpg.genKeyPair(); + + return NostrUtil.bytesFromBigInteger(((ECPrivateKey) processorKeyPair.getPrivate()).getS()); + + } catch (InvalidAlgorithmParameterException + | NoSuchAlgorithmException + | NoSuchProviderException e) { + throw new RuntimeException(e); + } + } + + /** + * Derive the x-only public key bytes for a given private key. + * + * @param secKey 32-byte secret key + * @return the 32-byte x-only public key + * @throws SchnorrException if the private key is out of range + */ + public static byte[] genPubKey(byte[] secKey) throws SchnorrException { + BigInteger x = NostrUtil.bigIntFromBytes(secKey); + if (!(BigInteger.ONE.compareTo(x) <= 0 + && x.compareTo(Point.getn().subtract(BigInteger.ONE)) <= 0)) { + throw new SchnorrException("The secret key must be an integer in the range 1..n-1."); + } + Point ret = Point.mul(Point.G, x); + return Point.bytesFromPoint(ret); + } + + private static byte[] taggedHashUnchecked(String tag, byte[] msg) { + try { + return Point.taggedHash(tag, msg); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } +} diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/schnorr/SchnorrException.java b/nostr-java-core/src/main/java/nostr/crypto/schnorr/SchnorrException.java similarity index 100% rename from nostr-java-crypto/src/main/java/nostr/crypto/schnorr/SchnorrException.java rename to nostr-java-core/src/main/java/nostr/crypto/schnorr/SchnorrException.java diff --git a/nostr-java-util/src/main/java/nostr/util/NostrException.java b/nostr-java-core/src/main/java/nostr/util/NostrException.java similarity index 100% rename from nostr-java-util/src/main/java/nostr/util/NostrException.java rename to nostr-java-core/src/main/java/nostr/util/NostrException.java diff --git a/nostr-java-util/src/main/java/nostr/util/NostrUtil.java b/nostr-java-core/src/main/java/nostr/util/NostrUtil.java similarity index 77% rename from nostr-java-util/src/main/java/nostr/util/NostrUtil.java rename to nostr-java-core/src/main/java/nostr/util/NostrUtil.java index b5c3a2bb5..4df90ebe2 100644 --- a/nostr-java-util/src/main/java/nostr/util/NostrUtil.java +++ b/nostr-java-core/src/main/java/nostr/util/NostrUtil.java @@ -9,48 +9,33 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; +import java.util.HexFormat; /** * @author squirrel */ public class NostrUtil { - private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + private static final HexFormat HEX = HexFormat.of(); private static final SecureRandom RANDOM = new SecureRandom(); public static String bytesToHex(byte[] b) { - char[] hexChars = new char[b.length * 2]; - for (int j = 0; j < b.length; j++) { - int v = b[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars).toLowerCase(); + return HEX.formatHex(b); } public static byte[] hexToBytes(String s) { HexStringValidator.validateHex(s, 64); - return hexToBytesConvert(s); + return HEX.parseHex(s); } public static byte[] hex128ToBytes(String s) { HexStringValidator.validateHex(s, 128); - return hexToBytesConvert(s); + return HEX.parseHex(s); } public static byte[] nip04PubKeyHexToBytes(String s) { HexStringValidator.validateHex(s, 66); - return hexToBytesConvert(s); - } - - private static byte[] hexToBytesConvert(String s) { - int len = s.length(); - byte[] buf = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - buf[i / 2] = - (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); - } - return buf; + return HEX.parseHex(s); } public static byte[] bytesFromInt(int n) { diff --git a/nostr-java-util/src/main/java/nostr/util/exception/NostrCryptoException.java b/nostr-java-core/src/main/java/nostr/util/exception/NostrCryptoException.java similarity index 100% rename from nostr-java-util/src/main/java/nostr/util/exception/NostrCryptoException.java rename to nostr-java-core/src/main/java/nostr/util/exception/NostrCryptoException.java diff --git a/nostr-java-util/src/main/java/nostr/util/exception/NostrEncodingException.java b/nostr-java-core/src/main/java/nostr/util/exception/NostrEncodingException.java similarity index 100% rename from nostr-java-util/src/main/java/nostr/util/exception/NostrEncodingException.java rename to nostr-java-core/src/main/java/nostr/util/exception/NostrEncodingException.java diff --git a/nostr-java-util/src/main/java/nostr/util/exception/NostrNetworkException.java b/nostr-java-core/src/main/java/nostr/util/exception/NostrNetworkException.java similarity index 97% rename from nostr-java-util/src/main/java/nostr/util/exception/NostrNetworkException.java rename to nostr-java-core/src/main/java/nostr/util/exception/NostrNetworkException.java index 0409f8c06..d1555dfe6 100644 --- a/nostr-java-util/src/main/java/nostr/util/exception/NostrNetworkException.java +++ b/nostr-java-core/src/main/java/nostr/util/exception/NostrNetworkException.java @@ -112,9 +112,7 @@ *

  • nostr.websocket.poll-interval-ms: Polling interval for responses (default 500)
  • * * - * @see nostr.api.client.StandardWebSocketClient - * @see nostr.api.client.NostrSpringWebSocketClient - * @see nostr.config.RelayConfig + * @see nostr.client.springwebsocket.NostrRelayClient * @since 0.1.0 */ @StandardException diff --git a/nostr-java-util/src/main/java/nostr/util/exception/NostrProtocolException.java b/nostr-java-core/src/main/java/nostr/util/exception/NostrProtocolException.java similarity index 97% rename from nostr-java-util/src/main/java/nostr/util/exception/NostrProtocolException.java rename to nostr-java-core/src/main/java/nostr/util/exception/NostrProtocolException.java index b1546375c..68abd9400 100644 --- a/nostr-java-util/src/main/java/nostr/util/exception/NostrProtocolException.java +++ b/nostr-java-core/src/main/java/nostr/util/exception/NostrProtocolException.java @@ -39,7 +39,7 @@ *
    {@code
      * try {
      *     String relayMessage = "[\"INVALID\", \"malformed\"]";
    - *     GenericMessage message = GenericMessage.fromJson(relayMessage);
    + *     BaseMessage message = BaseMessageDecoder.decode(relayMessage);
      * } catch (NostrProtocolException e) {
      *     logger.error("Invalid relay message: {}", e.getMessage());
      *     // Ignore or log malformed messages from relay
    diff --git a/nostr-java-util/src/main/java/nostr/util/exception/NostrRuntimeException.java b/nostr-java-core/src/main/java/nostr/util/exception/NostrRuntimeException.java
    similarity index 100%
    rename from nostr-java-util/src/main/java/nostr/util/exception/NostrRuntimeException.java
    rename to nostr-java-core/src/main/java/nostr/util/exception/NostrRuntimeException.java
    diff --git a/nostr-java-util/src/main/java/nostr/util/validator/HexStringValidator.java b/nostr-java-core/src/main/java/nostr/util/validator/HexStringValidator.java
    similarity index 100%
    rename from nostr-java-util/src/main/java/nostr/util/validator/HexStringValidator.java
    rename to nostr-java-core/src/main/java/nostr/util/validator/HexStringValidator.java
    diff --git a/nostr-java-util/src/main/java/nostr/util/validator/Nip05Content.java b/nostr-java-core/src/main/java/nostr/util/validator/Nip05Content.java
    similarity index 100%
    rename from nostr-java-util/src/main/java/nostr/util/validator/Nip05Content.java
    rename to nostr-java-core/src/main/java/nostr/util/validator/Nip05Content.java
    diff --git a/nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java b/nostr-java-core/src/main/java/nostr/util/validator/Nip05Validator.java
    similarity index 58%
    rename from nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java
    rename to nostr-java-core/src/main/java/nostr/util/validator/Nip05Validator.java
    index ff0c4cf3f..3657786b6 100644
    --- a/nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java
    +++ b/nostr-java-core/src/main/java/nostr/util/validator/Nip05Validator.java
    @@ -8,10 +8,8 @@
     import lombok.Data;
     import lombok.extern.slf4j.Slf4j;
     import nostr.util.NostrException;
    -import nostr.util.http.DefaultHttpClientProvider;
    -import nostr.util.http.HttpClientProvider;
    -
     import java.io.IOException;
    +import java.util.function.Function;
     import java.net.IDN;
     import java.net.URI;
     import java.net.URISyntaxException;
    @@ -21,8 +19,14 @@
     import java.net.http.HttpResponse;
     import java.nio.charset.StandardCharsets;
     import java.time.Duration;
    +import java.util.List;
     import java.util.Locale;
     import java.util.Map;
    +import java.util.Objects;
    +import java.util.concurrent.CompletableFuture;
    +import java.util.concurrent.CompletionException;
    +import java.util.concurrent.Executor;
    +import java.util.concurrent.ThreadFactory;
     import java.util.regex.Pattern;
     
     /**
    @@ -42,13 +46,40 @@ public class Nip05Validator {
       @Builder.Default @JsonIgnore private final Duration requestTimeout = Duration.ofSeconds(5);
     
       @Builder.Default @JsonIgnore
    -  private final HttpClientProvider httpClientProvider = new DefaultHttpClientProvider();
    +  private final Function httpClientFactory = DEFAULT_HTTP_CLIENT_FACTORY;
     
       private static final Pattern LOCAL_PART_PATTERN = Pattern.compile("^[a-zA-Z0-9-_.]+$");
       private static final Pattern DOMAIN_PATTERN = Pattern.compile("^[A-Za-z0-9.-]+(:\\d{1,5})?$");
    +  private static final ThreadFactory VALIDATION_THREAD_FACTORY =
    +      Thread.ofVirtual().name("nostr-nip05-", 0).factory();
    +  private static final Executor VALIDATION_EXECUTOR =
    +      command -> VALIDATION_THREAD_FACTORY.newThread(command).start();
    +  private static final ThreadFactory HTTP_THREAD_FACTORY =
    +      Thread.ofVirtual().name("nostr-http-", 0).factory();
    +  private static final Executor HTTP_EXECUTOR =
    +      command -> HTTP_THREAD_FACTORY.newThread(command).start();
    +  private static final Function DEFAULT_HTTP_CLIENT_FACTORY =
    +      timeout -> HttpClient.newBuilder().connectTimeout(timeout).executor(HTTP_EXECUTOR).build();
       private static final ObjectMapper MAPPER_BLACKBIRD =
           JsonMapper.builder().addModule(new BlackbirdModule()).build();
     
    +  /**
    +   * Result of a NIP-05 validation attempt.
    +   *
    +   * @param nip05 identifier that was validated
    +   * @param valid true when validation succeeded
    +   * @param errorMessage failure reason when {@code valid} is false
    +   */
    +  public record ValidationResult(String nip05, boolean valid, String errorMessage) {
    +    static ValidationResult success(String nip05) {
    +      return new ValidationResult(nip05, true, null);
    +    }
    +
    +    static ValidationResult failure(String nip05, String errorMessage) {
    +      return new ValidationResult(nip05, false, errorMessage);
    +    }
    +  }
    +
     
       /**
        * Validate the nip05 identifier by checking the public key registered on the remote server.
    @@ -92,9 +123,56 @@ public void validate() throws NostrException {
         validatePublicKey(host, port, localPart);
       }
     
    +  /**
    +   * Validate the NIP-05 identifier asynchronously on a Virtual Thread.
    +   *
    +   * @return future that completes when validation succeeds
    +   */
    +  public CompletableFuture validateAsync() {
    +    return CompletableFuture.runAsync(
    +        () -> {
    +          try {
    +            validate();
    +          } catch (NostrException ex) {
    +            throw new CompletionException(ex);
    +          }
    +        },
    +        VALIDATION_EXECUTOR);
    +  }
    +
    +  /**
    +   * Validate many NIP-05 identifiers in parallel on Virtual Threads.
    +   *
    +   * @param validators validators to execute
    +   * @return ordered list of per-identifier validation results
    +   */
    +  public static List validateBatch(List validators) {
    +    Objects.requireNonNull(validators, "validators");
    +    if (validators.isEmpty()) {
    +      return List.of();
    +    }
    +
    +    List> futures =
    +        validators.stream()
    +            .map(
    +                validator -> {
    +                  Objects.requireNonNull(validator, "validators contains null entry");
    +                  return validator
    +                      .validateAsync()
    +                      .thenApply(ignored -> ValidationResult.success(validator.getNip05()))
    +                      .exceptionally(
    +                          failure ->
    +                              ValidationResult.failure(
    +                                  validator.getNip05(), extractValidationError(failure)));
    +                })
    +            .toList();
    +
    +    return futures.stream().map(CompletableFuture::join).toList();
    +  }
    +
       private void validatePublicKey(String host, int port, String localPart) throws NostrException {
         @SuppressWarnings("resource")
    -    HttpClient client = httpClientProvider.create(connectTimeout);
    +    HttpClient client = httpClientFactory.apply(connectTimeout);
     
         URI uri;
         try {
    @@ -156,4 +234,13 @@ private String getPublicKey(String content, String localPart) throws NostrExcept
         }
         return names.get(localPart);
       }
    +
    +  private static String extractValidationError(Throwable failure) {
    +    Throwable cause = failure;
    +    while (cause instanceof CompletionException && cause.getCause() != null) {
    +      cause = cause.getCause();
    +    }
    +    String message = cause.getMessage();
    +    return message == null || message.isBlank() ? cause.getClass().getSimpleName() : message;
    +  }
     }
    diff --git a/nostr-java-crypto/src/test/java/nostr/crypto/CryptoTest.java b/nostr-java-core/src/test/java/nostr/crypto/CryptoTest.java
    similarity index 100%
    rename from nostr-java-crypto/src/test/java/nostr/crypto/CryptoTest.java
    rename to nostr-java-core/src/test/java/nostr/crypto/CryptoTest.java
    diff --git a/nostr-java-crypto/src/test/java/nostr/crypto/PointTest.java b/nostr-java-core/src/test/java/nostr/crypto/PointTest.java
    similarity index 100%
    rename from nostr-java-crypto/src/test/java/nostr/crypto/PointTest.java
    rename to nostr-java-core/src/test/java/nostr/crypto/PointTest.java
    diff --git a/nostr-java-crypto/src/test/java/nostr/crypto/bech32/Bech32Test.java b/nostr-java-core/src/test/java/nostr/crypto/bech32/Bech32Test.java
    similarity index 100%
    rename from nostr-java-crypto/src/test/java/nostr/crypto/bech32/Bech32Test.java
    rename to nostr-java-core/src/test/java/nostr/crypto/bech32/Bech32Test.java
    diff --git a/nostr-java-crypto/src/test/java/nostr/crypto/schnorr/SchnorrTest.java b/nostr-java-core/src/test/java/nostr/crypto/schnorr/SchnorrTest.java
    similarity index 100%
    rename from nostr-java-crypto/src/test/java/nostr/crypto/schnorr/SchnorrTest.java
    rename to nostr-java-core/src/test/java/nostr/crypto/schnorr/SchnorrTest.java
    diff --git a/nostr-java-util/src/test/java/nostr/util/NostrUtilExtendedTest.java b/nostr-java-core/src/test/java/nostr/util/NostrUtilExtendedTest.java
    similarity index 100%
    rename from nostr-java-util/src/test/java/nostr/util/NostrUtilExtendedTest.java
    rename to nostr-java-core/src/test/java/nostr/util/NostrUtilExtendedTest.java
    diff --git a/nostr-java-util/src/test/java/nostr/util/NostrUtilRandomTest.java b/nostr-java-core/src/test/java/nostr/util/NostrUtilRandomTest.java
    similarity index 100%
    rename from nostr-java-util/src/test/java/nostr/util/NostrUtilRandomTest.java
    rename to nostr-java-core/src/test/java/nostr/util/NostrUtilRandomTest.java
    diff --git a/nostr-java-util/src/test/java/nostr/util/NostrUtilTest.java b/nostr-java-core/src/test/java/nostr/util/NostrUtilTest.java
    similarity index 100%
    rename from nostr-java-util/src/test/java/nostr/util/NostrUtilTest.java
    rename to nostr-java-core/src/test/java/nostr/util/NostrUtilTest.java
    diff --git a/nostr-java-util/src/test/java/nostr/util/validator/HexStringValidatorTest.java b/nostr-java-core/src/test/java/nostr/util/validator/HexStringValidatorTest.java
    similarity index 100%
    rename from nostr-java-util/src/test/java/nostr/util/validator/HexStringValidatorTest.java
    rename to nostr-java-core/src/test/java/nostr/util/validator/HexStringValidatorTest.java
    diff --git a/nostr-java-util/src/test/java/nostr/util/validator/Nip05ValidatorTest.java b/nostr-java-core/src/test/java/nostr/util/validator/Nip05ValidatorTest.java
    similarity index 71%
    rename from nostr-java-util/src/test/java/nostr/util/validator/Nip05ValidatorTest.java
    rename to nostr-java-core/src/test/java/nostr/util/validator/Nip05ValidatorTest.java
    index df696fd14..61b509fd9 100644
    --- a/nostr-java-util/src/test/java/nostr/util/validator/Nip05ValidatorTest.java
    +++ b/nostr-java-core/src/test/java/nostr/util/validator/Nip05ValidatorTest.java
    @@ -1,7 +1,6 @@
     package nostr.util.validator;
     
     import nostr.util.NostrException;
    -import nostr.util.http.HttpClientProvider;
     import org.junit.jupiter.api.Test;
     
     import java.io.IOException;
    @@ -12,14 +11,17 @@
     import java.net.http.HttpRequest;
     import java.net.http.HttpResponse;
     import java.time.Duration;
    +import java.util.List;
     import java.util.Collections;
     import java.util.Optional;
     import java.util.concurrent.CompletableFuture;
     
     import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
     import static org.junit.jupiter.api.Assertions.assertEquals;
    +import static org.junit.jupiter.api.Assertions.assertFalse;
     import static org.junit.jupiter.api.Assertions.assertNull;
     import static org.junit.jupiter.api.Assertions.assertThrows;
    +import static org.junit.jupiter.api.Assertions.assertTrue;
     
     public class Nip05ValidatorTest {
     
    @@ -48,7 +50,7 @@ public void testSuccessfulValidation() {
             Nip05Validator.builder()
                 .nip05("alice@example.com")
                 .publicKey("pub")
    -            .httpClientProvider(new FixedHttpClientProvider(client))
    +            .httpClientFactory(timeout -> client)
                 .build();
         assertDoesNotThrow(validator::validate);
       }
    @@ -62,11 +64,69 @@ public void testMismatchedPublicKey() {
             Nip05Validator.builder()
                 .nip05("alice@example.com")
                 .publicKey("pub")
    -            .httpClientProvider(new FixedHttpClientProvider(client))
    +            .httpClientFactory(timeout -> client)
                 .build();
         assertThrows(NostrException.class, validator::validate);
       }
     
    +  /* Validates asynchronous NIP-05 checks execute successfully on a completion stage. */
    +  @Test
    +  public void testAsyncValidation() {
    +    HttpResponse resp = new MockHttpResponse(200, "{\"names\":{\"alice\":\"pub\"}}");
    +    HttpClient client = new MockHttpClient(resp);
    +    Nip05Validator validator =
    +        Nip05Validator.builder()
    +            .nip05("alice@example.com")
    +            .publicKey("pub")
    +            .httpClientFactory(timeout -> client)
    +            .build();
    +    assertDoesNotThrow(() -> validator.validateAsync().join());
    +  }
    +
    +  /* Validates batched NIP-05 checks run in parallel and return per-item outcomes. */
    +  @Test
    +  public void testBatchValidationMixedOutcomes() {
    +    Nip05Validator successValidator =
    +        Nip05Validator.builder()
    +            .nip05("alice@example.com")
    +            .publicKey("pub")
    +            .httpClientFactory(timeout ->
    +                    new MockHttpClient(new MockHttpResponse(200, "{\"names\":{\"alice\":\"pub\"}}")))
    +            .build();
    +
    +    Nip05Validator mismatchValidator =
    +        Nip05Validator.builder()
    +            .nip05("bob@example.com")
    +            .publicKey("pub")
    +            .httpClientFactory(timeout ->
    +                    new MockHttpClient(new MockHttpResponse(200, "{\"names\":{\"bob\":\"wrong\"}}")))
    +            .build();
    +
    +    Nip05Validator failingValidator =
    +        Nip05Validator.builder()
    +            .nip05("charlie@example.com")
    +            .publicKey("pub")
    +            .httpClientFactory(timeout -> new MockHttpClient(new IOException("boom")))
    +            .build();
    +
    +    List results =
    +        Nip05Validator.validateBatch(
    +            List.of(successValidator, mismatchValidator, failingValidator));
    +
    +    assertEquals(3, results.size());
    +    assertEquals("alice@example.com", results.get(0).nip05());
    +    assertTrue(results.get(0).valid());
    +    assertNull(results.get(0).errorMessage());
    +
    +    assertEquals("bob@example.com", results.get(1).nip05());
    +    assertFalse(results.get(1).valid());
    +    assertTrue(results.get(1).errorMessage().contains("Public key mismatch"));
    +
    +    assertEquals("charlie@example.com", results.get(2).nip05());
    +    assertFalse(results.get(2).valid());
    +    assertTrue(results.get(2).errorMessage().contains("Error querying"));
    +  }
    +
       /* Propagates network failures with descriptive messages. */
       @Test
       public void testNetworkFailure() {
    @@ -75,7 +135,7 @@ public void testNetworkFailure() {
             Nip05Validator.builder()
                 .nip05("alice@example.com")
                 .publicKey("pub")
    -            .httpClientProvider(new FixedHttpClientProvider(client))
    +            .httpClientFactory(timeout -> client)
                 .build();
         assertThrows(NostrException.class, validator::validate);
       }
    @@ -94,19 +154,6 @@ public void testGetPublicKeyViaReflection() throws Exception {
         assertNull(missing);
       }
     
    -  private static class FixedHttpClientProvider implements HttpClientProvider {
    -    private final HttpClient client;
    -
    -    FixedHttpClientProvider(HttpClient client) {
    -      this.client = client;
    -    }
    -
    -    @Override
    -    public HttpClient create(Duration connectTimeout) {
    -      return client;
    -    }
    -  }
    -
       private static class MockHttpClient extends HttpClient {
         private final HttpResponse response;
         private final IOException exception;
    diff --git a/nostr-java-util/src/test/resources/application-test.properties b/nostr-java-core/src/test/resources/application-test.properties
    similarity index 100%
    rename from nostr-java-util/src/test/resources/application-test.properties
    rename to nostr-java-core/src/test/resources/application-test.properties
    diff --git a/nostr-java-base/src/test/resources/junit-platform.properties b/nostr-java-core/src/test/resources/junit-platform.properties
    similarity index 100%
    rename from nostr-java-base/src/test/resources/junit-platform.properties
    rename to nostr-java-core/src/test/resources/junit-platform.properties
    diff --git a/nostr-java-crypto/pom.xml b/nostr-java-crypto/pom.xml
    deleted file mode 100644
    index 23b02b2f3..000000000
    --- a/nostr-java-crypto/pom.xml
    +++ /dev/null
    @@ -1,73 +0,0 @@
    -
    -    4.0.0
    -
    -    
    -        xyz.tcheeric
    -        nostr-java
    -        1.3.0
    -        ../pom.xml
    -    
    -    
    -    nostr-java-crypto
    -    jar
    -    nostr-java-crypto
    -
    -    
    -        
    -            reposilite-releases
    -            https://maven.398ja.xyz/releases
    -        
    -        
    -            reposilite-snapshots
    -            https://maven.398ja.xyz/snapshots
    -        
    -    
    -
    -    
    -        A simple Java implementation (no external libs) of Sipa's Python reference implementation test vectors for BIP340 Schnorr signatures for secp256k1.
    -
    -        Sources:
    -        https://code.samourai.io/samouraidev/BIP340_Schnorr and
    -        https://github.com/unclebob/more-speech/tree/bdd2f32b37264f20bf6abb4887489e70d2b0fdf1
    -    
    -
    -    
    -        
    -        
    -            org.bouncycastle
    -            bcprov-jdk18on
    -        
    -
    -        
    -        
    -            ${project.groupId}
    -            nostr-java-util
    -            
    -        
    -
    -        
    -        
    -            org.projectlombok
    -            lombok
    -            
    -            provided
    -        
    -        
    -            org.slf4j
    -            slf4j-api
    -        
    -
    -        
    -        
    -            org.junit.jupiter
    -            junit-jupiter
    -            
    -            test
    -        
    -        
    -            org.junit.platform
    -            junit-platform-launcher
    -            test
    -        
    -    
    -
    diff --git a/nostr-java-crypto/src/test/resources/application-test.properties b/nostr-java-crypto/src/test/resources/application-test.properties
    deleted file mode 100644
    index 887cd2666..000000000
    --- a/nostr-java-crypto/src/test/resources/application-test.properties
    +++ /dev/null
    @@ -1,4 +0,0 @@
    -spring.threads.virtual.enabled=true
    -
    -logging.level.nostr.crypto=INFO
    -logging.pattern.console=%msg%n
    diff --git a/nostr-java-encryption/pom.xml b/nostr-java-encryption/pom.xml
    deleted file mode 100644
    index d960cd820..000000000
    --- a/nostr-java-encryption/pom.xml
    +++ /dev/null
    @@ -1,63 +0,0 @@
    -
    -    4.0.0
    -
    -    
    -        xyz.tcheeric
    -        nostr-java
    -        1.3.0
    -        ../pom.xml
    -    
    -    
    -    nostr-java-encryption
    -    jar
    -
    -    nostr-java-encryption
    -    http://maven.apache.org
    -
    -    
    -        
    -            reposilite-releases
    -            https://maven.398ja.xyz/releases
    -        
    -        
    -            reposilite-snapshots
    -            https://maven.398ja.xyz/snapshots
    -        
    -    
    -
    -    
    -        
    -        
    -            ${project.groupId}
    -            nostr-java-crypto
    -            
    -        
    -        
    -            ${project.groupId}
    -            nostr-java-util
    -            
    -        
    -
    -        
    -        
    -            org.projectlombok
    -            lombok
    -            
    -            provided
    -        
    -
    -        
    -        
    -            org.junit.jupiter
    -            junit-jupiter
    -            
    -            test
    -        
    -        
    -            org.junit.platform
    -            junit-platform-launcher
    -            test
    -        
    -    
    -
    -
    diff --git a/nostr-java-event/pom.xml b/nostr-java-event/pom.xml
    index 129f8eb88..0fc4b5f0f 100644
    --- a/nostr-java-event/pom.xml
    +++ b/nostr-java-event/pom.xml
    @@ -4,10 +4,10 @@
         
             xyz.tcheeric
             nostr-java
    -        1.3.0
    +        2.0.0
             ../pom.xml
         
    -    
    +
         nostr-java-event
         jar
         nostr-java-event
    @@ -27,19 +27,10 @@
             
             
                 ${project.groupId}
    -            nostr-java-base
    -            
    -        
    -        
    -            ${project.groupId}
    -            nostr-java-crypto
    -            
    -        
    -        
    -            ${project.groupId}
    -            nostr-java-util
    -            
    +            nostr-java-core
             
    +
    +        
             
                 com.fasterxml.jackson.core
                 jackson-databind
    @@ -47,22 +38,23 @@
             
                 com.fasterxml.jackson.module
                 jackson-module-blackbird
    -            
             
    +
    +        
             
                 org.projectlombok
                 lombok
    -            
                 provided
             
             
                 org.slf4j
                 slf4j-api
             
    +
    +        
             
                 org.junit.jupiter
                 junit-jupiter
    -            
                 test
             
             
    diff --git a/nostr-java-base/src/main/java/nostr/base/BaseKey.java b/nostr-java-event/src/main/java/nostr/base/BaseKey.java
    similarity index 95%
    rename from nostr-java-base/src/main/java/nostr/base/BaseKey.java
    rename to nostr-java-event/src/main/java/nostr/base/BaseKey.java
    index 939b6614d..dc8b6db47 100644
    --- a/nostr-java-base/src/main/java/nostr/base/BaseKey.java
    +++ b/nostr-java-event/src/main/java/nostr/base/BaseKey.java
    @@ -11,6 +11,7 @@
     import nostr.crypto.bech32.Bech32Prefix;
     import nostr.util.NostrUtil;
     
    +import java.io.Serializable;
     import java.util.Arrays;
     
     /**
    @@ -19,7 +20,7 @@
     @AllArgsConstructor
     @Data
     @Slf4j
    -public abstract class BaseKey implements IKey {
    +public abstract class BaseKey implements Serializable {
     
       @NonNull @EqualsAndHashCode.Exclude protected final KeyType type;
     
    @@ -27,7 +28,6 @@ public abstract class BaseKey implements IKey {
     
       protected final Bech32Prefix prefix;
     
    -  @Override
       public String toBech32String() {
         try {
           return Bech32.toBech32(prefix, rawData);
    diff --git a/nostr-java-base/src/main/java/nostr/base/Command.java b/nostr-java-event/src/main/java/nostr/base/Command.java
    similarity index 100%
    rename from nostr-java-base/src/main/java/nostr/base/Command.java
    rename to nostr-java-event/src/main/java/nostr/base/Command.java
    diff --git a/nostr-java-base/src/main/java/nostr/base/Encoder.java b/nostr-java-event/src/main/java/nostr/base/Encoder.java
    similarity index 100%
    rename from nostr-java-base/src/main/java/nostr/base/Encoder.java
    rename to nostr-java-event/src/main/java/nostr/base/Encoder.java
    diff --git a/nostr-java-base/src/main/java/nostr/base/IDecoder.java b/nostr-java-event/src/main/java/nostr/base/IDecoder.java
    similarity index 94%
    rename from nostr-java-base/src/main/java/nostr/base/IDecoder.java
    rename to nostr-java-event/src/main/java/nostr/base/IDecoder.java
    index 855f0862c..9aa2a333f 100644
    --- a/nostr-java-base/src/main/java/nostr/base/IDecoder.java
    +++ b/nostr-java-event/src/main/java/nostr/base/IDecoder.java
    @@ -10,7 +10,7 @@
      * @author eric
      * @param 
      */
    -public interface IDecoder {
    +public interface IDecoder {
       ObjectMapper I_DECODER_MAPPER_BLACKBIRD =
           JsonMapper.builder()
               .addModule(new BlackbirdModule())
    diff --git a/nostr-java-base/src/main/java/nostr/base/ISignable.java b/nostr-java-event/src/main/java/nostr/base/ISignable.java
    similarity index 100%
    rename from nostr-java-base/src/main/java/nostr/base/ISignable.java
    rename to nostr-java-event/src/main/java/nostr/base/ISignable.java
    diff --git a/nostr-java-base/src/main/java/nostr/base/KeyEncodingException.java b/nostr-java-event/src/main/java/nostr/base/KeyEncodingException.java
    similarity index 100%
    rename from nostr-java-base/src/main/java/nostr/base/KeyEncodingException.java
    rename to nostr-java-event/src/main/java/nostr/base/KeyEncodingException.java
    diff --git a/nostr-java-base/src/main/java/nostr/base/KeyType.java b/nostr-java-event/src/main/java/nostr/base/KeyType.java
    similarity index 100%
    rename from nostr-java-base/src/main/java/nostr/base/KeyType.java
    rename to nostr-java-event/src/main/java/nostr/base/KeyType.java
    diff --git a/nostr-java-event/src/main/java/nostr/base/Kinds.java b/nostr-java-event/src/main/java/nostr/base/Kinds.java
    new file mode 100644
    index 000000000..c16f411da
    --- /dev/null
    +++ b/nostr-java-event/src/main/java/nostr/base/Kinds.java
    @@ -0,0 +1,76 @@
    +package nostr.base;
    +
    +import java.time.temporal.ValueRange;
    +
    +/**
    + * Constants and utility methods for Nostr event kinds.
    + *
    + * 

    This replaces the Kind enum with simple int constants and static range-check methods. + */ +public final class Kinds { + + private Kinds() {} + + // Standard event kinds + public static final int SET_METADATA = 0; + public static final int TEXT_NOTE = 1; + public static final int RECOMMEND_SERVER = 2; + public static final int CONTACT_LIST = 3; + public static final int ENCRYPTED_DIRECT_MESSAGE = 4; + public static final int DELETION = 5; + public static final int REPOST = 6; + public static final int REACTION = 7; + public static final int REACTION_TO_WEBSITE = 17; + public static final int CHANNEL_CREATE = 40; + public static final int CHANNEL_METADATA = 41; + public static final int CHANNEL_MESSAGE = 42; + public static final int HIDE_MESSAGE = 43; + public static final int MUTE_USER = 44; + public static final int OTS_EVENT = 1040; + public static final int REPORT = 1984; + public static final int COINJOIN_POOL = 2022; + public static final int RESERVED_CASHU_WALLET_TOKENS = 7_374; + public static final int WALLET_UNSPENT_PROOF = 7_375; + public static final int WALLET_TX_HISTORY = 7_376; + public static final int NUTZAP = 9_321; + public static final int ZAP_REQUEST = 9_734; + public static final int ZAP_RECEIPT = 9_735; + public static final int REPLACEABLE_EVENT = 10_000; + public static final int PIN_LIST = 10_001; + public static final int RELAY_LIST_METADATA = 10_002; + public static final int NUTZAP_INFORMATIONAL = 10_019; + public static final int WALLET = 17_375; + public static final int EPHEMERAL_EVENT = 20_000; + public static final int CLIENT_AUTH = 22_242; + public static final int NOSTR_CONNECT = 24_133; + public static final int ADDRESSABLE_EVENT = 30_000; + public static final int BADGE_DEFINITION = 30_008; + public static final int BADGE_AWARD = 30_009; + public static final int STALL_CREATE_OR_UPDATE = 30_017; + public static final int PRODUCT_CREATE_OR_UPDATE = 30_018; + public static final int LONG_FORM_TEXT_NOTE = 30_023; + public static final int LONG_FORM_DRAFT = 30_024; + public static final int APPLICATION_SPECIFIC_DATA = 30_078; + public static final int CLASSIFIED_LISTING = 30_402; + public static final int CLASSIFIED_LISTING_INACTIVE = 30_403; + public static final int CALENDAR_DATE_BASED_EVENT = 31_922; + public static final int CALENDAR_TIME_BASED_EVENT = 31_923; + public static final int CALENDAR_EVENT = 31_924; + public static final int CALENDAR_RSVP_EVENT = 31_925; + + public static boolean isValid(int kind) { + return ValueRange.of(0, 65_535).isValidIntValue(kind); + } + + public static boolean isReplaceable(int kind) { + return kind >= 10_000 && kind < 20_000; + } + + public static boolean isEphemeral(int kind) { + return kind >= 20_000 && kind < 30_000; + } + + public static boolean isAddressable(int kind) { + return kind >= 30_000 && kind < 40_000; + } +} diff --git a/nostr-java-base/src/main/java/nostr/base/NipConstants.java b/nostr-java-event/src/main/java/nostr/base/NipConstants.java similarity index 100% rename from nostr-java-base/src/main/java/nostr/base/NipConstants.java rename to nostr-java-event/src/main/java/nostr/base/NipConstants.java diff --git a/nostr-java-base/src/main/java/nostr/base/PrivateKey.java b/nostr-java-event/src/main/java/nostr/base/PrivateKey.java similarity index 100% rename from nostr-java-base/src/main/java/nostr/base/PrivateKey.java rename to nostr-java-event/src/main/java/nostr/base/PrivateKey.java diff --git a/nostr-java-base/src/main/java/nostr/base/PublicKey.java b/nostr-java-event/src/main/java/nostr/base/PublicKey.java similarity index 100% rename from nostr-java-base/src/main/java/nostr/base/PublicKey.java rename to nostr-java-event/src/main/java/nostr/base/PublicKey.java diff --git a/nostr-java-base/src/main/java/nostr/base/Relay.java b/nostr-java-event/src/main/java/nostr/base/Relay.java similarity index 100% rename from nostr-java-base/src/main/java/nostr/base/Relay.java rename to nostr-java-event/src/main/java/nostr/base/Relay.java diff --git a/nostr-java-base/src/main/java/nostr/base/Signature.java b/nostr-java-event/src/main/java/nostr/base/Signature.java similarity index 100% rename from nostr-java-base/src/main/java/nostr/base/Signature.java rename to nostr-java-event/src/main/java/nostr/base/Signature.java diff --git a/nostr-java-base/src/main/java/nostr/base/SubscriptionId.java b/nostr-java-event/src/main/java/nostr/base/SubscriptionId.java similarity index 100% rename from nostr-java-base/src/main/java/nostr/base/SubscriptionId.java rename to nostr-java-event/src/main/java/nostr/base/SubscriptionId.java diff --git a/nostr-java-base/src/main/java/nostr/base/json/EventJsonMapper.java b/nostr-java-event/src/main/java/nostr/base/json/EventJsonMapper.java similarity index 100% rename from nostr-java-base/src/main/java/nostr/base/json/EventJsonMapper.java rename to nostr-java-event/src/main/java/nostr/base/json/EventJsonMapper.java diff --git a/nostr-java-event/src/main/java/nostr/event/BaseEvent.java b/nostr-java-event/src/main/java/nostr/event/BaseEvent.java deleted file mode 100644 index 77ea7a643..000000000 --- a/nostr-java-event/src/main/java/nostr/event/BaseEvent.java +++ /dev/null @@ -1,58 +0,0 @@ -package nostr.event; - -import lombok.NoArgsConstructor; -import nostr.base.IEvent; - -/** - * Base class for all Nostr event implementations. - * - *

    This abstract class provides a common foundation for all event types in the Nostr protocol. - * It implements the {@link IEvent} interface which defines the core contract for events, - * including event ID retrieval and Bech32 encoding support (NIP-19). - * - *

    Hierarchy: - *

    - * BaseEvent (abstract)
    - *   ├─ GenericEvent (NIP-01 implementation)
    - *   │   ├─ CustomEmojiEvent (NIP-30)
    - *   │   ├─ EncryptedDirectMessageEvent (NIP-04)
    - *   │   ├─ GenericMetadataEvent (NIP-01)
    - *   │   ├─ CalendarEvent (NIP-52)
    - *   │   └─ ... (other NIP-specific events)
    - *   └─ Other custom event implementations
    - * 
    - * - *

    Design: This class follows the Template Method pattern, providing the base - * structure while allowing subclasses to implement specific event behavior. Most event - * implementations extend {@link nostr.event.impl.GenericEvent} which provides the full - * NIP-01 event structure. - * - *

    Usage: Typically, you don't extend this class directly. Instead: - *

      - *
    • Use {@link nostr.event.impl.GenericEvent} for basic NIP-01 events
    • - *
    • Extend {@link nostr.event.impl.GenericEvent} for NIP-specific events
    • - *
    • Only extend {@code BaseEvent} directly for custom, non-standard event types
    • - *
    - * - *

    Example: - *

    {@code
    - * // Most common: Use GenericEvent directly
    - * GenericEvent event = GenericEvent.builder()
    - *     .kind(Kind.TEXT_NOTE)
    - *     .content("Hello Nostr!")
    - *     .build();
    - *
    - * // Or use NIP-specific implementations that extend GenericEvent
    - * CalendarEvent calendarEvent = CalendarEvent.builder()
    - *     .name("Nostr Conference 2025")
    - *     .start(startTime)
    - *     .build();
    - * }
    - * - * @see nostr.event.impl.GenericEvent - * @see IEvent - * @see NIP-01 - * @since 0.1.0 - */ -@NoArgsConstructor -public abstract class BaseEvent implements IEvent {} diff --git a/nostr-java-event/src/main/java/nostr/event/BaseMessage.java b/nostr-java-event/src/main/java/nostr/event/BaseMessage.java index 59ea22ca2..2bc2949bc 100644 --- a/nostr-java-event/src/main/java/nostr/event/BaseMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/BaseMessage.java @@ -1,14 +1,13 @@ package nostr.event; import lombok.Getter; -import nostr.base.IElement; import nostr.event.json.codec.EventEncodingException; /** * @author squirrel */ @Getter -public abstract class BaseMessage implements IElement { +public abstract class BaseMessage { private final String command; protected BaseMessage(String command) { diff --git a/nostr-java-event/src/main/java/nostr/event/BaseTag.java b/nostr-java-event/src/main/java/nostr/event/BaseTag.java index 60dfe86c7..4a32c75b4 100644 --- a/nostr-java-event/src/main/java/nostr/event/BaseTag.java +++ b/nostr-java-event/src/main/java/nostr/event/BaseTag.java @@ -1,294 +1,33 @@ package nostr.event; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NonNull; import lombok.ToString; -import nostr.base.ElementAttribute; -import nostr.base.IEvent; -import nostr.base.ITag; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; import nostr.event.json.deserializer.TagDeserializer; import nostr.event.json.serializer.BaseTagSerializer; import nostr.event.tag.GenericTag; -import nostr.event.tag.TagRegistry; -import org.apache.commons.lang3.stream.Streams; -import java.beans.IntrospectionException; -import java.beans.PropertyDescriptor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.util.Arrays; import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -/** - * Base class for all Nostr event tags. - * - *

    Tags are metadata elements attached to Nostr events, defined as arrays in the NIP-01 - * specification. Each tag has a code (the first element) followed by one or more parameters. - * This class provides the foundation for all tag implementations in the library. - * - *

    Tag Structure: - *

    {@code
    - * // JSON representation
    - * ["e", "event_id", "relay_url", "marker"]
    - *  ^     ^            ^            ^
    - *  |     |            |            |
    - * code  param0       param1      param2
    - * }
    - * - *

    Common Tag Types: - *

      - *
    • e tag: References another event (EventTag)
    • - *
    • p tag: References a user's public key (PubKeyTag)
    • - *
    • a tag: References an addressable event (AddressTag)
    • - *
    • d tag: Identifier for addressable events (IdentifierTag)
    • - *
    • t tag: Hashtags (HashtagTag)
    • - *
    • r tag: References a URL (ReferenceTag)
    • - *
    • Custom tags: GenericTag for unknown tag codes
    • - *
    - * - *

    Tag Creation: - *

    {@code
    - * // Method 1: Using specific tag classes
    - * EventTag eventTag = EventTag.builder()
    - *     .idEvent("event_id_hex")
    - *     .recommendedRelayUrl("wss://relay.example.com")
    - *     .marker("reply")
    - *     .build();
    - *
    - * // Method 2: Using factory method
    - * BaseTag tag = BaseTag.create("e", "event_id_hex", "relay_url", "reply");
    - *
    - * // Method 3: Using GenericTag for custom/unknown tags
    - * GenericTag customTag = new GenericTag("customcode", List.of(
    - *     new ElementAttribute("param0", "value1"),
    - *     new ElementAttribute("param1", "value2")
    - * ));
    - * }
    - * - *

    Tag Registry: The library maintains a {@link TagRegistry} that maps tag codes - * to their corresponding classes. When deserializing events, the registry is consulted to - * create the appropriate tag type. Unknown tag codes are deserialized as {@link GenericTag}. - * - *

    Design Patterns: - *

      - *
    • Factory Pattern: {@code create()} methods provide flexible tag creation
    • - *
    • Registry Pattern: {@link TagRegistry} maps codes to tag classes
    • - *
    • Template Method: Subclasses define specific tag fields and behavior
    • - *
    - * - *

    Serialization: Tags are automatically serialized/deserialized using Jackson - * with custom {@link BaseTagSerializer} and {@link TagDeserializer}. The serialization - * preserves the tag code and parameter order required by NIP-01. - * - *

    Reflection API: This class provides reflection-based methods for accessing - * tag fields dynamically: - *

      - *
    • {@link #getCode()} - Returns the tag code from {@code @Tag} annotation
    • - *
    • {@link #getSupportedFields()} - Returns fields annotated with {@code @Key}
    • - *
    • {@link #getFieldValue(Field)} - Gets field value using reflection
    • - *
    - * - *

    Example - Custom Tag Implementation: - *

    {@code
    - * @Tag(code = "mycustom", name = "My Custom Tag")
    - * @Data
    - * @EqualsAndHashCode(callSuper = false)
    - * public class MyCustomTag extends BaseTag {
    - *
    - *   @Key(order = 0)
    - *   private String parameter1;
    - *
    - *   @Key(order = 1)
    - *   private String parameter2;
    - *
    - *   // Builder pattern provided by Lombok @Data
    - * }
    - *
    - * // Register the tag
    - * TagRegistry.register("mycustom", genericTag -> {
    - *   // Convert GenericTag to MyCustomTag
    - *   return MyCustomTag.builder()
    - *       .parameter1(genericTag.getAttributes().get(0).getValue())
    - *       .parameter2(genericTag.getAttributes().get(1).getValue())
    - *       .build();
    - * });
    - * }
    - * - *

    Thread Safety: Tag instances are immutable after creation (due to Lombok - * {@code @Data} generating only getters for final fields). The {@code setParent()} method - * intentionally does nothing to avoid retaining parent event references. - * - * @see ITag - * @see GenericTag - * @see TagRegistry - * @see nostr.base.annotation.Tag - * @see nostr.base.annotation.Key - * @see NIP-01 - Tags - * @since 0.1.0 - */ @Data @ToString -@EqualsAndHashCode(callSuper = false) +@EqualsAndHashCode @JsonDeserialize(using = TagDeserializer.class) @JsonSerialize(using = BaseTagSerializer.class) -public abstract class BaseTag implements ITag { +public abstract class BaseTag { - /** - * Sets the parent event for this tag. - * - *

    Implementation Note: This method intentionally does nothing. Parent references - * are not retained to avoid circular references and memory issues. Tags are value objects - * that should not hold references to their containing events. - * - * @param event the parent event (ignored) - */ - @Override - public void setParent(IEvent event) { - // Intentionally left blank to avoid retaining parent references. - } - - /** - * Returns the tag code as defined in the {@code @Tag} annotation. - * - *

    The tag code is the first element in the tag array and identifies the tag type. - * For example, "e" for event references, "p" for public key references, etc. - * - * @return tag code string (e.g., "e", "p", "a", "d", "t", etc.) - */ - @Override public String getCode() { - return this.getClass().getAnnotation(Tag.class).code(); - } - - /** - * Gets the value of a field using reflection. - * - *

    This method uses Java Beans introspection to read the field value through its getter - * method. If the field cannot be read (no getter, access denied, etc.), an empty Optional - * is returned. - * - * @param field the field to read - * @return Optional containing the field value as a String, or empty if unavailable - */ - public Optional getFieldValue(Field field) { - try { - return Optional.ofNullable( - new PropertyDescriptor(field.getName(), this.getClass()).getReadMethod().invoke(this)) - .map(Object::toString); - } catch (IllegalAccessException - | IllegalArgumentException - | InvocationTargetException - | IntrospectionException ex) { - return Optional.empty(); - } - } - - /** - * Returns all fields that are annotated with {@code @Key} and have non-null values. - * - *

    This method is used during serialization to determine which tag parameters should - * be included in the JSON array. Only fields marked with {@code @Key} annotation are - * considered, and only those with present values are returned. - * - * @return list of fields with {@code @Key} annotation that have values - */ - public List getSupportedFields() { - return Streams.failableStream(Arrays.stream(this.getClass().getDeclaredFields())) - .filter(f -> Objects.nonNull(f.getAnnotation(Key.class))) - .filter(f -> getFieldValue(f).isPresent()) - .collect(Collectors.toList()); + return ""; } - /** - * Factory method to create a tag from a code and variable parameters. - * - *

    This is a convenience method that delegates to {@link #create(String, List)}. - * - * @param code tag code (e.g., "e", "p", "a") - * @param params tag parameters - * @return BaseTag instance (specific type if registered, GenericTag otherwise) - * @see #create(String, List) - */ public static BaseTag create(@NonNull String code, @NonNull String... params) { return create(code, List.of(params)); } - /** - * Factory method to create a tag from a code and parameter list. - * - *

    This method consults the {@link TagRegistry} to determine if a specific tag class - * is registered for the given code. If found, it creates an instance of that class. - * Otherwise, it returns a {@link GenericTag}. - * - *

    Example: - *

    {@code
    -   * // Creates an EventTag (registered for "e" code)
    -   * BaseTag eventTag = BaseTag.create("e", List.of("event_id", "relay_url"));
    -   *
    -   * // Creates a GenericTag (no registration for "custom" code)
    -   * BaseTag customTag = BaseTag.create("custom", List.of("param1", "param2"));
    -   * }
    - * - * @param code tag code (e.g., "e", "p", "a") - * @param params list of tag parameters - * @return BaseTag instance (specific type if registered, GenericTag otherwise) - */ public static BaseTag create(@NonNull String code, @NonNull List params) { - GenericTag genericTag = - new GenericTag( - code, - IntStream.range(0, params.size()) - .mapToObj( - i -> new ElementAttribute("param".concat(String.valueOf(i)), params.get(i))) - .toList()); - - return Optional.ofNullable(TagRegistry.get(code)) - .map(f -> (BaseTag) f.apply(genericTag)) - .orElse(genericTag); - } - - /** - * Helper method for deserializers to set optional tag fields. - * - *

    If the JsonNode is null or missing, no action is taken. This is used in custom - * deserializers to populate tag fields from JSON without throwing exceptions for - * missing optional parameters. - * - * @param the tag type - * @param node the JSON node (may be null) - * @param con consumer that sets the field value - * @param tag the tag instance to populate - */ - protected static void setOptionalField( - JsonNode node, BiConsumer con, T tag) { - Optional.ofNullable(node).ifPresent(n -> con.accept(n, tag)); - } - - /** - * Helper method for deserializers to set required tag fields. - * - *

    If the JsonNode is null or missing, a NoSuchElementException is thrown. This is - * used in custom deserializers to populate mandatory tag fields from JSON. - * - * @param the tag type - * @param node the JSON node (must not be null) - * @param con consumer that sets the field value - * @param tag the tag instance to populate - * @throws java.util.NoSuchElementException if node is null - */ - protected static void setRequiredField( - JsonNode node, BiConsumer con, T tag) { - con.accept(Optional.ofNullable(node).orElseThrow(), tag); + return new GenericTag(code, params); } } diff --git a/nostr-java-event/src/main/java/nostr/event/Deleteable.java b/nostr-java-event/src/main/java/nostr/event/Deleteable.java deleted file mode 100644 index 2d773481f..000000000 --- a/nostr-java-event/src/main/java/nostr/event/Deleteable.java +++ /dev/null @@ -1,6 +0,0 @@ -package nostr.event; - -public interface Deleteable { - - Integer getKind(); -} diff --git a/nostr-java-event/src/main/java/nostr/event/IContent.java b/nostr-java-event/src/main/java/nostr/event/IContent.java deleted file mode 100644 index 463cddbdc..000000000 --- a/nostr-java-event/src/main/java/nostr/event/IContent.java +++ /dev/null @@ -1,9 +0,0 @@ -package nostr.event; - -/** - * @author eric - */ -public interface IContent { - - String getId(); -} diff --git a/nostr-java-event/src/main/java/nostr/event/JsonContent.java b/nostr-java-event/src/main/java/nostr/event/JsonContent.java deleted file mode 100644 index 9e0e2644e..000000000 --- a/nostr-java-event/src/main/java/nostr/event/JsonContent.java +++ /dev/null @@ -1,19 +0,0 @@ -package nostr.event; - -import com.fasterxml.jackson.core.JsonProcessingException; - -import static nostr.base.json.EventJsonMapper.mapper; - -/** - * @author eric - */ -public interface JsonContent { - - default String value() { - try { - return mapper().writeValueAsString(this); - } catch (JsonProcessingException ex) { - throw new RuntimeException(ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/NIP01Event.java b/nostr-java-event/src/main/java/nostr/event/NIP01Event.java deleted file mode 100644 index 3d33f16a3..000000000 --- a/nostr-java-event/src/main/java/nostr/event/NIP01Event.java +++ /dev/null @@ -1,27 +0,0 @@ -package nostr.event; - -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.impl.GenericEvent; - -import java.util.List; - -/** - * @author guilhermegps - */ -@NoArgsConstructor -public abstract class NIP01Event extends GenericEvent { - - public NIP01Event(PublicKey pubKey, Kind kind, List tags) { - super(pubKey, kind, tags); - } - - public NIP01Event(PublicKey pubKey, Kind kind, List tags, String content) { - super(pubKey, kind, tags, content); - } - - public NIP01Event(PublicKey sender, Integer kind, List tags, String content) { - super(sender, kind, tags, content); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/NIP04Event.java b/nostr-java-event/src/main/java/nostr/event/NIP04Event.java deleted file mode 100644 index 5764f0121..000000000 --- a/nostr-java-event/src/main/java/nostr/event/NIP04Event.java +++ /dev/null @@ -1,28 +0,0 @@ -package nostr.event; - -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.impl.GenericEvent; - -import java.util.List; - -/** - * @author guilhermegps - */ -@NoArgsConstructor -public abstract class NIP04Event extends GenericEvent { - - public NIP04Event( - @NonNull PublicKey pubKey, - @NonNull Kind kind, - @NonNull List tags, - @NonNull String content) { - super(pubKey, kind, tags, content); - } - - public NIP04Event(@NonNull PublicKey pubKey, @NonNull Kind kind) { - super(pubKey, kind); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/NIP05Event.java b/nostr-java-event/src/main/java/nostr/event/NIP05Event.java deleted file mode 100644 index 5a1a0bc41..000000000 --- a/nostr-java-event/src/main/java/nostr/event/NIP05Event.java +++ /dev/null @@ -1,17 +0,0 @@ -package nostr.event; - -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.impl.GenericEvent; - -/** - * @author guilhermegps - */ -@NoArgsConstructor -public abstract class NIP05Event extends GenericEvent { - - public NIP05Event(PublicKey pubKey, Kind kind) { - super(pubKey, kind); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/NIP09Event.java b/nostr-java-event/src/main/java/nostr/event/NIP09Event.java deleted file mode 100644 index b05d02044..000000000 --- a/nostr-java-event/src/main/java/nostr/event/NIP09Event.java +++ /dev/null @@ -1,19 +0,0 @@ -package nostr.event; - -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.impl.GenericEvent; - -import java.util.List; - -/** - * @author guilhermegps - */ -@NoArgsConstructor -public abstract class NIP09Event extends GenericEvent { - - public NIP09Event(PublicKey pubKey, Kind kind, List tags, String content) { - super(pubKey, kind, tags, content); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/NIP25Event.java b/nostr-java-event/src/main/java/nostr/event/NIP25Event.java deleted file mode 100644 index 4010e20bd..000000000 --- a/nostr-java-event/src/main/java/nostr/event/NIP25Event.java +++ /dev/null @@ -1,31 +0,0 @@ -package nostr.event; - -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.impl.GenericEvent; - -import java.util.List; - -/** - * @author guilhermegps - */ -@NoArgsConstructor -public abstract class NIP25Event extends GenericEvent { - - public NIP25Event(PublicKey pubKey, Kind kind, List tags) { - super(pubKey, kind, tags); - } - - public NIP25Event(PublicKey pubKey, Kind kind, List tags, String content) { - super(pubKey, kind, tags, content); - } - - public NIP25Event(PublicKey sender, Integer kind, List tags, String content) { - super(sender, kind, tags, content); - } - - public NIP25Event(PublicKey pubKey, Kind reaction) { - super(pubKey, reaction); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/NIP52Event.java b/nostr-java-event/src/main/java/nostr/event/NIP52Event.java deleted file mode 100644 index 7c6a6a6a4..000000000 --- a/nostr-java-event/src/main/java/nostr/event/NIP52Event.java +++ /dev/null @@ -1,23 +0,0 @@ -package nostr.event; - -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.impl.AddressableEvent; - -import java.util.List; - -@EqualsAndHashCode(callSuper = false) -@NoArgsConstructor -public abstract class NIP52Event extends AddressableEvent { - - public NIP52Event( - @NonNull PublicKey pubKey, - @NonNull Kind kind, - @NonNull List baseTags, - @NonNull String content) { - super(pubKey, kind.getValue(), baseTags, content); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/NIP99Event.java b/nostr-java-event/src/main/java/nostr/event/NIP99Event.java deleted file mode 100644 index 0861b870a..000000000 --- a/nostr-java-event/src/main/java/nostr/event/NIP99Event.java +++ /dev/null @@ -1,21 +0,0 @@ -package nostr.event; - -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.impl.GenericEvent; - -import java.util.List; - -@EqualsAndHashCode(callSuper = false) -@NoArgsConstructor -public abstract class NIP99Event extends GenericEvent { - public NIP99Event(PublicKey pubKey, Kind kind, List baseTags) { - this(pubKey, kind, baseTags, null); - } - - public NIP99Event(PublicKey pubKey, Kind kind, List baseTags, String content) { - super(pubKey, kind, baseTags, content); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/Nip05Content.java b/nostr-java-event/src/main/java/nostr/event/Nip05Content.java deleted file mode 100644 index b64dbbfbc..000000000 --- a/nostr-java-event/src/main/java/nostr/event/Nip05Content.java +++ /dev/null @@ -1,23 +0,0 @@ -package nostr.event; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.NoArgsConstructor; -import nostr.base.IElement; - -import java.util.List; -import java.util.Map; - -/** - * @author eric - */ -@Data -@NoArgsConstructor -public class Nip05Content implements IElement { - - @JsonProperty("names") - private Map names; - - @JsonProperty("relays") - private Map> relays; -} diff --git a/nostr-java-event/src/main/java/nostr/event/Reaction.java b/nostr-java-event/src/main/java/nostr/event/Reaction.java deleted file mode 100644 index 1ad8a0fab..000000000 --- a/nostr-java-event/src/main/java/nostr/event/Reaction.java +++ /dev/null @@ -1,18 +0,0 @@ -package nostr.event; - -import lombok.Getter; - -/** - * @author squirrel - */ -@Getter -public enum Reaction { - LIKE("+"), - DISLIKE("-"); - - private final String emoji; - - Reaction(String emoji) { - this.emoji = emoji; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/Response.java b/nostr-java-event/src/main/java/nostr/event/Response.java deleted file mode 100644 index a2f6e0ee2..000000000 --- a/nostr-java-event/src/main/java/nostr/event/Response.java +++ /dev/null @@ -1,17 +0,0 @@ -package nostr.event; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import nostr.base.Relay; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class Response { - - private BaseMessage message; - private Relay relay; -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/Amount.java b/nostr-java-event/src/main/java/nostr/event/entities/Amount.java deleted file mode 100644 index 00b70526a..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/Amount.java +++ /dev/null @@ -1,15 +0,0 @@ -package nostr.event.entities; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class Amount { - private Integer amount; - private String unit; -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/CalendarContent.java b/nostr-java-event/src/main/java/nostr/event/entities/CalendarContent.java deleted file mode 100644 index ab5d695bb..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/CalendarContent.java +++ /dev/null @@ -1,189 +0,0 @@ -package nostr.event.entities; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; -import nostr.event.tag.AddressTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.LabelNamespaceTag; -import nostr.event.tag.LabelTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.ReferenceTag; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -@EqualsAndHashCode(callSuper = false) -public class CalendarContent extends NIP42Content { - - - // below fields mandatory - @Getter private final IdentifierTag identifierTag; - @Getter private final String title; - @Getter private final Long start; - - // below fields optional - private Long end; - private String startTzid; - private String endTzid; - private String summary; - private String image; - private String location; - private final Map> classTypeTagsMap = new HashMap<>(); - - public CalendarContent( - @NonNull IdentifierTag identifierTag, @NonNull String title, @NonNull Long start) { - this.identifierTag = identifierTag; - this.title = title; - this.start = start; - } - - public void setEnd(@NonNull Long end) { - this.end = end; - } - - public Optional getEnd() { - return Optional.ofNullable(this.end); - } - - public void setStartTzid(@NonNull String startTzid) { - this.startTzid = startTzid; - } - - public Optional getStartTzid() { - return Optional.ofNullable(this.startTzid); - } - - public void setEndTzid(@NonNull String endTzid) { - this.endTzid = endTzid; - } - - public Optional getEndTzid() { - return Optional.ofNullable(this.endTzid); - } - - public void setSummary(@NonNull String summary) { - this.summary = summary; - } - - public Optional getSummary() { - return Optional.ofNullable(this.summary); - } - - public void setImage(@NonNull String image) { - this.image = image; - } - - public Optional getImage() { - return Optional.ofNullable(this.image); - } - - public void setLocation(@NonNull String location) { - this.location = location; - } - - public Optional getLocation() { - return Optional.ofNullable(this.location); - } - - public void addParticipantPubKeyTag(@NonNull PubKeyTag pubKeyTag) { - addTag((T) pubKeyTag); - } - - public void addParticipantPubKeyTags(@NonNull List pubKeyTags) { - pubKeyTags.forEach(this::addParticipantPubKeyTag); - } - - public List getParticipantPubKeyTags() { - return getTagsByType(PubKeyTag.class).stream().toList(); - } - - public void addHashtagTag(@NonNull HashtagTag hashtagTag) { - addTag((T) hashtagTag); - } - - public void addHashtagTags(@NonNull List hashtagTags) { - hashtagTags.forEach(this::addHashtagTag); - } - - public List getHashtagTags() { - return getTagsByType(HashtagTag.class); - } - - public void addReferenceTag(@NonNull ReferenceTag referenceTag) { - addTag((T) referenceTag); - } - - public List getReferenceTags() { - return getTagsByType(ReferenceTag.class); - } - - public void addLabelTag(@NonNull LabelTag labelTag) { - addTag((T) labelTag); - } - - public void addLabelTags(@NonNull List labelTags) { - labelTags.forEach(this::addLabelTag); - } - - public List getLabelTags() { - return getTagsByType(LabelTag.class); - } - - public void addLabelNamespaceTag(@NonNull LabelNamespaceTag labelNamespaceTag) { - addTag((T) labelNamespaceTag); - } - - public void addLabelNamespaceTags(@NonNull List labelNamespaceTags) { - labelNamespaceTags.forEach(this::addLabelNamespaceTag); - } - - public List getLabelNamespaceTags() { - return getTagsByType(LabelNamespaceTag.class); - } - - public void addAddressTag(@NonNull AddressTag addressTag) { - addTag((T) addressTag); - } - - public List getAddressTags() { - return getTagsByType(AddressTag.class); - } - - public void setGeohashTag(@NonNull GeohashTag geohashTag) { - addTag((T) geohashTag); - } - - public Optional getGeohashTag() { - return getTagsByType(GeohashTag.class).stream().findFirst(); - } - - private List getTagsByType(Class clazz) { - Tag annotation = clazz.getAnnotation(Tag.class); - List list = getBaseTags(annotation).stream().map(clazz::cast).toList(); - return list; - } - - private List getBaseTags(@NonNull Tag type) { - - String code = type.code(); - List value = classTypeTagsMap.get(code); - Optional> value1 = Optional.ofNullable(value); - List baseTags = value1.orElse(Collections.emptyList()); - return baseTags; - } - - private void addTag(@NonNull T baseTag) { - String code = baseTag.getCode(); - List list = classTypeTagsMap.computeIfAbsent(code, k -> new ArrayList<>()); - list.add(baseTag); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/CalendarRsvpContent.java b/nostr-java-event/src/main/java/nostr/event/entities/CalendarRsvpContent.java deleted file mode 100644 index 8a3a9190b..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/CalendarRsvpContent.java +++ /dev/null @@ -1,65 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; - -import java.util.Optional; - -@Builder -@JsonDeserialize(builder = CalendarRsvpContent.CalendarRsvpContentBuilder.class) -@EqualsAndHashCode(callSuper = false) -public class CalendarRsvpContent extends NIP42Content { - - - // below fields mandatory - @Getter private final IdentifierTag identifierTag; - @Getter private final AddressTag addressTag; - @Getter private final String status; - - // below fields optional - private PubKeyTag authorPubKeyTag; - private EventTag eventTag; - private GenericTag fbTag; - - public static CalendarRsvpContentBuilder builder( - @NonNull IdentifierTag identifierTag, - @NonNull AddressTag addressTag, - @NonNull String status) { - return new CalendarRsvpContentBuilder() - .identifierTag(identifierTag) - .addressTag(addressTag) - .status(status); - } - - public Optional getAuthorPubKeyTag() { - return Optional.ofNullable(authorPubKeyTag); - } - - public void setAuthorPubKeyTag(@NonNull PubKeyTag authorPubKeyTag) { - this.authorPubKeyTag = authorPubKeyTag; - } - - public Optional getEventTag() { - return Optional.ofNullable(eventTag); - } - - public void setEventTag(@NonNull EventTag eventTag) { - this.eventTag = eventTag; - } - - public Optional getFbTag() { - return Optional.ofNullable(fbTag); - } - - public void setFbTag(@NonNull GenericTag fbTag) { - this.fbTag = fbTag; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/CashuMint.java b/nostr-java-event/src/main/java/nostr/event/entities/CashuMint.java deleted file mode 100644 index 8f00d03e1..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/CashuMint.java +++ /dev/null @@ -1,27 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonValue; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; - -import java.util.List; - -@Data -@RequiredArgsConstructor -@AllArgsConstructor -@Builder -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class CashuMint { - - @EqualsAndHashCode.Include private final String url; - private List units; - - @Override - @JsonValue - public String toString() { - return url; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/CashuProof.java b/nostr-java-event/src/main/java/nostr/event/entities/CashuProof.java deleted file mode 100644 index 529bb4cfa..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/CashuProof.java +++ /dev/null @@ -1,43 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.event.json.codec.EventEncodingException; - -import static nostr.base.json.EventJsonMapper.mapper; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class CashuProof { - - @EqualsAndHashCode.Include private String id; - private Integer amount; - - @EqualsAndHashCode.Include private String secret; - - @JsonProperty("C") - @EqualsAndHashCode.Include - private String C; - - @EqualsAndHashCode.Include - @JsonInclude(JsonInclude.Include.NON_NULL) - private String witness; - - @Override - public String toString() { - try { - return mapper().writeValueAsString(this); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to serialize Cashu proof", ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/CashuQuote.java b/nostr-java-event/src/main/java/nostr/event/entities/CashuQuote.java deleted file mode 100644 index 042d5d339..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/CashuQuote.java +++ /dev/null @@ -1,17 +0,0 @@ -package nostr.event.entities; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class CashuQuote { - private String id; - private Long expiration; - private CashuMint mint; - private CashuWallet wallet; -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/CashuToken.java b/nostr-java-event/src/main/java/nostr/event/entities/CashuToken.java deleted file mode 100644 index 3554ab8fc..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/CashuToken.java +++ /dev/null @@ -1,64 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.event.json.serializer.CashuTokenSerializer; - -import java.util.ArrayList; -import java.util.List; - -@Data -@AllArgsConstructor -@Builder -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -@JsonSerialize(using = CashuTokenSerializer.class) -public class CashuToken { - - @EqualsAndHashCode.Include private CashuMint mint; - - @EqualsAndHashCode.Include - @Builder.Default - private List proofs = new ArrayList<>(); - - @Builder.Default private List destroyed = new ArrayList<>(); - - public CashuToken() { - this.proofs = new ArrayList<>(); - this.destroyed = new ArrayList<>(); - } - - public CashuToken(@NonNull CashuMint mint, @NonNull List proofs) { - this(mint, proofs, new ArrayList<>()); - } - - public void addDestroyed(@NonNull String eventId) { - this.destroyed.add(eventId); - } - - public void removeDestroyed(@NonNull String eventId) { - this.destroyed.remove(eventId); - } - - public Integer calculateAmount() { - if (proofs == null || proofs.isEmpty()) return 0; - return proofs.stream().mapToInt(CashuProof::getAmount).sum(); - } - - /** - * Number of destroyed event references recorded in this token. - */ - public int getDestroyedCount() { - return this.destroyed != null ? this.destroyed.size() : 0; - } - - /** - * Checks whether a destroyed event id is recorded. - */ - public boolean containsDestroyed(@NonNull String eventId) { - return this.destroyed != null && this.destroyed.contains(eventId); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/CashuWallet.java b/nostr-java-event/src/main/java/nostr/event/entities/CashuWallet.java deleted file mode 100644 index 085aa5eaf..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/CashuWallet.java +++ /dev/null @@ -1,117 +0,0 @@ -package nostr.event.entities; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.base.Relay; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -@Data -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -@AllArgsConstructor -@Builder -public class CashuWallet { - - @EqualsAndHashCode.Include private String id; - - private String name; - private String description; - private Integer balance; - - @EqualsAndHashCode.Include private String privateKey; - - - private final Set mints; - private final Map> relays; - private Set tokens; - - public CashuWallet() { - this.balance = 0; - this.mints = new HashSet<>(); - this.relays = new HashMap<>(); - this.tokens = new HashSet<>(); - } - - public void reset() { - this.resetBalance(); - this.tokens = new HashSet<>(); - } - - public void resetBalance() { - this.balance = 0; - } - - public void increaseBalance(Integer amount) { - this.balance += amount; - } - - public void decreaseBalance(Integer amount) { - this.balance -= amount; - } - - public void addToken(@NonNull CashuToken token) { - this.tokens.add(token); - this.refreshBalance(); - } - - public void removeToken(@NonNull CashuToken token) { - this.tokens.remove(token); - this.refreshBalance(); - } - - public void addMint(@NonNull CashuMint mint) { - this.mints.add(mint); - } - - public void removeMint(@NonNull CashuMint mint) { - this.mints.remove(mint); - } - - public CashuMint getMint(@NonNull String mintUrl) { - return this.mints.stream() - .filter(mint -> mint.getUrl().equals(mintUrl)) - .findFirst() - .orElse(null); - } - - public void addRelay(@NonNull String unit, @NonNull Relay relay) { - Set relaySet = this.relays.get(unit); - if (relaySet == null) { - relaySet = new HashSet<>(); - } - relaySet.add(relay); - this.relays.put(unit, relaySet); - } - - public void removeRelay(@NonNull String unit, @NonNull Relay relay) { - Set relaySet = this.relays.get(unit); - if (relaySet == null) { - return; - } - - relaySet.remove(relay); - if (relaySet.isEmpty()) { - this.relays.remove(unit); - } else { - this.relays.put(unit, relaySet); - } - } - - public Set getRelays(@NonNull String unit) { - return this.relays.get(unit); - } - - public void refreshBalance() { - int total = 0; - for (CashuToken token : this.tokens) { - total += token.calculateAmount(); - } - this.setBalance(total); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/ChannelProfile.java b/nostr-java-event/src/main/java/nostr/event/entities/ChannelProfile.java deleted file mode 100644 index 42579c429..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/ChannelProfile.java +++ /dev/null @@ -1,28 +0,0 @@ -package nostr.event.entities; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; - -/** - * @author eric - */ -@Data -@ToString(callSuper = true) -@EqualsAndHashCode(callSuper = true) -public class ChannelProfile extends Profile { - - public ChannelProfile(String name, String about, URL picture) { - super(name, about, picture); - } - - public ChannelProfile(String name, String about, String url) - throws MalformedURLException, URISyntaxException { - this(name, about, new URI(url).toURL()); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/ClassifiedListing.java b/nostr-java-event/src/main/java/nostr/event/entities/ClassifiedListing.java deleted file mode 100644 index 7fcb6f6dd..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/ClassifiedListing.java +++ /dev/null @@ -1,31 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.event.JsonContent; -import nostr.event.tag.PriceTag; - -@Data -@Builder -@JsonDeserialize(builder = ClassifiedListing.ClassifiedListingBuilder.class) -@EqualsAndHashCode(callSuper = false) -public class ClassifiedListing implements JsonContent { - private String id; - private final String title; - private final String summary; - - @EqualsAndHashCode.Exclude private Long publishedAt; - private String location; - - @JsonProperty("price") - private final PriceTag priceTag; - - public static ClassifiedListingBuilder builder( - @NonNull String title, @NonNull String summary, @NonNull PriceTag priceTag) { - return new ClassifiedListingBuilder().title(title).summary(summary).priceTag(priceTag); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/CustomerOrder.java b/nostr-java-event/src/main/java/nostr/event/entities/CustomerOrder.java deleted file mode 100644 index 30638e2bd..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/CustomerOrder.java +++ /dev/null @@ -1,66 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.Setter; -import lombok.ToString; -import nostr.base.PublicKey; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@Getter -@Setter -@EqualsAndHashCode(callSuper = false) -@ToString(callSuper = true) -public class CustomerOrder extends NIP15Content.CheckoutContent { - - @JsonProperty private final String id; - - @JsonProperty private String name; - - @JsonProperty private String address; - - @JsonProperty private String message; - - @JsonProperty private Contact contact; - - @JsonProperty private List items; - - @JsonProperty("shipping_id") - private String shippingId; - - public CustomerOrder() { - this.items = new ArrayList<>(); - this.id = UUID.randomUUID().toString(); - } - - @Data - public static class Contact { - - @JsonProperty("nostr") - private final PublicKey publicKey; - - @JsonProperty private String phone; - - @JsonProperty private String email; - - public Contact(@NonNull PublicKey publicKey) { - this.publicKey = publicKey; - } - } - - @Data - @NoArgsConstructor - public static class Item { - - @JsonProperty private Product product; - - @JsonProperty private int quantity; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/NIP15Content.java b/nostr-java-event/src/main/java/nostr/event/entities/NIP15Content.java deleted file mode 100644 index 846139f21..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/NIP15Content.java +++ /dev/null @@ -1,24 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; -import nostr.event.JsonContent; -import nostr.event.impl.CheckoutEvent; - -public abstract class NIP15Content implements JsonContent { - - public abstract String getId(); - - public String toString() { - return value(); - } - - @EqualsAndHashCode(callSuper = true) - @Data - public abstract static class CheckoutContent extends NIP15Content { - @JsonProperty private CheckoutEvent.MessageType messageType; - } - - public abstract static class MerchantContent extends NIP15Content {} -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/NIP42Content.java b/nostr-java-event/src/main/java/nostr/event/entities/NIP42Content.java deleted file mode 100644 index 122ed1ca8..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/NIP42Content.java +++ /dev/null @@ -1,5 +0,0 @@ -package nostr.event.entities; - -import nostr.event.JsonContent; - -public abstract class NIP42Content implements JsonContent {} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/NutZap.java b/nostr-java-event/src/main/java/nostr/event/entities/NutZap.java deleted file mode 100644 index 6cd4dc6a9..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/NutZap.java +++ /dev/null @@ -1,35 +0,0 @@ -package nostr.event.entities; - -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.PublicKey; -import nostr.event.tag.EventTag; - -import java.util.List; - -@Data -@NoArgsConstructor -public class NutZap { - - private CashuMint mint; - private List proofs; - private PublicKey recipient; - private EventTag nutZappedEvent; - - public void addProof(@NonNull CashuProof cashuProof) { - if (proofs == null) { - proofs = new java.util.ArrayList<>(); - } - proofs.add(cashuProof); - } - - /** - * Sum the amount contained in this zap's proofs. - * Returns 0 when no proofs exist. - */ - public int getTotalAmount() { - if (proofs == null || proofs.isEmpty()) return 0; - return proofs.stream().mapToInt(CashuProof::getAmount).sum(); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/NutZapInformation.java b/nostr-java-event/src/main/java/nostr/event/entities/NutZapInformation.java deleted file mode 100644 index eea2c6aae..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/NutZapInformation.java +++ /dev/null @@ -1,17 +0,0 @@ -package nostr.event.entities; - -import lombok.Data; -import lombok.NoArgsConstructor; -import nostr.base.Relay; - -import java.util.ArrayList; -import java.util.List; - -@Data -@NoArgsConstructor -public class NutZapInformation { - - public List relays = new ArrayList<>(); - public List mints = new ArrayList<>(); - public String p2pkPubkey; -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/PaymentRequest.java b/nostr-java-event/src/main/java/nostr/event/entities/PaymentRequest.java deleted file mode 100644 index 439137459..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/PaymentRequest.java +++ /dev/null @@ -1,54 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonValue; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@Getter -@Setter -@EqualsAndHashCode(callSuper = false) -@ToString(callSuper = true) -public class PaymentRequest extends NIP15Content.CheckoutContent { - - @JsonProperty private final String id; - - @JsonProperty private String message; - - @JsonProperty("payment_options") - private final List paymentOptions; - - public PaymentRequest() { - this.paymentOptions = new ArrayList<>(); - this.id = UUID.randomUUID().toString(); - } - - @Data - @NoArgsConstructor - public static class PaymentOptions { - - public enum Type { - URL, - BTC, - LN, - LNURL; - - @JsonValue - public String getValue() { - return name().toLowerCase(); - } - } - - @JsonProperty private Type type; - - @JsonProperty private String link; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/PaymentShipmentStatus.java b/nostr-java-event/src/main/java/nostr/event/entities/PaymentShipmentStatus.java deleted file mode 100644 index ea58c2028..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/PaymentShipmentStatus.java +++ /dev/null @@ -1,28 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -import java.util.UUID; - -@Getter -@Setter -@EqualsAndHashCode(callSuper = false) -@ToString(callSuper = true) -public class PaymentShipmentStatus extends NIP15Content.CheckoutContent { - - @JsonProperty private final String id; - - @JsonProperty private String message; - - @JsonProperty private boolean paid; - - @JsonProperty private boolean shipped; - - public PaymentShipmentStatus() { - this.id = UUID.randomUUID().toString(); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/Product.java b/nostr-java-event/src/main/java/nostr/event/entities/Product.java deleted file mode 100644 index 4c65a2ddb..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/Product.java +++ /dev/null @@ -1,51 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@Getter -@Setter -@EqualsAndHashCode(callSuper = false) -public class Product extends NIP15Content.MerchantContent { - - @JsonProperty private final String id; - - @JsonProperty private Stall stall; - - @JsonProperty private String name; - - @JsonProperty private String description; - - @JsonProperty private List images; - - @JsonProperty private String currency; - - @JsonProperty private Float price; - - @JsonProperty private int quantity; - - @JsonProperty private List specs; - - public Product() { - this.specs = new ArrayList<>(); - this.images = new ArrayList<>(); - this.id = UUID.randomUUID().toString(); - } - - @Data - @AllArgsConstructor - public static class Spec { - - @JsonProperty private final String key; - - @JsonProperty private final String value; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/Profile.java b/nostr-java-event/src/main/java/nostr/event/entities/Profile.java deleted file mode 100644 index 6829bafa3..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/Profile.java +++ /dev/null @@ -1,27 +0,0 @@ -package nostr.event.entities; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.ToString; -import lombok.experimental.SuperBuilder; - -import java.net.URL; - -/** - * @author eric - */ -@Data -@EqualsAndHashCode -@SuperBuilder -@AllArgsConstructor -@NoArgsConstructor -public abstract class Profile { - - private String name; - - @ToString.Exclude private String about; - - @ToString.Exclude private URL picture; -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/Reaction.java b/nostr-java-event/src/main/java/nostr/event/entities/Reaction.java deleted file mode 100644 index 8319d340a..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/Reaction.java +++ /dev/null @@ -1,18 +0,0 @@ -package nostr.event.entities; - -import lombok.Getter; - -/** - * @author squirrel - */ -@Getter -public enum Reaction { - LIKE("+"), - DISLIKE("-"); - - private final String emoji; - - Reaction(String emoji) { - this.emoji = emoji; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/Response.java b/nostr-java-event/src/main/java/nostr/event/entities/Response.java deleted file mode 100644 index 2da8994cf..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/Response.java +++ /dev/null @@ -1,18 +0,0 @@ -package nostr.event.entities; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import nostr.base.Relay; -import nostr.event.BaseMessage; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class Response { - - private BaseMessage message; - private Relay relay; -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/SpendingHistory.java b/nostr-java-event/src/main/java/nostr/event/entities/SpendingHistory.java deleted file mode 100644 index ef830fa1f..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/SpendingHistory.java +++ /dev/null @@ -1,50 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonValue; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.event.tag.EventTag; - -import java.util.ArrayList; -import java.util.List; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class SpendingHistory { - private Direction direction; - private Amount amount; - - @Builder.Default private List eventTags = new ArrayList<>(); - - public enum Direction { - RECEIVED("in"), - SENT("out"); - - private final String value; - - Direction(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } - } - - public void addEventTag(@NonNull EventTag eventTag) { - this.eventTags.add(eventTag); - } - - /** - * Returns the number of associated event tags. - */ - public int getEventTagCount() { - return this.eventTags != null ? this.eventTags.size() : 0; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/Stall.java b/nostr-java-event/src/main/java/nostr/event/entities/Stall.java deleted file mode 100644 index c2d304880..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/Stall.java +++ /dev/null @@ -1,48 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@Getter -@Setter -@EqualsAndHashCode(callSuper = false) -public class Stall extends NIP15Content.MerchantContent { - - @JsonProperty private final String id; - - @JsonProperty private String name; - - @JsonProperty private String description; - - @JsonProperty private String currency; - - @JsonProperty private Shipping shipping; - - public Stall() { - this.id = UUID.randomUUID().toString().concat(UUID.randomUUID().toString()).substring(0, 64); - } - - @Data - public static class Shipping { - - @JsonProperty private final String id; - - @JsonProperty private String name; - - @JsonProperty private Float cost; - - @JsonProperty private List countries; - - public Shipping() { - this.countries = new ArrayList<>(); - this.id = UUID.randomUUID().toString(); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/UserProfile.java b/nostr-java-event/src/main/java/nostr/event/entities/UserProfile.java deleted file mode 100644 index ebc1d104f..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/UserProfile.java +++ /dev/null @@ -1,60 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.experimental.SuperBuilder; -import lombok.extern.slf4j.Slf4j; -import nostr.base.IBech32Encodable; -import nostr.base.PublicKey; -import nostr.crypto.bech32.Bech32; -import nostr.crypto.bech32.Bech32Prefix; - -import java.net.URL; - -import static nostr.base.json.EventJsonMapper.mapper; - -/** - * @author squirrel - */ -@Data -@EqualsAndHashCode(callSuper = false) -@SuperBuilder -@NoArgsConstructor -@Slf4j -public final class UserProfile extends Profile implements IBech32Encodable { - - @JsonIgnore private PublicKey publicKey; - - private String nip05; - - public UserProfile( - @NonNull PublicKey publicKey, String name, String nip05, String about, URL picture) { - super(name, about, picture); - this.publicKey = publicKey; - this.nip05 = nip05; - } - - @Override - public String toBech32() { - try { - return Bech32.encode( - Bech32.Encoding.BECH32, Bech32Prefix.NPROFILE.getCode(), this.publicKey.getRawData()); - } catch (Exception ex) { - log.error("Failed to convert UserProfile to Bech32 format", ex); - throw new RuntimeException("Failed to convert UserProfile to Bech32 format", ex); - } - } - - @Override - public String toString() { - try { - return mapper().writeValueAsString(this); - } catch (JsonProcessingException ex) { - throw new RuntimeException(ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/ZapReceipt.java b/nostr-java-event/src/main/java/nostr/event/entities/ZapReceipt.java deleted file mode 100644 index 324c06f36..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/ZapReceipt.java +++ /dev/null @@ -1,28 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.event.JsonContent; - -@Data -@EqualsAndHashCode(callSuper = false) -public class ZapReceipt implements JsonContent { - - @JsonProperty private final String bolt11; - - @JsonProperty private final String descriptionSha256; - - @JsonProperty private final String preimage; - - public ZapReceipt(@NonNull String bolt11, @NonNull String descriptionSha256, String preimage) { - this.descriptionSha256 = descriptionSha256; - this.bolt11 = bolt11; - this.preimage = preimage; - } - - public ZapReceipt(@NonNull String bolt11, @NonNull String descriptionSha256) { - this(bolt11, descriptionSha256, null); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/entities/ZapRequest.java b/nostr-java-event/src/main/java/nostr/event/entities/ZapRequest.java deleted file mode 100644 index 85b4cbd15..000000000 --- a/nostr-java-event/src/main/java/nostr/event/entities/ZapRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package nostr.event.entities; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.event.JsonContent; -import nostr.event.tag.RelaysTag; - -@Data -@EqualsAndHashCode(callSuper = false) -public class ZapRequest implements JsonContent { - @JsonProperty("relays") - private final RelaysTag relaysTag; - - @JsonProperty private final Long amount; - - @JsonProperty("lnurl") - private final String lnUrl; - - public ZapRequest(@NonNull RelaysTag relaysTag, @NonNull Long amount, @NonNull String lnUrl) { - this.relaysTag = relaysTag; - this.amount = amount; - this.lnUrl = lnUrl; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/AbstractFilterable.java b/nostr-java-event/src/main/java/nostr/event/filter/AbstractFilterable.java deleted file mode 100644 index ef95e01aa..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/AbstractFilterable.java +++ /dev/null @@ -1,19 +0,0 @@ -package nostr.event.filter; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; -import lombok.ToString; - -@Getter -@EqualsAndHashCode -@ToString -public abstract class AbstractFilterable implements Filterable { - private final T filterable; - private final String filterKey; - - protected AbstractFilterable(@NonNull T filterable, @NonNull String filterKey) { - this.filterable = filterable; - this.filterKey = filterKey; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/AddressTagFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/AddressTagFilter.java deleted file mode 100644 index b67c981e1..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/AddressTagFilter.java +++ /dev/null @@ -1,73 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.AddressTag; -import nostr.event.tag.IdentifierTag; - -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@EqualsAndHashCode(callSuper = true) -public class AddressTagFilter extends AbstractFilterable { - public static final String FILTER_KEY = "#a"; - - public AddressTagFilter(T addressableTag) { - super(addressableTag, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> - Filterable.getTypeSpecificTags(AddressTag.class, genericEvent).stream() - .anyMatch(addressTag -> addressTag.equals(getAddressableTag())); - } - - @Override - public Object getFilterableValue() { - String requiredAttributes = - Stream.of( - getAddressableTag().getKind(), - getAddressableTag().getPublicKey().toHexString(), - getAddressableTag().getIdentifierTag().getUuid()) - .map(Object::toString) - .collect(Collectors.joining(":")); - return Optional.ofNullable(getAddressableTag().getRelay()) - .map(relay -> String.join("\",\"", requiredAttributes, relay.getUri())) - .orElse(requiredAttributes); - } - - private T getAddressableTag() { - return super.getFilterable(); - } - - public static Function fxn = - node -> new AddressTagFilter<>(createAddressTag(node)); - - protected static AddressTag createAddressTag(@NonNull JsonNode node) { - String[] nodes = node.asText().split(","); - List list = Arrays.stream(nodes[0].split(":")).toList(); - - final AddressTag addressTag = new AddressTag(); - addressTag.setKind(Integer.valueOf(list.get(0))); - addressTag.setPublicKey(new PublicKey(list.get(1))); - addressTag.setIdentifierTag(new IdentifierTag(list.get(2))); - - if (!Objects.equals(2, nodes.length)) return addressTag; - - addressTag.setIdentifierTag(new IdentifierTag(list.get(2).replaceAll("\"$", ""))); - addressTag.setRelay(new Relay(nodes[1].replaceAll("^\"", ""))); - - return addressTag; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/AuthorFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/AuthorFilter.java deleted file mode 100644 index 71881816d..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/AuthorFilter.java +++ /dev/null @@ -1,35 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import nostr.base.PublicKey; -import nostr.event.impl.GenericEvent; - -import java.util.function.Function; -import java.util.function.Predicate; - -@EqualsAndHashCode(callSuper = true) -public class AuthorFilter extends AbstractFilterable { - public static final String FILTER_KEY = "authors"; - - public AuthorFilter(T publicKey) { - super(publicKey, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> genericEvent.getPubKey().toHexString().equals(getFilterableValue()); - } - - @Override - public String getFilterableValue() { - return getAuthor().toHexString(); - } - - private T getAuthor() { - return super.getFilterable(); - } - - public static Function fxn = - node -> new AuthorFilter<>(new PublicKey(node.asText())); -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java index d3b564ba8..501d566dd 100644 --- a/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java +++ b/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java @@ -1,34 +1,214 @@ package nostr.event.filter; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; import nostr.event.impl.GenericEvent; +import nostr.event.json.EventJsonMapper; +import nostr.event.tag.GenericTag; -import java.util.function.Function; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.function.Predicate; -@EqualsAndHashCode(callSuper = true) -public class EventFilter extends AbstractFilterable { - public static final String FILTER_KEY = "ids"; +/** + * A composable NIP-01 event filter. + * + *

    Replaces the individual filter wrappers (KindFilter, AuthorFilter, etc.) + * with a single builder-based filter object. + */ +@Getter +@EqualsAndHashCode +@ToString +public class EventFilter { - public EventFilter(T event) { - super(event, FILTER_KEY); + private final List ids; + private final List authors; + private final List kinds; + private final Long since; + private final Long until; + private final Integer limit; + private final Map> tagFilters; + + private EventFilter(Builder builder) { + this.ids = Collections.unmodifiableList(builder.ids); + this.authors = Collections.unmodifiableList(builder.authors); + this.kinds = Collections.unmodifiableList(builder.kinds); + this.since = builder.since; + this.until = builder.until; + this.limit = builder.limit; + this.tagFilters = Collections.unmodifiableMap(builder.tagFilters); + } + + public static Builder builder() { + return new Builder(); } - @Override - public Predicate getPredicate() { - return (genericEvent) -> genericEvent.getId().equals(getFilterableValue()); + public Predicate toPredicate() { + Predicate predicate = e -> true; + + if (!ids.isEmpty()) { + predicate = predicate.and(e -> ids.contains(e.getId())); + } + if (!authors.isEmpty()) { + predicate = predicate.and(e -> authors.contains(e.getPubKey().toHexString())); + } + if (!kinds.isEmpty()) { + predicate = predicate.and(e -> kinds.contains(e.getKind())); + } + if (since != null) { + predicate = predicate.and(e -> e.getCreatedAt() != null && e.getCreatedAt() > since); + } + if (until != null) { + predicate = predicate.and(e -> e.getCreatedAt() != null && e.getCreatedAt() < until); + } + if (!tagFilters.isEmpty()) { + for (Map.Entry> entry : tagFilters.entrySet()) { + String tagCode = entry.getKey(); + List values = entry.getValue(); + predicate = predicate.and(e -> + e.getTags().stream() + .filter(GenericTag.class::isInstance) + .map(GenericTag.class::cast) + .filter(t -> t.getCode().equals(tagCode)) + .anyMatch(t -> t.getParams().stream().anyMatch(values::contains))); + } + } + + return predicate; + } + + /** + * Serializes this filter to a JSON object node. + */ + public ObjectNode toJsonNode() { + ObjectNode node = EventJsonMapper.getMapper().createObjectNode(); + + if (!ids.isEmpty()) { + ArrayNode arr = node.putArray("ids"); + ids.forEach(arr::add); + } + if (!authors.isEmpty()) { + ArrayNode arr = node.putArray("authors"); + authors.forEach(arr::add); + } + if (!kinds.isEmpty()) { + ArrayNode arr = node.putArray("kinds"); + kinds.forEach(arr::add); + } + if (since != null) { + node.put("since", since); + } + if (until != null) { + node.put("until", until); + } + if (limit != null) { + node.put("limit", limit); + } + for (Map.Entry> entry : tagFilters.entrySet()) { + ArrayNode arr = node.putArray("#" + entry.getKey()); + entry.getValue().forEach(arr::add); + } + + return node; } - @Override - public String getFilterableValue() { - return getEvent().getId(); + /** + * Serializes this filter to a JSON string. + */ + public String toJson() { + return toJsonNode().toString(); } - private T getEvent() { - return super.getFilterable(); + /** + * Deserializes an EventFilter from a JSON string. + */ + public static EventFilter fromJson(String json) { + try { + JsonNode root = EventJsonMapper.getMapper().readTree(json); + return fromJsonNode(root); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid filter JSON: " + json, e); + } } - public static Function fxn = - node -> new EventFilter<>(new GenericEvent(node.asText())); + /** + * Deserializes an EventFilter from a JsonNode. + */ + public static EventFilter fromJsonNode(JsonNode root) { + Builder builder = builder(); + + if (root.has("ids")) { + root.get("ids").forEach(n -> builder.ids.add(n.asText())); + } + if (root.has("authors")) { + root.get("authors").forEach(n -> builder.authors.add(n.asText())); + } + if (root.has("kinds")) { + root.get("kinds").forEach(n -> builder.kinds.add(n.asInt())); + } + if (root.has("since")) { + builder.since = root.get("since").asLong(); + } + if (root.has("until")) { + builder.until = root.get("until").asLong(); + } + if (root.has("limit")) { + builder.limit = root.get("limit").asInt(); + } + + Iterator fieldNames = root.fieldNames(); + while (fieldNames.hasNext()) { + String field = fieldNames.next(); + if (field.startsWith("#")) { + String tagCode = field.substring(1); + List values = new ArrayList<>(); + root.get(field).forEach(n -> values.add(n.asText())); + builder.tagFilters.put(tagCode, values); + } + } + + return builder.build(); + } + + public static class Builder { + private final List ids = new ArrayList<>(); + private final List authors = new ArrayList<>(); + private final List kinds = new ArrayList<>(); + private Long since; + private Long until; + private Integer limit; + private final Map> tagFilters = new HashMap<>(); + + public Builder ids(List ids) { this.ids.addAll(ids); return this; } + public Builder id(String id) { this.ids.add(id); return this; } + public Builder authors(List authors) { this.authors.addAll(authors); return this; } + public Builder author(String author) { this.authors.add(author); return this; } + public Builder kinds(List kinds) { this.kinds.addAll(kinds); return this; } + public Builder kind(int kind) { this.kinds.add(kind); return this; } + public Builder since(long since) { this.since = since; return this; } + public Builder until(long until) { this.until = until; return this; } + public Builder limit(int limit) { this.limit = limit; return this; } + public Builder addTagFilter(String tagCode, List values) { + this.tagFilters.computeIfAbsent(tagCode, k -> new ArrayList<>()).addAll(values); + return this; + } + public Builder addTagFilter(String tagCode, String value) { + this.tagFilters.computeIfAbsent(tagCode, k -> new ArrayList<>()).add(value); + return this; + } + + public EventFilter build() { + return new EventFilter(this); + } + } } diff --git a/nostr-java-event/src/main/java/nostr/event/filter/Filterable.java b/nostr-java-event/src/main/java/nostr/event/filter/Filterable.java deleted file mode 100644 index 50a5d17f5..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/Filterable.java +++ /dev/null @@ -1,93 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.NonNull; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; - -import java.util.List; -import java.util.Optional; -import java.util.function.Predicate; - -import static nostr.base.json.EventJsonMapper.mapper; - -public interface Filterable { - Predicate getPredicate(); - - T getFilterable(); - - Object getFilterableValue(); - - String getFilterKey(); - - static List getTypeSpecificTags( - @NonNull Class tagClass, @NonNull GenericEvent event) { - return event.getTags().stream().filter(tagClass::isInstance).map(tagClass::cast).toList(); - } - - /** - * Convenience: return the first tag of the specified type, if present. - */ - static java.util.Optional firstTagOfType( - @NonNull Class tagClass, @NonNull GenericEvent event) { - return getTypeSpecificTags(tagClass, event).stream().findFirst(); - } - - /** - * Convenience: return the first tag of the specified type and code, if present. - */ - static java.util.Optional firstTagOfTypeWithCode( - @NonNull Class tagClass, @NonNull String code, @NonNull GenericEvent event) { - return getTypeSpecificTags(tagClass, event).stream() - .filter(t -> code.equals(t.getCode())) - .findFirst(); - } - - /** - * Convenience: return the first tag of the specified type or throw with a clear message. - * - * Rationale: callers often need a single tag instance; this avoids repeated casts and stream code. - */ - static T requireTagOfType( - @NonNull Class tagClass, @NonNull GenericEvent event, @NonNull String errorMessage) { - return firstTagOfType(tagClass, event) - .orElseThrow(() -> new java.util.NoSuchElementException(errorMessage)); - } - - /** - * Convenience: return the first tag of the specified type and code or throw with a clear message. - */ - static T requireTagOfTypeWithCode( - @NonNull Class tagClass, - @NonNull String code, - @NonNull GenericEvent event, - @NonNull String errorMessage) { - return firstTagOfTypeWithCode(tagClass, code, event) - .orElseThrow(() -> new java.util.NoSuchElementException(errorMessage)); - } - - /** - * Convenience overload: generic error if not found. - */ - static T requireTagOfTypeWithCode( - @NonNull Class tagClass, @NonNull String code, @NonNull GenericEvent event) { - return requireTagOfTypeWithCode( - tagClass, code, event, "Missing required tag of type %s with code '%s'".formatted(tagClass.getSimpleName(), code)); - } - - default ObjectNode toObjectNode(ObjectNode objectNode) { - ArrayNode arrayNode = mapper().createArrayNode(); - - Optional.ofNullable(objectNode.get(getFilterKey())) - .ifPresent(jsonNode -> jsonNode.elements().forEachRemaining(arrayNode::add)); - - addToArrayNode(arrayNode); - - return objectNode.set(getFilterKey(), arrayNode); - } - - default void addToArrayNode(ArrayNode arrayNode) { - arrayNode.addAll(mapper().createArrayNode().add(getFilterableValue().toString())); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/Filters.java b/nostr-java-event/src/main/java/nostr/event/filter/Filters.java index 1c91b767e..0388f8756 100644 --- a/nostr-java-event/src/main/java/nostr/event/filter/Filters.java +++ b/nostr-java-event/src/main/java/nostr/event/filter/Filters.java @@ -4,72 +4,28 @@ import lombok.Getter; import lombok.NonNull; import lombok.ToString; -import nostr.base.IElement; +import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Objects; - -import static java.util.stream.Collectors.groupingBy; +/** + * Container for one or more EventFilter objects, used in REQ messages. + */ @Getter @EqualsAndHashCode @ToString -public class Filters implements IElement { - public static final int DEFAULT_FILTERS_LIMIT = 10; - private static final String FILTERS_EMPTY_ERROR = "Filters cannot be empty."; - private static final String FILTER_KEY_ERROR = "Filter key for filterable [%s] is not defined"; - private static final String POSITIVE_LIMIT_ERROR = "Limit must be positive."; - private final Map> filtersMap; - - private Integer limit = DEFAULT_FILTERS_LIMIT; - - public Filters(@NonNull Filterable... filterablesByDefaultType) { - this(List.of(filterablesByDefaultType)); - } - - public Filters(@NonNull List filterablesByDefaultType) { - this(filterablesByDefaultType.stream().collect(groupingBy(Filterable::getFilterKey))); - } +public class Filters { - private Filters(@NonNull Map> filterablesByCustomType) { - validateFiltersMap(filterablesByCustomType); - this.filtersMap = filterablesByCustomType; - } - - public List getFilterByType(@NonNull String type) { - return filtersMap.getOrDefault(type, List.of()); - } + private final List filters; - public void setLimit(@NonNull Integer limit) { - if (limit <= 0) { - throw new IllegalArgumentException(POSITIVE_LIMIT_ERROR); - } - this.limit = limit; + public Filters(@NonNull EventFilter... filters) { + this(List.of(filters)); } - private static void validateFiltersMap(Map> filtersMap) - throws IllegalArgumentException { - if (filtersMap.isEmpty()) { - throw new IllegalArgumentException(FILTERS_EMPTY_ERROR); + public Filters(@NonNull List filters) { + if (filters.isEmpty()) { + throw new IllegalArgumentException("Filters cannot be empty."); } - - filtersMap - .values() - .forEach( - filterables -> { - if (filterables.isEmpty()) { - throw new IllegalArgumentException(FILTERS_EMPTY_ERROR); - } - }); - - filtersMap.forEach( - (key, value) -> { - String filterKey = Objects.requireNonNullElse(key, ""); - if (filterKey.isEmpty()) { - throw new IllegalArgumentException( - String.format(FILTER_KEY_ERROR, value.getFirst().getFilterKey())); - } - }); + this.filters = Collections.unmodifiableList(filters); } } diff --git a/nostr-java-event/src/main/java/nostr/event/filter/GenericTagQueryFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/GenericTagQueryFilter.java deleted file mode 100644 index 765982d95..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/GenericTagQueryFilter.java +++ /dev/null @@ -1,55 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import nostr.base.ElementAttribute; -import nostr.base.GenericTagQuery; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.GenericTag; - -import java.util.function.Function; -import java.util.function.Predicate; - -@EqualsAndHashCode(callSuper = true) -public class GenericTagQueryFilter extends AbstractFilterable { - public static final String HASH_PREFIX = "#"; - - public GenericTagQueryFilter(T genericTagQuery) { - super(genericTagQuery, genericTagQuery.tagName()); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> - Filterable.getTypeSpecificTags(GenericTag.class, genericEvent).stream() - .filter(genericTag -> genericTag.getCode().equals(stripLeadingHashTag())) - .anyMatch( - genericTag -> - genericTag.getAttributes().stream() - .map(ElementAttribute::value) - .toList() - .contains(getFilterableValue())); - } - - @Override - public String getFilterKey() { - return getGenericTagQuery().tagName(); - } - - @Override - public String getFilterableValue() { - return getGenericTagQuery().value(); - } - - private T getGenericTagQuery() { - return super.getFilterable(); - } - - private String stripLeadingHashTag() { - return getFilterKey().startsWith(HASH_PREFIX) ? getFilterKey().substring(1) : getFilterKey(); - } - - public static Function fxn(String type) { - return node -> new GenericTagQueryFilter<>(new GenericTagQuery(type, node.asText())); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/GeohashTagFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/GeohashTagFilter.java deleted file mode 100644 index 5071a75b1..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/GeohashTagFilter.java +++ /dev/null @@ -1,37 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.GeohashTag; - -import java.util.function.Function; -import java.util.function.Predicate; - -@EqualsAndHashCode(callSuper = true) -public class GeohashTagFilter extends AbstractFilterable { - public static final String FILTER_KEY = "#g"; - - public GeohashTagFilter(T geohashTag) { - super(geohashTag, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> - Filterable.getTypeSpecificTags(GeohashTag.class, genericEvent).stream() - .anyMatch(geoHashTag -> geoHashTag.getLocation().equals(getFilterableValue())); - } - - @Override - public String getFilterableValue() { - return getGeoHashTag().getLocation(); - } - - private T getGeoHashTag() { - return super.getFilterable(); - } - - public static Function fxn = - node -> new GeohashTagFilter<>(new GeohashTag(node.asText())); -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/HashtagTagFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/HashtagTagFilter.java deleted file mode 100644 index 6b40f8caa..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/HashtagTagFilter.java +++ /dev/null @@ -1,37 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.HashtagTag; - -import java.util.function.Function; -import java.util.function.Predicate; - -@EqualsAndHashCode(callSuper = true) -public class HashtagTagFilter extends AbstractFilterable { - public static final String FILTER_KEY = "#t"; - - public HashtagTagFilter(T hashtagTag) { - super(hashtagTag, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> - Filterable.getTypeSpecificTags(HashtagTag.class, genericEvent).stream() - .anyMatch(hashtagTag -> hashtagTag.getHashTag().equals(getFilterableValue())); - } - - @Override - public String getFilterableValue() { - return getHashtagTag().getHashTag(); - } - - private T getHashtagTag() { - return super.getFilterable(); - } - - public static Function fxn = - node -> new HashtagTagFilter<>(new HashtagTag(node.asText())); -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/IdentifierTagFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/IdentifierTagFilter.java deleted file mode 100644 index a924130d5..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/IdentifierTagFilter.java +++ /dev/null @@ -1,39 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.IdentifierTag; - -import java.util.function.Function; -import java.util.function.Predicate; - -@EqualsAndHashCode(callSuper = true) -public class IdentifierTagFilter extends AbstractFilterable { - public static final String FILTER_KEY = "#d"; - - public IdentifierTagFilter(T identifierTag) { - super(identifierTag, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> - Filterable.getTypeSpecificTags(IdentifierTag.class, genericEvent).stream() - .anyMatch( - genericEventIdentifierTag -> - genericEventIdentifierTag.getUuid().equals(getFilterableValue())); - } - - @Override - public String getFilterableValue() { - return getIdentifierTag().getUuid(); - } - - private T getIdentifierTag() { - return super.getFilterable(); - } - - public static Function fxn = - node -> new IdentifierTagFilter<>(new IdentifierTag(node.asText())); -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/KindFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/KindFilter.java deleted file mode 100644 index 89ca57ef1..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/KindFilter.java +++ /dev/null @@ -1,43 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import lombok.EqualsAndHashCode; -import nostr.base.Kind; -import nostr.event.impl.GenericEvent; - -import java.util.function.Function; -import java.util.function.Predicate; - -import static nostr.base.json.EventJsonMapper.mapper; - -@EqualsAndHashCode(callSuper = true) -public class KindFilter extends AbstractFilterable { - public static final String FILTER_KEY = "kinds"; - - public KindFilter(T kind) { - super(kind, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> genericEvent.getKind().equals(getFilterableValue()); - } - - @Override - public void addToArrayNode(ArrayNode arrayNode) { - arrayNode.addAll(mapper().createArrayNode().add(getFilterableValue())); - } - - @Override - public Integer getFilterableValue() { - return getKind().getValue(); - } - - private T getKind() { - return super.getFilterable(); - } - - public static Function fxn = - node -> new KindFilter<>(Kind.valueOfStrict(node.asInt())); -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/ReferencedEventFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/ReferencedEventFilter.java deleted file mode 100644 index ec121727b..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/ReferencedEventFilter.java +++ /dev/null @@ -1,37 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.EventTag; - -import java.util.function.Function; -import java.util.function.Predicate; - -@EqualsAndHashCode(callSuper = true) -public class ReferencedEventFilter extends AbstractFilterable { - public static final String FILTER_KEY = "#e"; - - public ReferencedEventFilter(T referencedEventTag) { - super(referencedEventTag, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> - Filterable.getTypeSpecificTags(EventTag.class, genericEvent).stream() - .anyMatch(eventTag -> eventTag.getIdEvent().equals(getFilterableValue())); - } - - @Override - public String getFilterableValue() { - return getReferencedEventTag().getIdEvent(); - } - - private T getReferencedEventTag() { - return super.getFilterable(); - } - - public static Function fxn = - node -> new ReferencedEventFilter<>(new EventTag(node.asText())); -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/ReferencedPublicKeyFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/ReferencedPublicKeyFilter.java deleted file mode 100644 index 4966e480a..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/ReferencedPublicKeyFilter.java +++ /dev/null @@ -1,39 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import nostr.base.PublicKey; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.PubKeyTag; - -import java.util.function.Function; -import java.util.function.Predicate; - -@EqualsAndHashCode(callSuper = true) -public class ReferencedPublicKeyFilter extends AbstractFilterable { - public static final String FILTER_KEY = "#p"; - - public ReferencedPublicKeyFilter(T referencedPubKeyTag) { - super(referencedPubKeyTag, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> - Filterable.getTypeSpecificTags(PubKeyTag.class, genericEvent).stream() - .anyMatch( - pubKeyTag -> pubKeyTag.getPublicKey().toHexString().equals(getFilterableValue())); - } - - @Override - public String getFilterableValue() { - return getReferencedPublicKey().getPublicKey().toHexString(); - } - - private T getReferencedPublicKey() { - return super.getFilterable(); - } - - public static Function fxn = - node -> new ReferencedPublicKeyFilter<>(new PubKeyTag(new PublicKey(node.asText()))); -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/SinceFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/SinceFilter.java deleted file mode 100644 index d7f92b10b..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/SinceFilter.java +++ /dev/null @@ -1,43 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.EqualsAndHashCode; -import nostr.event.impl.GenericEvent; - -import java.util.List; -import java.util.function.Function; -import java.util.function.Predicate; - -import static nostr.base.json.EventJsonMapper.mapper; - -@EqualsAndHashCode(callSuper = true) -public class SinceFilter extends AbstractFilterable { - public static final String FILTER_KEY = "since"; - - public SinceFilter(Long since) { - super(since, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> genericEvent.getCreatedAt() > getSince(); - } - - @Override - public ObjectNode toObjectNode(ObjectNode objectNode) { - return mapper().createObjectNode().put(FILTER_KEY, getSince()); - } - - @Override - public String getFilterableValue() { - return getSince().toString(); - } - - private Long getSince() { - return super.getFilterable(); - } - - public static Function> fxn = - node -> List.of(new SinceFilter(node.asLong())); -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/UntilFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/UntilFilter.java deleted file mode 100644 index 0f20ff063..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/UntilFilter.java +++ /dev/null @@ -1,43 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.EqualsAndHashCode; -import nostr.event.impl.GenericEvent; - -import java.util.List; -import java.util.function.Function; -import java.util.function.Predicate; - -import static nostr.base.json.EventJsonMapper.mapper; - -@EqualsAndHashCode(callSuper = true) -public class UntilFilter extends AbstractFilterable { - public static final String FILTER_KEY = "until"; - - public UntilFilter(Long until) { - super(until, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> genericEvent.getCreatedAt() < getUntil(); - } - - @Override - public ObjectNode toObjectNode(ObjectNode objectNode) { - return mapper().createObjectNode().put(FILTER_KEY, getUntil()); - } - - @Override - public String getFilterableValue() { - return getUntil().toString(); - } - - private Long getUntil() { - return super.getFilterable(); - } - - public static Function> fxn = - node -> List.of(new UntilFilter(node.asLong())); -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/UrlTagFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/UrlTagFilter.java deleted file mode 100644 index 933745dcf..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/UrlTagFilter.java +++ /dev/null @@ -1,36 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.UrlTag; - -import java.util.function.Function; -import java.util.function.Predicate; - -public class UrlTagFilter extends AbstractFilterable { - - public static final String FILTER_KEY = "#u"; - - public UrlTagFilter(T urlTag) { - super(urlTag, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> - Filterable.getTypeSpecificTags(UrlTag.class, genericEvent).stream() - .anyMatch(urlTag -> urlTag.getUrl().equals(getFilterableValue())); - } - - @Override - public Object getFilterableValue() { - return getUrlTag().getUrl(); - } - - private T getUrlTag() { - return super.getFilterable(); - } - - public static Function fxn = - node -> new UrlTagFilter<>(new UrlTag(node.asText())); -} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/VoteTagFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/VoteTagFilter.java deleted file mode 100644 index e63d844fd..000000000 --- a/nostr-java-event/src/main/java/nostr/event/filter/VoteTagFilter.java +++ /dev/null @@ -1,37 +0,0 @@ -package nostr.event.filter; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.EqualsAndHashCode; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.VoteTag; - -import java.util.function.Function; -import java.util.function.Predicate; - -@EqualsAndHashCode(callSuper = true) -public class VoteTagFilter extends AbstractFilterable { - public static final String FILTER_KEY = "#v"; - - public VoteTagFilter(T voteTag) { - super(voteTag, FILTER_KEY); - } - - @Override - public Predicate getPredicate() { - return (genericEvent) -> - Filterable.getTypeSpecificTags(VoteTag.class, genericEvent).stream() - .anyMatch(voteTag -> voteTag.getVote().equals(getFilterableValue())); - } - - @Override - public Integer getFilterableValue() { - return getVoteTag().getVote(); - } - - private T getVoteTag() { - return super.getFilterable(); - } - - public static Function fxn = - node -> new VoteTagFilter<>(new VoteTag(node.asInt())); -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/AbstractBaseCalendarEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/AbstractBaseCalendarEvent.java deleted file mode 100644 index 1acb0558f..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/AbstractBaseCalendarEvent.java +++ /dev/null @@ -1,25 +0,0 @@ -package nostr.event.impl; - -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.BaseTag; -import nostr.event.JsonContent; -import nostr.event.NIP52Event; - -import java.util.List; - -@NoArgsConstructor -public abstract class AbstractBaseCalendarEvent extends NIP52Event { - - public AbstractBaseCalendarEvent( - @NonNull PublicKey sender, - @NonNull Kind kind, - @NonNull List baseTags, - @NonNull String content) { - super(sender, kind, baseTags, content); - } - - protected abstract T getCalendarContent(); -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/AbstractBaseNostrConnectEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/AbstractBaseNostrConnectEvent.java deleted file mode 100644 index de10023f9..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/AbstractBaseNostrConnectEvent.java +++ /dev/null @@ -1,33 +0,0 @@ -package nostr.event.impl; - -import lombok.NoArgsConstructor; -import nostr.base.PublicKey; -import nostr.event.BaseTag; -import nostr.event.tag.PubKeyTag; - -import java.util.List; - -@NoArgsConstructor -public abstract class AbstractBaseNostrConnectEvent extends EphemeralEvent { - public AbstractBaseNostrConnectEvent( - PublicKey pubKey, List baseTagList, String content) { - super(pubKey, 24_133, baseTagList, content); - } - - public PublicKey getActor() { - var pTag = - nostr.event.filter.Filterable.requireTagOfType( - PubKeyTag.class, this, "Invalid `tags`: missing PubKeyTag (p)"); - return pTag.getPublicKey(); - } - - public void validate() { - super.validate(); - - // 1. p - tag validation - nostr.event.filter.Filterable - .firstTagOfType(PubKeyTag.class, this) - .orElseThrow( - () -> new AssertionError("Invalid `tags`: Must include at least one valid PubKeyTag.")); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/AddressableEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/AddressableEvent.java deleted file mode 100644 index 400d4e44e..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/AddressableEvent.java +++ /dev/null @@ -1,47 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.NIP01Event; - -import java.util.List; - -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Addressable Events") -@NoArgsConstructor -public class AddressableEvent extends NIP01Event { - - public AddressableEvent(PublicKey pubKey, Integer kind, List tags, String content) { - super(pubKey, kind, tags, content); - } - - /** - * Validates that the event kind is within the addressable event range. - * - *

    Per NIP-01, addressable events (also called parameterized replaceable events) must have - * kinds in the range [30000, 40000). These events are replaceable and addressable via the - * combination of kind, pubkey, and 'd' tag. - * - * @throws AssertionError if kind is not in the valid range [30000, 40000) - */ - @Override - public void validateKind() { - super.validateKind(); - - Integer n = getKind(); - // NIP-01: Addressable events must be in range [30000, 40000) - if (n >= 30_000 && n < 40_000) { - return; // Valid addressable event kind - } - - throw new AssertionError( - String.format( - "Invalid kind value %d. Addressable events must be in range [30000, 40000).", n), - null); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CalendarDateBasedEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CalendarDateBasedEvent.java deleted file mode 100644 index d28c85285..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/CalendarDateBasedEvent.java +++ /dev/null @@ -1,129 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.entities.CalendarContent; -import nostr.event.json.deserializer.CalendarDateBasedEventDeserializer; -import nostr.event.tag.GenericTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.ReferenceTag; - -import java.util.Date; -import java.util.List; -import java.util.Optional; - -@Event(name = "Date-Based Calendar Event", nip = 52) -@JsonDeserialize(using = CalendarDateBasedEventDeserializer.class) -@NoArgsConstructor -public class CalendarDateBasedEvent - extends AbstractBaseCalendarEvent> { - - public CalendarDateBasedEvent(PublicKey sender, List baseTags, String content) { - super(sender, Kind.CALENDAR_DATE_BASED_EVENT, baseTags, content); - } - - public String getId() { - return getCalendarContent().getIdentifierTag().getUuid(); - } - - public String getTile() { - return getCalendarContent().getTitle(); - } - - public Date getStart() { - return new Date(getCalendarContent().getStart()); - } - - public Optional getEnd() { - CalendarContent calendarContent = getCalendarContent(); - Optional end = calendarContent.getEnd(); - return end.map(Date::new); - } - - public Optional getLocation() { - return getCalendarContent().getLocation(); - } - - public Optional getGeohash() { - Optional geohashTag = getCalendarContent().getGeohashTag(); - return geohashTag.map(GeohashTag::getLocation); - } - - public List getParticipants() { - return getCalendarContent().getParticipantPubKeyTags(); - } - - public List getHashtags() { - return getCalendarContent().getHashtagTags(); - } - - public List getReferences() { - return getCalendarContent().getReferenceTags(); - } - - @Override - protected CalendarContent getCalendarContent() { - CalendarContent calendarContent = - new CalendarContent<>( - nostr.event.filter.Filterable.requireTagOfTypeWithCode( - IdentifierTag.class, "d", this), - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "title", this) - .getAttributes() - .get(0) - .value() - .toString(), - Long.parseLong( - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "start", this) - .getAttributes() - .get(0) - .value() - .toString())); - - // Update the calendarContent object with the values from the tags - nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GenericTag.class, "end", this) - .ifPresent( - tag -> - calendarContent.setEnd( - Long.parseLong(tag.getAttributes().get(0).value().toString()))); - - nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GenericTag.class, "location", this) - .ifPresent(tag -> calendarContent.setLocation(tag.getAttributes().get(0).value().toString())); - - nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GeohashTag.class, "g", this) - .ifPresent(calendarContent::setGeohashTag); - - nostr.event.filter.Filterable - .getTypeSpecificTags(PubKeyTag.class, this) - .forEach(calendarContent::addParticipantPubKeyTag); - - nostr.event.filter.Filterable - .getTypeSpecificTags(HashtagTag.class, this) - .forEach(calendarContent::addHashtagTag); - - nostr.event.filter.Filterable - .getTypeSpecificTags(ReferenceTag.class, this) - .forEach(calendarContent::addReferenceTag); - - return calendarContent; - } - - @Override - public void validateKind() { - if (getKind() != Kind.CALENDAR_DATE_BASED_EVENT.getValue()) { - throw new AssertionError( - "Invalid kind value. Expected " + Kind.CALENDAR_DATE_BASED_EVENT.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CalendarEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CalendarEvent.java deleted file mode 100644 index 0c7075ec5..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/CalendarEvent.java +++ /dev/null @@ -1,91 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.JsonContent; -import nostr.event.entities.CalendarContent; -import nostr.event.json.deserializer.CalendarEventDeserializer; -import nostr.event.tag.AddressTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.IdentifierTag; - -import java.util.List; - -@Event(name = "Calendar Event", nip = 52) -@JsonDeserialize(using = CalendarEventDeserializer.class) -@NoArgsConstructor -public class CalendarEvent extends AbstractBaseCalendarEvent { - - public CalendarEvent( - @NonNull PublicKey sender, @NonNull List baseTags, @NonNull String content) { - super(sender, Kind.CALENDAR_EVENT, baseTags, content); - } - - public String getId() { - return getCalendarContent().getIdentifierTag().getUuid(); - } - - public String getTitle() { - return getCalendarContent().getTitle(); - } - - public List getCalendarEventIds() { - return getCalendarContent().getAddressTags().stream() - .map(tag -> tag.getIdentifierTag().getUuid()) - .toList(); - } - - public List getCalendarEventAuthors() { - return getCalendarContent().getAddressTags().stream().map(AddressTag::getPublicKey).toList(); - } - - @Override - protected CalendarContent getCalendarContent() { - - IdentifierTag idTag = - nostr.event.filter.Filterable.requireTagOfTypeWithCode(IdentifierTag.class, "d", this); - String title = - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "title", this) - .getAttributes() - .get(0) - .value() - .toString(); - - CalendarContent calendarContent = new CalendarContent<>(idTag, title, -1L); - - nostr.event.filter.Filterable - .getTypeSpecificTags(AddressTag.class, this) - .forEach(calendarContent::addAddressTag); - - return calendarContent; - } - - @Override - protected void validateTags() { - super.validateTags(); - - // Validate required tags ("d", "title") - if (nostr.event.filter.Filterable.firstTagOfTypeWithCode(IdentifierTag.class, "d", this) - .isEmpty()) { - throw new AssertionError("Missing `d` tag for the event identifier."); - } - - if (nostr.event.filter.Filterable.firstTagOfTypeWithCode(GenericTag.class, "title", this) - .isEmpty()) { - throw new AssertionError("Missing `title` tag for the event title."); - } - } - - @Override - public void validateKind() { - if (getKind() != Kind.CALENDAR_EVENT.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.CALENDAR_EVENT.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpEvent.java deleted file mode 100644 index 40102d526..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpEvent.java +++ /dev/null @@ -1,126 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.annotation.JsonValue; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.entities.CalendarRsvpContent; -import nostr.event.json.deserializer.CalendarRsvpEventDeserializer; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; - -import java.util.List; -import java.util.Optional; - -@EqualsAndHashCode(callSuper = false) -@Event(name = "CalendarRsvpEvent", nip = 52) -@JsonDeserialize(using = CalendarRsvpEventDeserializer.class) -@NoArgsConstructor -public class CalendarRsvpEvent extends AbstractBaseCalendarEvent { - - public enum Status { - ACCEPTED("accepted"), - TENTATIVE("tentative"), - DECLINED("declined"); - - private final String status; - - Status(String status) { - this.status = status; - } - - @JsonValue - public String getStatus() { - return status; - } - } - - public enum FB { - FREE("free"), - BUSY("busy"); - - private final String value; - - FB(String fb) { - this.value = fb; - } - - @JsonValue - public String getValue() { - return value; - } - } - - public CalendarRsvpEvent( - @NonNull PublicKey sender, @NonNull List baseTags, @NonNull String content) { - super(sender, Kind.CALENDAR_RSVP_EVENT, baseTags, content); - } - - public Status getStatus() { - return Status.valueOf(getCalendarContent().getStatus().toUpperCase()); - } - - public Optional getFB() { - return getCalendarContent() - .getFbTag() - .map(fbTag -> fbTag.getAttributes().get(0).value().toString().toUpperCase()) - .map(FB::valueOf); - } - - public Optional getEventId() { - return getCalendarContent().getEventTag().map(EventTag::getIdEvent); - } - - public String getId() { - return getCalendarContent().getIdentifierTag().getUuid(); - } - - public Optional getAuthor() { - return getCalendarContent().getAuthorPubKeyTag().map(PubKeyTag::getPublicKey); - } - - @Override - protected CalendarRsvpContent getCalendarContent() { - CalendarRsvpContent calendarRsvpContent = - CalendarRsvpContent.builder( - nostr.event.filter.Filterable.requireTagOfTypeWithCode( - IdentifierTag.class, "d", this), - nostr.event.filter.Filterable.requireTagOfTypeWithCode( - AddressTag.class, "a", this), - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "status", this) - .getAttributes() - .get(0) - .value() - .toString()) - .build(); - - nostr.event.filter.Filterable - .firstTagOfType(EventTag.class, this) - .ifPresent(calendarRsvpContent::setEventTag); - // FB tag is encoded as a generic tag with code 'fb' - Optional.ofNullable(getTag("fb")) - .ifPresent(baseTag -> calendarRsvpContent.setFbTag((GenericTag) baseTag)); - nostr.event.filter.Filterable - .firstTagOfType(PubKeyTag.class, this) - .ifPresent(calendarRsvpContent::setAuthorPubKeyTag); - - return calendarRsvpContent; - } - - @Override - public void validateKind() { - if (getKind() != Kind.CALENDAR_RSVP_EVENT.getValue()) { - throw new AssertionError( - "Invalid kind value. Expected " + Kind.CALENDAR_RSVP_EVENT.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CalendarTimeBasedEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CalendarTimeBasedEvent.java deleted file mode 100644 index da5240aa2..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/CalendarTimeBasedEvent.java +++ /dev/null @@ -1,99 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.entities.CalendarContent; -import nostr.event.json.deserializer.CalendarTimeBasedEventDeserializer; -import nostr.event.tag.GenericTag; -import nostr.event.tag.LabelTag; - -import java.util.List; -import java.util.Optional; - -@EqualsAndHashCode(callSuper = false) -@Event(name = "Time-Based Calendar Event", nip = 52) -@JsonDeserialize(using = CalendarTimeBasedEventDeserializer.class) -@NoArgsConstructor -public class CalendarTimeBasedEvent extends CalendarDateBasedEvent { - - public CalendarTimeBasedEvent( - @NonNull PublicKey sender, @NonNull List baseTags, @NonNull String content) { - super(sender, baseTags, content); - this.setKind(Kind.CALENDAR_TIME_BASED_EVENT.getValue()); - } - - public Optional getStartTzid() { - return getCalendarContent().getStartTzid(); - } - - public Optional getEndTzid() { - return getCalendarContent().getEndTzid(); - } - - public Optional getSummary() { - return getCalendarContent().getSummary(); - } - - public Optional getLocation() { - return super.getLocation(); - } - - public List getLabels() { - List labelTags = getCalendarContent().getLabelTags(); - return labelTags.stream().map(l -> "#" + l.getNameSpace() + "." + l.getLabel()).toList(); - } - - @Override - protected CalendarContent getCalendarContent() { - CalendarContent calendarContent = super.getCalendarContent(); - - // Update the calendarContent object with the values from the tags - calendarContent.setStartTzid( - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "start_tzid", this) - .getAttributes() - .get(0) - .value() - .toString()); - calendarContent.setEndTzid( - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "end_tzid", this) - .getAttributes() - .get(0) - .value() - .toString()); - calendarContent.setSummary( - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "summary", this) - .getAttributes() - .get(0) - .value() - .toString()); - calendarContent.setLocation( - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "location", this) - .getAttributes() - .get(0) - .value() - .toString()); - nostr.event.filter.Filterable - .getTypeSpecificTags(LabelTag.class, this) - .forEach(calendarContent::addLabelTag); - - return calendarContent; - } - - @Override - public void validateKind() { - if (getKind() != Kind.CALENDAR_TIME_BASED_EVENT.getValue()) { - throw new AssertionError( - "Invalid kind value. Expected " + Kind.CALENDAR_TIME_BASED_EVENT.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CanonicalAuthenticationEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CanonicalAuthenticationEvent.java deleted file mode 100644 index e87c10dfd..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/CanonicalAuthenticationEvent.java +++ /dev/null @@ -1,65 +0,0 @@ -package nostr.event.impl; - -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.tag.GenericTag; - -import java.util.List; - -/** - * @author squirrel - */ -@Event(name = "Canonical authentication event", nip = 42) -@NoArgsConstructor -public class CanonicalAuthenticationEvent extends EphemeralEvent { - - public CanonicalAuthenticationEvent( - @NonNull PublicKey pubKey, @NonNull List tags, @NonNull String content) { - super(pubKey, Kind.CLIENT_AUTH, tags, content); - } - - public String getChallenge() { - return nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GenericTag.class, "challenge", this) - .filter(tag -> !tag.getAttributes().isEmpty()) - .map(tag -> tag.getAttributes().get(0).value().toString()) - .orElse(null); - } - - public Relay getRelay() { - return nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GenericTag.class, "relay", this) - .filter(tag -> !tag.getAttributes().isEmpty()) - .map(tag -> new Relay(tag.getAttributes().get(0).value().toString())) - .orElse(null); - } - - @Override - protected void validateTags() { - super.validateTags(); - - // Check 'challenge' tag - nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GenericTag.class, "challenge", this) - .filter(tag -> !tag.getAttributes().isEmpty()) - .orElseThrow(() -> new AssertionError("Missing or invalid `challenge` tag.")); - - // Check 'relay' tag - nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GenericTag.class, "relay", this) - .filter(tag -> !tag.getAttributes().isEmpty()) - .orElseThrow(() -> new AssertionError("Missing or invalid `relay` tag.")); - } - - @Override - public void validateKind() { - if (getKind() != Kind.CLIENT_AUTH.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.CLIENT_AUTH.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/ChannelCreateEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/ChannelCreateEvent.java deleted file mode 100644 index 761f7bd26..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/ChannelCreateEvent.java +++ /dev/null @@ -1,63 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.base.json.EventJsonMapper; -import nostr.event.entities.ChannelProfile; -import nostr.event.json.codec.EventEncodingException; - -import java.util.ArrayList; - -/** - * @author guilhermegps - */ -@Event(name = "Create Channel", nip = 28) -@NoArgsConstructor -public class ChannelCreateEvent extends GenericEvent { - - public ChannelCreateEvent(PublicKey pubKey, String content) { - super(pubKey, Kind.CHANNEL_CREATE, new ArrayList<>(), content); - } - - public ChannelProfile getChannelProfile() { - String content = getContent(); - try { - return EventJsonMapper.mapper().readValue(content, ChannelProfile.class); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to parse channel profile content", ex); - } - } - - @Override - protected void validateKind() { - if (getKind() != Kind.CHANNEL_CREATE.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.CHANNEL_CREATE.getValue()); - } - } - - protected void validateContent() { - super.validateContent(); - - try { - ChannelProfile profile = getChannelProfile(); - - if (profile.getName() == null || profile.getName().isEmpty()) { - throw new AssertionError("Invalid `content`: `name` field is required."); - } - - if (profile.getAbout() == null || profile.getAbout().isEmpty()) { - throw new AssertionError("Invalid `content`: `about` field is required."); - } - - if (profile.getPicture() == null) { - throw new AssertionError("Invalid `content`: `picture` field is required."); - } - - } catch (EventEncodingException e) { - throw new AssertionError("Invalid `content`: Must be a valid ChannelProfile JSON object.", e); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/ChannelMessageEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/ChannelMessageEvent.java deleted file mode 100644 index 219448721..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/ChannelMessageEvent.java +++ /dev/null @@ -1,138 +0,0 @@ -package nostr.event.impl; - -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.Marker; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.tag.EventTag; - -import java.util.ArrayList; -import java.util.List; - -/** - * @author guilhermegps - */ -@Event(name = "Channel Message", nip = 28) -@NoArgsConstructor -public class ChannelMessageEvent extends GenericEvent { - - public ChannelMessageEvent(PublicKey pubKey, List baseTags, String content) { - super(pubKey, Kind.CHANNEL_MESSAGE, baseTags, content); - } - - public String getChannelCreateEventId() { - return nostr.event.filter.Filterable.getTypeSpecificTags(EventTag.class, this).stream() - .filter(tag -> tag.getMarkerOptional().filter(m -> m == Marker.ROOT).isPresent()) - .map(EventTag::getIdEvent) - .findFirst() - .orElseThrow(); - } - - public String getChannelMessageReplyEventId() { - return nostr.event.filter.Filterable.getTypeSpecificTags(EventTag.class, this).stream() - .filter(tag -> tag.getMarkerOptional().filter(m -> m == Marker.REPLY).isPresent()) - .map(EventTag::getIdEvent) - .findFirst() - .orElse(null); - } - - public Relay getRootRecommendedRelay() { - return nostr.event.filter.Filterable.getTypeSpecificTags(EventTag.class, this).stream() - .filter(tag -> tag.getMarkerOptional().filter(m -> m == Marker.ROOT).isPresent()) - .map(EventTag::getRecommendedRelayUrlOptional) - .flatMap(java.util.Optional::stream) - .map(Relay::new) - .findFirst() - .orElse(null); - } - - public Relay getReplyRecommendedRelay(@NonNull String eventId) { - return nostr.event.filter.Filterable.getTypeSpecificTags(EventTag.class, this).stream() - .filter(tag -> tag.getMarkerOptional().filter(m -> m == Marker.REPLY).isPresent() - && tag.getIdEvent().equals(eventId)) - .map(EventTag::getRecommendedRelayUrlOptional) - .flatMap(java.util.Optional::stream) - .map(Relay::new) - .findFirst() - .orElse(null); - } - - public void validate() { - super.validate(); - - // Check 'e' root - tag - EventTag rootTag = - nostr.event.filter.Filterable.getTypeSpecificTags(EventTag.class, this).stream() - .filter(tag -> tag.getMarkerOptional().filter(m -> m == Marker.ROOT).isPresent()) - .findFirst() - .orElseThrow(() -> new AssertionError("Missing or invalid `e` root tag.")); - } - - public ChannelMessageEvent( - @NonNull PublicKey pubKey, - @NonNull ChannelCreateEvent rootEvent, - String content, - Relay recommendedRelay) { - super(pubKey, Kind.CHANNEL_MESSAGE, new ArrayList<>(), content); - final EventTag eventTag = - EventTag.builder().idEvent(rootEvent.getId()).marker(Marker.ROOT).build(); - if (recommendedRelay != null) { - eventTag.setRecommendedRelayUrl((recommendedRelay.getUri())); - } - this.addTag(eventTag); - } - - public ChannelMessageEvent( - @NonNull PublicKey pubKey, - @NonNull ChannelCreateEvent rootEvent, - @NonNull ChannelMessageEvent replyEvent, - String content) { - super(pubKey, Kind.CHANNEL_MESSAGE, new ArrayList<>(), content); - this.addTag(EventTag.builder().idEvent(rootEvent.getId()).marker(Marker.ROOT).build()); - this.addTag(EventTag.builder().idEvent(replyEvent.getId()).marker(Marker.REPLY).build()); - } - - public ChannelMessageEvent( - @NonNull PublicKey pubKey, - @NonNull ChannelCreateEvent rootEvent, - @NonNull ChannelMessageEvent replyEvent, - String content, - Relay recommendedRelay) { - this(pubKey, rootEvent, replyEvent, content, recommendedRelay, recommendedRelay); - } - - public ChannelMessageEvent( - @NonNull PublicKey pubKey, - ChannelCreateEvent rootEvent, - ChannelMessageEvent replyEvent, - String content, - Relay recommendedRelayRoot, - Relay recommendedRelayReply) { - super(pubKey, Kind.CHANNEL_MESSAGE, new ArrayList<>(), content); - - final EventTag rootEventTag = - EventTag.builder().idEvent(rootEvent.getId()).marker(Marker.ROOT).build(); - if (recommendedRelayRoot != null) { - rootEventTag.setRecommendedRelayUrl(recommendedRelayRoot.getUri()); - } - this.addTag(rootEventTag); - - final EventTag replyEventTag = - EventTag.builder().idEvent(replyEvent.getId()).marker(Marker.REPLY).build(); - if (recommendedRelayReply != null) { - replyEventTag.setRecommendedRelayUrl(recommendedRelayReply.getUri()); - } - this.addTag(replyEventTag); - } - - @Override - protected void validateKind() { - if (getKind() != Kind.CHANNEL_MESSAGE.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.CHANNEL_MESSAGE.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/ChannelMetadataEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/ChannelMetadataEvent.java deleted file mode 100644 index ed504fb09..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/ChannelMetadataEvent.java +++ /dev/null @@ -1,94 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.Marker; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.base.json.EventJsonMapper; -import nostr.event.BaseTag; -import nostr.event.entities.ChannelProfile; -import nostr.event.json.codec.EventEncodingException; -import nostr.event.tag.EventTag; -import nostr.event.tag.HashtagTag; - -import java.util.List; - -/** - * @author guilhermegps - */ -@Event(name = "Channel Metadata", nip = 28) -@NoArgsConstructor -public class ChannelMetadataEvent extends GenericEvent { - - public ChannelMetadataEvent(PublicKey pubKey, List baseTagList, String content) { - super(pubKey, Kind.CHANNEL_METADATA, baseTagList, content); - } - - public ChannelProfile getChannelProfile() { - String content = getContent(); - try { - return EventJsonMapper.mapper().readValue(content, ChannelProfile.class); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to parse channel profile content", ex); - } - } - - @Override - protected void validateContent() { - super.validateContent(); - - try { - ChannelProfile profile = getChannelProfile(); - - if (profile.getName() == null || profile.getName().isEmpty()) { - throw new AssertionError("Invalid `content`: `name` field is required."); - } - - if (profile.getAbout() == null || profile.getAbout().isEmpty()) { - throw new AssertionError("Invalid `content`: `about` field is required."); - } - - if (profile.getPicture() == null) { - throw new AssertionError("Invalid `content`: `picture` field is required."); - } - } catch (EventEncodingException e) { - throw new AssertionError("Invalid `content`: Must be a valid ChannelProfile JSON object.", e); - } - } - - public String getChannelCreateEventId() { - return nostr.event.filter.Filterable.getTypeSpecificTags(EventTag.class, this).stream() - .filter(tag -> tag.getMarkerOptional().filter(m -> m == Marker.ROOT).isPresent()) - .map(EventTag::getIdEvent) - .findFirst() - .orElseThrow(); - } - - public List getCategories() { - return nostr.event.filter.Filterable.getTypeSpecificTags(HashtagTag.class, this).stream() - .map(HashtagTag::getHashTag) - .toList(); - } - - protected void validateTags() { - super.validateTags(); - - // Check 'e' root - tag - EventTag rootTag = - nostr.event.filter.Filterable - .getTypeSpecificTags(EventTag.class, this) - .stream() - .filter(tag -> tag.getMarkerOptional().filter(m -> m == Marker.ROOT).isPresent()) - .findFirst() - .orElseThrow(() -> new AssertionError("Missing or invalid `e` root tag.")); - } - - @Override - protected void validateKind() { - if (getKind() != Kind.CHANNEL_METADATA.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.CHANNEL_METADATA.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CheckoutEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CheckoutEvent.java deleted file mode 100644 index 05e820d66..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/CheckoutEvent.java +++ /dev/null @@ -1,71 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.annotation.JsonValue; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import nostr.base.PublicKey; -import nostr.event.BaseTag; -import nostr.event.entities.NIP15Content; - -import java.util.List; - -/** - * @author eric - */ -@Data -@NoArgsConstructor -@EqualsAndHashCode(callSuper = false) -public abstract class CheckoutEvent - extends DirectMessageEvent { - - private MessageType messageType; - - public CheckoutEvent( - PublicKey sender, List tags, String content, MessageType messageType) { - super(sender, tags, content); - this.messageType = messageType; - } - - public enum MessageType { - NEW_ORDER(0, "CustomerOrder"), - PAYMENT_REQUEST(1, "Merchant"), - ORDER_STATUS_UPDATE(2, "Merchant"); - - private final int value; - @Getter private final String sentBy; - - MessageType(int value, String sentBy) { - this.value = value; - this.sentBy = sentBy; - } - - @JsonValue - public int getValue() { - return value; - } - } - - protected abstract T getEntity(); - - @Override - protected void validateContent() { - super.validateContent(); - - try { - T entity = getEntity(); - if (entity == null) { - throw new AssertionError("Invalid `content`: Must be a valid CustomerOrder JSON object."); - } - - if (entity.getMessageType() != this.messageType) { - throw new AssertionError( - "Invalid `content`: The `messageType` field must match the entity's `messageType`."); - } - - } catch (Exception e) { - throw new AssertionError("Invalid `content`: Must be a valid CustomerOrder JSON object.", e); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/ClassifiedListingEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/ClassifiedListingEvent.java deleted file mode 100644 index 4a5b413d6..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/ClassifiedListingEvent.java +++ /dev/null @@ -1,173 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.NIP99Event; -import nostr.event.json.deserializer.ClassifiedListingEventDeserializer; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PriceTag; - -import java.time.Instant; -import java.util.List; - -@EqualsAndHashCode(callSuper = false) -@Event(name = "ClassifiedListingEvent", nip = 99) -@JsonDeserialize(using = ClassifiedListingEventDeserializer.class) -@NoArgsConstructor -public class ClassifiedListingEvent extends NIP99Event { - - public ClassifiedListingEvent(PublicKey pubKey, Kind kind, List tags, String content) { - super(pubKey, kind, tags, content); - } - - @Getter - public enum Status { - ACTIVE("active"), - SOLD("sold"); - - private final String value; - - Status(@NonNull String value) { - this.value = value; - } - } - - public Instant getPublishedAt() { - var tag = - nostr.event.filter.Filterable.requireTagOfTypeWithCode( - GenericTag.class, "published_at", this); - return Instant.ofEpochSecond(Long.parseLong(tag.getAttributes().get(0).value().toString())); - } - - public String getLocation() { - return nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "location", this) - .getAttributes() - .get(0) - .value() - .toString(); - } - - public String getTitle() { - return nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "title", this) - .getAttributes() - .get(0) - .value() - .toString(); - } - - public String getSummary() { - return nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "summary", this) - .getAttributes() - .get(0) - .value() - .toString(); - } - - public String getImage() { - return nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "image", this) - .getAttributes() - .get(0) - .value() - .toString(); - } - - public Status getStatus() { - String status = - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "status", this) - .getAttributes() - .get(0) - .value() - .toString(); - return Status.valueOf(status); - } - - public String getPrice() { - PriceTag priceTag = - (PriceTag) - getTags().stream().filter(tag -> tag instanceof PriceTag).findFirst().orElseThrow(); - - return priceTag.getNumber().toString() - + " " - + priceTag.getCurrency() - + priceTag.getFrequencyOptional().map(f -> " " + f).orElse(""); - } - - @Override - protected void validateTags() { - super.validateTags(); - - // Validate published_at - try { - Long.parseLong( - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "published_at", this) - .getAttributes() - .get(0) - .value() - .toString()); - } catch (java.util.NoSuchElementException e) { - throw new AssertionError("Missing `published_at` tag for the publication date/time."); - } catch (NumberFormatException e) { - throw new AssertionError("Invalid `published_at` tag value: must be a numeric timestamp."); - } - - // Validate location - if (nostr.event.filter.Filterable.firstTagOfTypeWithCode(GenericTag.class, "location", this) - .isEmpty()) { - throw new AssertionError("Missing `location` tag for the listing location."); - } - - // Validate title - if (nostr.event.filter.Filterable.firstTagOfTypeWithCode(GenericTag.class, "title", this) - .isEmpty()) { - throw new AssertionError("Missing `title` tag for the listing title."); - } - - // Validate summary - if (nostr.event.filter.Filterable.firstTagOfTypeWithCode(GenericTag.class, "summary", this) - .isEmpty()) { - throw new AssertionError("Missing `summary` tag for the listing summary."); - } - - // Validate image - if (nostr.event.filter.Filterable.firstTagOfTypeWithCode(GenericTag.class, "image", this) - .isEmpty()) { - throw new AssertionError("Missing `image` tag for the listing image."); - } - - // Validate status - if (nostr.event.filter.Filterable.firstTagOfTypeWithCode(GenericTag.class, "status", this) - .isEmpty()) { - throw new AssertionError("Missing `status` tag for the listing status."); - } - } - - @Override - public void validateKind() { - var n = getKind(); - // Accept only NIP-99 classified listing kinds - if (n == Kind.CLASSIFIED_LISTING.getValue() || n == Kind.CLASSIFIED_LISTING_INACTIVE.getValue()) { - return; - } - - throw new AssertionError( - String.format( - "Invalid kind value [%s]. Classified Listing must be either %d or %d", - n, - Kind.CLASSIFIED_LISTING.getValue(), - Kind.CLASSIFIED_LISTING_INACTIVE.getValue()), - null); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/ContactListEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/ContactListEvent.java deleted file mode 100644 index 11afd1b38..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/ContactListEvent.java +++ /dev/null @@ -1,43 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; - -import java.util.List; - -/** - * @author eric - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Contact List and Petnames", nip = 2) -@NoArgsConstructor -public class ContactListEvent extends GenericEvent { - - public ContactListEvent(@NonNull PublicKey pubKey, @NonNull List tags) { - super(pubKey, Kind.CONTACT_LIST, tags); - } - - @Override - protected void validateTags() { - super.validateTags(); - - boolean hasPTag = getTags().stream().anyMatch(t -> "p".equals(t.getCode())); - if (!hasPTag) { - throw new AssertionError("Missing `p` tag for contact list entries."); - } - } - - @Override - protected void validateKind() { - if (getKind() != Kind.CONTACT_LIST.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.CONTACT_LIST.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateProductEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateProductEvent.java deleted file mode 100644 index ddf2820c4..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateProductEvent.java +++ /dev/null @@ -1,68 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.base.json.EventJsonMapper; -import nostr.event.BaseTag; -import nostr.event.entities.Product; -import nostr.event.json.codec.EventEncodingException; - -import java.util.List; - -/** - * @author eric - */ -@Event(name = "Create Or Update Product Event", nip = 15) -@NoArgsConstructor -public class CreateOrUpdateProductEvent extends MerchantEvent { - - public CreateOrUpdateProductEvent(PublicKey sender, List tags, @NonNull String content) { - super(sender, 30_018, tags, content); - } - - public Product getProduct() { - try { - return EventJsonMapper.mapper().readValue(getContent(), Product.class); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to parse product content", ex); - } - } - - protected Product getEntity() { - return getProduct(); - } - - @Override - public void validateKind() { - if (getKind() != Kind.PRODUCT_CREATE_OR_UPDATE.getValue()) { - throw new AssertionError( - "Invalid kind value. Expected " + Kind.PRODUCT_CREATE_OR_UPDATE.getValue()); - } - } - - protected void validateContent() { - super.validateContent(); - - try { - Product product = getProduct(); - - if (product.getName() == null || product.getName().isEmpty()) { - throw new AssertionError("Invalid `content`: `name` field is required."); - } - - if (product.getCurrency() == null || product.getCurrency().isEmpty()) { - throw new AssertionError("Invalid `content`: `currency` field is required."); - } - - if (product.getPrice() == null) { - throw new AssertionError("Invalid `content`: `price` field is required."); - } - } catch (EventEncodingException e) { - throw new AssertionError("Invalid `content`: Must be a valid Product JSON object.", e); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateStallEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateStallEvent.java deleted file mode 100644 index bf83b2afe..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateStallEvent.java +++ /dev/null @@ -1,69 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.base.json.EventJsonMapper; -import nostr.event.BaseTag; -import nostr.event.entities.Stall; -import nostr.event.json.codec.EventEncodingException; - -import java.util.List; - -/** - * @author eric - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Create or update a stall", nip = 15) -@NoArgsConstructor -public class CreateOrUpdateStallEvent extends MerchantEvent { - - public CreateOrUpdateStallEvent( - @NonNull PublicKey sender, @NonNull List tags, @NonNull String content) { - super(sender, Kind.STALL_CREATE_OR_UPDATE.getValue(), tags, content); - } - - public Stall getStall() { - try { - return EventJsonMapper.mapper().readValue(getContent(), Stall.class); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to parse stall content", ex); - } - } - - @Override - protected Stall getEntity() { - return getStall(); - } - - @Override - public void validateKind() { - if (getKind() != Kind.STALL_CREATE_OR_UPDATE.getValue()) { - throw new AssertionError( - "Invalid kind value. Expected " + Kind.STALL_CREATE_OR_UPDATE.getValue()); - } - } - - protected void validateContent() { - super.validateContent(); - - try { - Stall stall = getStall(); - - if (stall.getName() == null || stall.getName().isEmpty()) { - throw new AssertionError("Invalid `content`: `name` field is required."); - } - if (stall.getCurrency() == null || stall.getCurrency().isEmpty()) { - throw new AssertionError("Invalid `content`: `currency` field is required."); - } - } catch (EventEncodingException e) { - throw new AssertionError("Invalid `content`: Must be a valid Stall JSON object.", e); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CustomerOrderEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CustomerOrderEvent.java deleted file mode 100644 index 2eafa3838..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/CustomerOrderEvent.java +++ /dev/null @@ -1,51 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.base.json.EventJsonMapper; -import nostr.event.BaseTag; -import nostr.event.entities.CustomerOrder; -import nostr.event.json.codec.EventEncodingException; - -import java.util.List; - -/** - * @author eric - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Customer Order Event", nip = 15) -@NoArgsConstructor -public class CustomerOrderEvent extends CheckoutEvent { - - public CustomerOrderEvent( - @NonNull PublicKey sender, @NonNull List tags, @NonNull String content) { - super(sender, tags, content, MessageType.NEW_ORDER); - } - - public CustomerOrder getCustomerOrder() { - try { - return EventJsonMapper.mapper().readValue(getContent(), CustomerOrder.class); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to parse customer order content", ex); - } - } - - protected CustomerOrder getEntity() { - return getCustomerOrder(); - } - - @Override - public void validateKind() { - if (getKind() != Kind.ENCRYPTED_DIRECT_MESSAGE.getValue()) { - throw new AssertionError( - "Invalid kind value. Expected " + Kind.ENCRYPTED_DIRECT_MESSAGE.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/DeletionEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/DeletionEvent.java deleted file mode 100644 index 0efe3c6da..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/DeletionEvent.java +++ /dev/null @@ -1,68 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.NIP09Event; -import nostr.event.tag.EventTag; - -import java.util.List; - -/** - * @author squirrel - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Event Deletion", nip = 9) -@NoArgsConstructor -public class DeletionEvent extends NIP09Event { - - public DeletionEvent(PublicKey pubKey, List tags, String content) { - super(pubKey, Kind.DELETION, tags, content); - } - - public DeletionEvent(PublicKey pubKey, List tags) { - this(pubKey, tags, "Deletion request"); - } - - @Override - protected void validateTags() { - super.validateTags(); - - // Validate `tags` field for at least one `EventTag` or `AuthorTag` - if (this.getTags() == null || this.getTags().isEmpty()) { - throw new AssertionError("Invalid `tags`: Must include at least one `e` or `a` tag."); - } - - boolean hasEventOrAuthorTag = - !nostr.event.filter.Filterable.getTypeSpecificTags(EventTag.class, this).isEmpty() - || nostr.event.filter.Filterable - .firstTagOfTypeWithCode(nostr.event.tag.AddressTag.class, "a", this) - .isPresent(); - if (!hasEventOrAuthorTag) { - throw new AssertionError("Invalid `tags`: Must include at least one `e` or `a` tag."); - } - - // Validate `tags` field for `KindTag` (`k` tag) - boolean hasKindTag = - nostr.event.filter.Filterable - .firstTagOfTypeWithCode(nostr.event.tag.GenericTag.class, "k", this) - .isPresent(); - if (!hasKindTag) { - throw new AssertionError( - "Invalid `tags`: Should include a `k` tag for the kind of each event being requested for" - + " deletion."); - } - } - - @Override - protected void validateKind() { - if (getKind() != Kind.DELETION.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.DELETION.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/DirectMessageEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/DirectMessageEvent.java deleted file mode 100644 index 3841fa6f9..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/DirectMessageEvent.java +++ /dev/null @@ -1,52 +0,0 @@ -package nostr.event.impl; - -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.NIP04Event; -import nostr.event.tag.PubKeyTag; - -import java.util.List; - -/** - * @author squirrel - */ -@NoArgsConstructor -@Event(name = "Encrypted Direct Message", nip = 4) -public class DirectMessageEvent extends NIP04Event { - - public DirectMessageEvent(PublicKey sender, List tags, String content) { - super(sender, Kind.ENCRYPTED_DIRECT_MESSAGE, tags, content); - } - - public DirectMessageEvent( - @NonNull PublicKey sender, @NonNull PublicKey recipient, @NonNull String content) { - super(sender, Kind.ENCRYPTED_DIRECT_MESSAGE); - this.setContent(content); - this.addTag(PubKeyTag.builder().publicKey(recipient).build()); - } - - @Override - protected void validateTags() { - - super.validateTags(); - - // Validate `tags` field for recipient's public key - boolean hasRecipientTag = - !nostr.event.filter.Filterable.getTypeSpecificTags(PubKeyTag.class, this).isEmpty(); - if (!hasRecipientTag) { - throw new AssertionError("Invalid `tags`: Must include a PubKeyTag for the recipient."); - } - } - - @Override - protected void validateKind() { - if (getKind() != Kind.ENCRYPTED_DIRECT_MESSAGE.getValue()) { - throw new AssertionError( - "Invalid kind value. Expected " + Kind.ENCRYPTED_DIRECT_MESSAGE.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/EphemeralEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/EphemeralEvent.java deleted file mode 100644 index 4f424889c..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/EphemeralEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.NIP01Event; - -import java.util.List; - -/** - * @author squirrel - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Ephemeral Events") -@NoArgsConstructor -public class EphemeralEvent extends NIP01Event { - - public EphemeralEvent(PublicKey pubKey, Integer kind, List tags, String content) { - super(pubKey, kind, tags, content); - } - - public EphemeralEvent(PublicKey pubKey, Kind kind, List tags, String content) { - super(pubKey, kind, tags, content); - } - - @Override - public void validateKind() { - var n = getKind(); - if (20_000 <= n && n < 30_000) return; - - throw new AssertionError("Invalid kind value. Must be between 20000 and 30000.", null); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/GenericEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/GenericEvent.java index 74ae092d2..7962ddb9a 100644 --- a/nostr-java-event/src/main/java/nostr/event/impl/GenericEvent.java +++ b/nostr-java-event/src/main/java/nostr/event/impl/GenericEvent.java @@ -8,26 +8,20 @@ import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import nostr.base.ISignable; -import nostr.base.ITag; -import nostr.base.Kind; +import nostr.base.Kinds; import nostr.base.PublicKey; import nostr.base.Signature; -import nostr.base.annotation.Key; import nostr.crypto.bech32.Bech32; import nostr.crypto.bech32.Bech32Prefix; -import nostr.event.BaseEvent; import nostr.event.BaseTag; -import nostr.event.Deleteable; import nostr.event.json.deserializer.PublicKeyDeserializer; import nostr.event.json.deserializer.SignatureDeserializer; import nostr.event.serializer.EventSerializer; -import nostr.event.util.EventTypeChecker; import nostr.event.validator.EventValidator; import nostr.util.NostrException; import nostr.util.validator.HexStringValidator; import java.beans.Transient; -import java.lang.reflect.InvocationTargetException; import java.nio.ByteBuffer; import java.time.Instant; import java.util.ArrayList; @@ -40,95 +34,34 @@ /** * Generic implementation of a Nostr event as defined in NIP-01. * - *

    This class represents the fundamental building block of the Nostr protocol. Events are - * immutable records signed with a private key, containing a unique ID, timestamp, kind, - * tags, and content. - * - *

    NIP-01 Event Structure: - *

    {@code
    - * {
    - *   "id": "event_id_hex",        // SHA-256 hash of canonical serialization
    - *   "pubkey": "pubkey_hex",      // Author's public key
    - *   "created_at": 1234567890,    // Unix timestamp
    - *   "kind": 1,                   // Event kind (see Kind enum)
    - *   "tags": [...],               // Array of tags
    - *   "content": "...",            // Event content (text, JSON, etc.)
    - *   "sig": "signature_hex"       // BIP-340 Schnorr signature
    - * }
    - * }
    - * - *

    Event Kinds: - *

      - *
    • Regular events (kind < 10,000): Immutable, stored indefinitely
    • - *
    • Replaceable events (10,000-19,999): Latest event replaces earlier ones
    • - *
    • Ephemeral events (20,000-29,999): Not stored by relays
    • - *
    • Addressable events (30,000-39,999): Replaceable with 'd' tag identifier
    • - *
    - * - *

    Usage Example: - *

    {@code
    - * // Create and sign an event
    - * Identity identity = new Identity(privateKey);
    - * GenericEvent event = GenericEvent.builder()
    - *     .pubKey(identity.getPublicKey())
    - *     .kind(Kind.TEXT_NOTE)
    - *     .content("Hello Nostr!")
    - *     .tags(List.of(new HashtagTag("nostr")))
    - *     .build();
    - *
    - * event.update(); // Compute ID
    - * event.sign(identity.getPrivateKey()); // Sign with private key
    - * event.validate(); // Verify all fields are valid
    - *
    - * // Send to relay
    - * client.send(event, relayUri);
    - * }
    - * - *

    Validation: This class uses a Template Method pattern for validation. - * Subclasses can override {@link #validateKind()}, {@link #validateTags()}, and - * {@link #validateContent()} to add NIP-specific validation while reusing base validation. - * - *

    Serialization: Event serialization is delegated to {@link EventSerializer} - * which produces canonical NIP-01 JSON format for computing event IDs and signatures. - * - *

    Thread Safety: This class is not thread-safe. Create separate instances - * per thread or use external synchronization. - * * @author squirrel - * @see EventValidator - * @see EventSerializer - * @see EventTypeChecker * @see NIP-01 */ @Slf4j @Data -@EqualsAndHashCode(callSuper = false) -public class GenericEvent extends BaseEvent implements ISignable, Deleteable { +@EqualsAndHashCode +public class GenericEvent implements ISignable { - @Key @EqualsAndHashCode.Include private String id; + @EqualsAndHashCode.Include private String id; - @Key - @JsonProperty("pubkey") +@JsonProperty("pubkey") @EqualsAndHashCode.Include @JsonDeserialize(using = PublicKeyDeserializer.class) private PublicKey pubKey; - @Key - @JsonProperty("created_at") +@JsonProperty("created_at") @EqualsAndHashCode.Exclude private Long createdAt; - @Key @EqualsAndHashCode.Exclude private Integer kind; + @EqualsAndHashCode.Exclude private Integer kind; - @Key - @EqualsAndHashCode.Exclude +@EqualsAndHashCode.Exclude @JsonProperty("tags") private List tags; - @Key @EqualsAndHashCode.Exclude private String content; + @EqualsAndHashCode.Exclude private String content; - @Key - @JsonProperty("sig") +@JsonProperty("sig") @EqualsAndHashCode.Exclude @JsonDeserialize(using = SignatureDeserializer.class) private Signature signature; @@ -146,40 +79,19 @@ public GenericEvent(@NonNull String id) { setId(id); } - public GenericEvent(@NonNull PublicKey pubKey, @NonNull Kind kind) { - this(pubKey, kind, new ArrayList<>(), ""); - } - - public GenericEvent(@NonNull PublicKey pubKey, @NonNull Integer kind) { + public GenericEvent(@NonNull PublicKey pubKey, int kind) { this(pubKey, kind, new ArrayList<>(), ""); } - public GenericEvent(@NonNull PublicKey pubKey, @NonNull Kind kind, @NonNull List tags) { - this(pubKey, kind, tags, ""); - } - - public GenericEvent( - @NonNull PublicKey pubKey, - @NonNull Kind kind, - @NonNull List tags, - @NonNull String content) { - this(pubKey, kind.getValue(), tags, content); - } - public GenericEvent( @NonNull PublicKey pubKey, - @NonNull Integer kind, + int kind, @NonNull List tags, @NonNull String content) { this.pubKey = pubKey; - // Accept provided kind value verbatim for custom kinds (e.g., NIP-defined ranges). - // Use the Kind-typed constructor when mapping enum constants to values. this.kind = kind; this.tags = new ArrayList<>(tags); this.content = content; - - // Update parents - updateTagsParents(this.tags); } public void setId(String id) { @@ -187,7 +99,6 @@ public void setId(String id) { this.id = id; } - @Override public String toBech32() { if (!isSigned()) { this.update(); @@ -202,67 +113,27 @@ public String toBech32() { public void setTags(List tags) { this.tags = new ArrayList<>(tags); - - for (BaseTag tag : this.tags) { - tag.setParent(this); - } } public List getTags() { return Collections.unmodifiableList(this.tags); } - /** - * Checks if this event is replaceable per NIP-01. - * - *

    Replaceable events (kind 10,000-19,999) can be superseded by newer events - * with the same kind from the same author. Relays should only keep the most recent one. - * - * @return true if event kind is in replaceable range (10,000-19,999) - * @see EventTypeChecker#isReplaceable(Integer) - */ @Transient public boolean isReplaceable() { - return nostr.event.util.EventTypeChecker.isReplaceable(this.kind); + return Kinds.isReplaceable(this.kind); } - /** - * Checks if this event is ephemeral per NIP-01. - * - *

    Ephemeral events (kind 20,000-29,999) are not stored by relays. They are - * meant for real-time interactions that don't need persistence. - * - * @return true if event kind is in ephemeral range (20,000-29,999) - * @see EventTypeChecker#isEphemeral(Integer) - */ @Transient public boolean isEphemeral() { - return nostr.event.util.EventTypeChecker.isEphemeral(this.kind); + return Kinds.isEphemeral(this.kind); } - /** - * Checks if this event is addressable/parametrized replaceable per NIP-01. - * - *

    Addressable events (kind 30,000-39,999) are replaceable events that include - * a 'd' tag acting as an identifier. They can be queried and replaced using the - * combination of author pubkey, kind, and 'd' tag value. - * - * @return true if event kind is in addressable range (30,000-39,999) - * @see EventTypeChecker#isAddressable(Integer) - */ @Transient public boolean isAddressable() { - return nostr.event.util.EventTypeChecker.isAddressable(this.kind); + return Kinds.isAddressable(this.kind); } - /** - * Adds a tag to this event. - * - *

    The tag will be added to the tags list if it's not already present (checked - * via equals()). The tag's parent will be set to this event. - * - * @param tag the tag to add (null tags are ignored) - */ public void addTag(BaseTag tag) { if (tag == null) { return; @@ -271,30 +142,10 @@ public void addTag(BaseTag tag) { if (tags == null) tags = new ArrayList<>(); if (!tags.contains(tag)) { - tag.setParent(this); tags.add(tag); } } - /** - * Updates the event's timestamp and computes its ID. - * - *

    This method: - *

      - *
    1. Sets {@code created_at} to the current Unix timestamp
    2. - *
    3. Serializes the event to canonical NIP-01 JSON format
    4. - *
    5. Computes the event ID as SHA-256 hash of the serialization
    6. - *
    - * - *

    Important: Call this method before signing the event. The event ID - * is what gets signed, not the individual fields. - * - *

    Thread Safety: This method modifies the event state and is not thread-safe. - * - * @throws RuntimeException if serialization fails (wraps NostrException) - * @see EventSerializer#serializeToBytes - * @see EventSerializer#computeEventId - */ public void update() { try { this.createdAt = Instant.now().getEpochSecond(); @@ -308,7 +159,6 @@ public void update() { } } - // Minimal builder to support tests expecting GenericEvent.builder() public static GenericEventBuilder builder() { return new GenericEventBuilder(); } @@ -316,8 +166,7 @@ public static GenericEventBuilder builder() { public static class GenericEventBuilder { private String id; private PublicKey pubKey; - private Kind kind; - private Integer customKind; + private Integer kind; private List tags = new ArrayList<>(); private String content = ""; private Long createdAt; @@ -326,8 +175,7 @@ public static class GenericEventBuilder { public GenericEventBuilder id(String id) { this.id = id; return this; } public GenericEventBuilder pubKey(PublicKey pubKey) { this.pubKey = pubKey; return this; } - public GenericEventBuilder kind(Kind kind) { this.kind = kind; return this; } - public GenericEventBuilder customKind(Integer customKind) { this.customKind = customKind; return this; } + public GenericEventBuilder kind(int kind) { this.kind = kind; return this; } public GenericEventBuilder tags(List tags) { this.tags = tags; return this; } public GenericEventBuilder content(String content) { this.content = content; return this; } public GenericEventBuilder createdAt(Long createdAt) { this.createdAt = createdAt; return this; } @@ -339,10 +187,10 @@ public GenericEvent build() { if (id != null) event.setId(id); event.setPubKey(pubKey); - if (customKind == null && kind == null) { + if (kind == null) { throw new IllegalArgumentException("A kind value must be provided when building a GenericEvent."); } - event.setKind(customKind != null ? customKind : kind.getValue()); + event.setKind(kind); event.setTags(tags != null ? new ArrayList<>(tags) : new ArrayList<>()); event.setContent(content != null ? content : ""); @@ -353,7 +201,6 @@ public GenericEvent build() { } } - /** Compatibility accessors for previously named serializedEventCache */ @com.fasterxml.jackson.annotation.JsonIgnore public byte[] getSerializedEventCache() { return this.get_serializedEvent(); @@ -369,103 +216,28 @@ public boolean isSigned() { return this.signature != null; } - /** - * Validates all event fields according to NIP-01 specification. - * - *

    This method uses the Template Method pattern. It validates base fields that - * all events must have, then calls protected methods that subclasses can override - * to add NIP-specific validation. - * - *

    Validation Steps: - *

      - *
    1. Validates event ID (64-character hex string)
    2. - *
    3. Validates public key (64-character hex string)
    4. - *
    5. Validates signature (128-character hex string)
    6. - *
    7. Validates created_at (non-negative Unix timestamp)
    8. - *
    9. Calls {@link #validateKind()} (can be overridden)
    10. - *
    11. Calls {@link #validateTags()} (can be overridden)
    12. - *
    13. Calls {@link #validateContent()} (can be overridden)
    14. - *
    - * - *

    Usage Example: - *

    {@code
    -   * GenericEvent event = createAndSignEvent();
    -   * try {
    -   *     event.validate();
    -   *     // Event is valid, safe to send to relay
    -   * } catch (AssertionError e) {
    -   *     // Event is invalid, fix before sending
    -   *     log.error("Invalid event: {}", e.getMessage());
    -   * }
    -   * }
    - * - * @throws AssertionError if any field fails validation - * @throws NullPointerException if required fields are null - * @see EventValidator - */ public void validate() { - // Validate base fields EventValidator.validateId(this.id); EventValidator.validatePubKey(this.pubKey); EventValidator.validateSignature(this.signature); EventValidator.validateCreatedAt(this.createdAt); - - // Call protected methods that can be overridden by subclasses validateKind(); validateTags(); validateContent(); } - /** - * Validates the event kind. - * - *

    Subclasses can override this method to add kind-specific validation. - * The default implementation validates that kind is non-negative. - * - * @throws AssertionError if kind is invalid - */ protected void validateKind() { EventValidator.validateKind(this.kind); } - /** - * Validates the event tags. - * - *

    Subclasses can override this method to add NIP-specific tag validation. - * For example, ZapRequestEvent requires 'amount' and 'relays' tags. - * - *

    Example Override: - *

    {@code
    -   * @Override
    -   * protected void validateTags() {
    -   *     super.validateTags(); // Call base validation first
    -   *     requireTag("amount");  // NIP-specific requirement
    -   * }
    -   * }
    - * - * @throws AssertionError if tags are invalid - */ protected void validateTags() { EventValidator.validateTags(this.tags); } - /** - * Validates the event content. - * - *

    Subclasses can override this method to add content-specific validation. - * The default implementation validates that content is non-null. - * - * @throws AssertionError if content is invalid - */ protected void validateContent() { EventValidator.validateContent(this.content); } - private String serialize() throws NostrException { - return nostr.event.serializer.EventSerializer.serialize( - this.pubKey, this.createdAt, this.kind, this.tags, this.content); - } - @Transient @Override public Consumer getSignatureConsumer() { @@ -482,30 +254,6 @@ public Supplier getByteArraySupplier() { return () -> ByteBuffer.wrap(this.get_serializedEvent()); } - protected final void updateTagsParents(List tagList) { - if (tagList != null && !tagList.isEmpty()) { - for (ITag t : tagList) { - t.setParent(this); - } - } - } - - protected void addStandardTag(List tag) { - Optional.ofNullable(tag).ifPresent(tagList -> tagList.forEach(this::addStandardTag)); - } - - protected void addStandardTag(BaseTag tag) { - Optional.ofNullable(tag).ifPresent(this::addTag); - } - - protected void addGenericTag(String key, String nip, Object value) { - Optional.ofNullable(value).ifPresent(s -> addTag(BaseTag.create(key, s.toString()))); - } - - protected void addStringListTag(String label, String nip, List tag) { - Optional.ofNullable(tag).ifPresent(tagList -> BaseTag.create(label, tagList)); - } - protected BaseTag getTag(@NonNull String code) { return getTags().stream().filter(tag -> code.equals(tag.getCode())).findFirst().orElseThrow(); } @@ -514,56 +262,10 @@ protected List getTags(@NonNull String code) { return getTags().stream().filter(tag -> code.equals(tag.getCode())).toList(); } - /** - * Ensure that a tag with the provided code exists. - * - * @param code the tag code to search for - * @return the first matching tag - * @throws AssertionError if no tag with the given code is present - */ protected BaseTag requireTag(@NonNull String code) { return getTags().stream() .filter(tag -> code.equals(tag.getCode())) .findFirst() .orElseThrow(() -> new AssertionError("Missing required `" + code + "` tag.")); } - - /** - * Ensure that at least one tag instance of the provided class exists. - * - * @param clazz the tag class to search for - * @param tag type - * @return the first matching tag instance - * @throws AssertionError if no matching tag is present - */ - protected T requireTagInstance(@NonNull Class clazz) { - return getTags().stream() - .filter(clazz::isInstance) - .map(clazz::cast) - .findFirst() - .orElseThrow( - () -> new AssertionError("Missing required `" + clazz.getSimpleName() + "` tag.")); - } - - public static T convert( - @NonNull GenericEvent genericEvent, @NonNull Class clazz) throws NostrException { - try { - T event = clazz.getConstructor().newInstance(); - event.setContent(genericEvent.getContent()); - event.setTags(genericEvent.getTags()); - event.setPubKey(genericEvent.getPubKey()); - event.setId(genericEvent.getId()); - event.set_serializedEvent(genericEvent.get_serializedEvent()); - event.setNip(genericEvent.getNip()); - event.setKind(genericEvent.getKind()); - event.setSignature(genericEvent.getSignature()); - event.setCreatedAt(genericEvent.getCreatedAt()); - return event; - } catch (InstantiationException - | IllegalAccessException - | InvocationTargetException - | NoSuchMethodException e) { - throw new NostrException("Failed to convert GenericEvent", e); - } - } } diff --git a/nostr-java-event/src/main/java/nostr/event/impl/HideMessageEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/HideMessageEvent.java deleted file mode 100644 index b6b7a1d5c..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/HideMessageEvent.java +++ /dev/null @@ -1,47 +0,0 @@ -package nostr.event.impl; - -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.tag.EventTag; - -import java.util.List; - -/** - * @author guilhermegps - */ -@Event(name = "Hide Message on Channel", nip = 28) -@NoArgsConstructor -public class HideMessageEvent extends GenericEvent { - - public HideMessageEvent(PublicKey pubKey, List tags, String content) { - super(pubKey, Kind.HIDE_MESSAGE, tags, content); - } - - public String getHiddenMessageEventId() { - EventTag eventTag = - nostr.event.filter.Filterable.requireTagOfType( - EventTag.class, this, "Missing or invalid `e` root tag."); - return eventTag.getIdEvent(); - } - - @Override - protected void validateTags() { - super.validateTags(); - - // Validate `tags` field for at least one `e` tag - boolean hasEventTag = this.getTags().stream().anyMatch(tag -> tag instanceof EventTag); - if (!hasEventTag) { - throw new AssertionError("Invalid `tags`: Must include at least one `e` tag."); - } - } - - @Override - protected void validateKind() { - if (getKind() != Kind.HIDE_MESSAGE.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.HIDE_MESSAGE.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/InternetIdentifierMetadataEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/InternetIdentifierMetadataEvent.java deleted file mode 100644 index 79fa439dd..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/InternetIdentifierMetadataEvent.java +++ /dev/null @@ -1,72 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.base.json.EventJsonMapper; -import nostr.event.NIP05Event; -import nostr.event.entities.UserProfile; -import nostr.event.json.codec.EventEncodingException; - -/** - * @author squirrel - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Internet Identifier Metadata Event", nip = 5) -@NoArgsConstructor -public final class InternetIdentifierMetadataEvent extends NIP05Event { - - private static final String NAME_PATTERN = "\\w[\\w\\-]+\\w"; - - public InternetIdentifierMetadataEvent(PublicKey pubKey, String content) { - super(pubKey, Kind.SET_METADATA); - this.setContent(content); - } - - public UserProfile getProfile() { - String content = getContent(); - try { - return EventJsonMapper.mapper().readValue(content, UserProfile.class); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to parse user profile content", ex); - } - } - - @Override - protected void validateContent() { - super.validateContent(); - - // Parse and validate the JSON content - UserProfile profile = getProfile(); - - // Validate required fields in the profile - if (profile.getNip05() == null || profile.getNip05().isEmpty()) { - throw new AssertionError("Invalid `content`: `nip05` field must not be null or empty."); - } - - boolean valid = true; - var strNameArr = profile.getNip05().split("@"); - if (strNameArr.length == 2) { - var localPart = strNameArr[0]; - valid = localPart.matches(NAME_PATTERN); - } - if (!valid) { - throw new AssertionError("Invalid profile name: " + profile, null); - } - - // Validate the NIP-05 identifier - // NOTE: This is now up to the client to perform this validation - } - - @Override - protected void validateKind() { - if (getKind() != Kind.SET_METADATA.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.SET_METADATA.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/MentionsEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/MentionsEvent.java deleted file mode 100644 index 011e9ecda..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/MentionsEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.tag.PubKeyTag; - -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * @author squirrel - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Handling Mentions", nip = 8) -@NoArgsConstructor -public final class MentionsEvent extends GenericEvent { - - public MentionsEvent(PublicKey pubKey, Integer kind, List tags, String content) { - super(pubKey, kind, tags, content); - } - - @Override - public void update() { - AtomicInteger counter = new AtomicInteger(0); - - // Replace mentioned pubkeys with positional references, only iterating PubKeyTag entries - nostr.event.filter.Filterable.getTypeSpecificTags(PubKeyTag.class, this) - .forEach( - tag -> { - String replacement = "#[" + counter.getAndIncrement() + "]"; - setContent(this.getContent().replace(tag.getPublicKey().toString(), replacement)); - }); - - super.update(); - } - - @Override - protected void validateTags() { - super.validateTags(); - - // Validate `tags` field for at least one PubKeyTag - boolean hasValidPubKeyTag = - !nostr.event.filter.Filterable.getTypeSpecificTags(PubKeyTag.class, this).isEmpty(); - if (!hasValidPubKeyTag) { - throw new AssertionError("Invalid `tags`: Must include at least one valid PubKeyTag."); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/MerchantEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/MerchantEvent.java deleted file mode 100644 index 2dc18d6cd..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/MerchantEvent.java +++ /dev/null @@ -1,60 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.BaseTag; -import nostr.event.entities.NIP15Content; -import nostr.event.tag.IdentifierTag; - -import java.util.List; - -@Data -@EqualsAndHashCode(callSuper = false) -@NoArgsConstructor -public abstract class MerchantEvent - extends AddressableEvent { - - public MerchantEvent(PublicKey sender, Kind kind, List tags, String content) { - this(sender, kind.getValue(), tags, content); - } - - public MerchantEvent(PublicKey sender, Integer kind, List tags, String content) { - super(sender, kind, tags, content); - } - - protected abstract T getEntity(); - - @Override - protected void validateTags() { - super.validateTags(); - - // Check 'd' tag - IdentifierTag idTag = - nostr.event.filter.Filterable.requireTagOfTypeWithCode(IdentifierTag.class, "d", this); - String id = idTag.getUuid(); - String entityId = getEntity().getId(); - if (!id.equals(entityId)) { - throw new AssertionError("The d-tag value MUST be the same as the stall id."); - } - } - - @Override - protected void validateContent() { - super.validateContent(); - - try { - T entity = getEntity(); - if (entity == null) { - throw new AssertionError("Invalid `content`: Unable to parse merchant entity."); - } - if (entity.getId() == null || entity.getId().isEmpty()) { - throw new AssertionError("Invalid `content`: `id` field is required."); - } - } catch (Exception e) { - throw new AssertionError("Invalid `content`: Must be a valid JSON object.", e); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/MerchantRequestPaymentEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/MerchantRequestPaymentEvent.java deleted file mode 100644 index 2459a8b38..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/MerchantRequestPaymentEvent.java +++ /dev/null @@ -1,45 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.base.json.EventJsonMapper; -import nostr.event.BaseTag; -import nostr.event.entities.PaymentRequest; - -import java.util.List; - -/** - * @author eric - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Merchant Request Payment Event", nip = 15) -@NoArgsConstructor -public class MerchantRequestPaymentEvent extends CheckoutEvent { - - public MerchantRequestPaymentEvent( - PublicKey sender, List tags, @NonNull String content) { - super(sender, tags, content, MessageType.PAYMENT_REQUEST); - } - - public PaymentRequest getPaymentRequest() { - return EventJsonMapper.mapper().convertValue(getContent(), PaymentRequest.class); - } - - protected PaymentRequest getEntity() { - return getPaymentRequest(); - } - - @Override - public void validateKind() { - if (getKind() != Kind.ENCRYPTED_DIRECT_MESSAGE.getValue()) { - throw new AssertionError( - "Invalid kind value. Expected " + Kind.ENCRYPTED_DIRECT_MESSAGE.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/MuteUserEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/MuteUserEvent.java deleted file mode 100644 index c5073c6c0..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/MuteUserEvent.java +++ /dev/null @@ -1,40 +0,0 @@ -package nostr.event.impl; - -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.tag.PubKeyTag; - -import java.util.List; - -/** - * @author guilhermegps - */ -@Event(name = "Mute User on Channel", nip = 28) -@NoArgsConstructor -public class MuteUserEvent extends GenericEvent { - - public MuteUserEvent(PublicKey pubKey, List baseTagList, String content) { - super(pubKey, Kind.MUTE_USER, baseTagList, content); - } - - public PublicKey getMutedUser() { - return requireTagInstance(PubKeyTag.class).getPublicKey(); - } - - @Override - protected void validateTags() { - super.validateTags(); - - requireTagInstance(PubKeyTag.class); - } - - @Override - protected void validateKind() { - if (getKind() != Kind.MUTE_USER.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.MUTE_USER.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/NostrConnectEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/NostrConnectEvent.java deleted file mode 100644 index d1eb64ca2..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/NostrConnectEvent.java +++ /dev/null @@ -1,20 +0,0 @@ -package nostr.event.impl; - -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.tag.PubKeyTag; - -import java.util.ArrayList; - -@EqualsAndHashCode(callSuper = false) -@Event(name = "Nostr Connect", nip = 46) -public class NostrConnectEvent extends EphemeralEvent { - - public NostrConnectEvent( - @NonNull PublicKey sender, @NonNull String content, @NonNull PublicKey recipient) { - super(sender, 24133, new ArrayList<>(), content); - this.addTag(PubKeyTag.builder().publicKey(recipient).build()); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/NostrConnectRequestEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/NostrConnectRequestEvent.java deleted file mode 100644 index a0d624c47..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/NostrConnectRequestEvent.java +++ /dev/null @@ -1,23 +0,0 @@ -package nostr.event.impl; - -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; - -import java.util.List; - -@EqualsAndHashCode(callSuper = false) -@Event(name = "Nostr Connect", nip = 46) -@NoArgsConstructor -public class NostrConnectRequestEvent extends AbstractBaseNostrConnectEvent { - - public NostrConnectRequestEvent(PublicKey pubKey, List baseTagList, String content) { - super(pubKey, baseTagList, content); - } - - public PublicKey getSigner() { - return getActor(); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/NostrConnectResponseEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/NostrConnectResponseEvent.java deleted file mode 100644 index 0a157d89f..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/NostrConnectResponseEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package nostr.event.impl; - -import lombok.NoArgsConstructor; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; - -import java.util.List; - -@Event(name = "Nostr Connect", nip = 46) -@NoArgsConstructor -public class NostrConnectResponseEvent extends AbstractBaseNostrConnectEvent { - - public NostrConnectResponseEvent(PublicKey pubKey, List baseTagList, String content) { - super(pubKey, baseTagList, content); - } - - public PublicKey getApp() { - return getActor(); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/NostrMarketplaceEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/NostrMarketplaceEvent.java deleted file mode 100644 index 45f202877..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/NostrMarketplaceEvent.java +++ /dev/null @@ -1,47 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.base.json.EventJsonMapper; -import nostr.event.BaseTag; -import nostr.event.entities.Product; -import nostr.event.json.codec.EventEncodingException; - -import java.util.List; - -/** - * @author eric - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "", nip = 15) -@NoArgsConstructor -public abstract class NostrMarketplaceEvent extends AddressableEvent { - - /** - * Creates a new marketplace event. - * - *

    Note: Kind values for marketplace events are defined in NIP-15. - * Consider using {@link nostr.base.Kind} enum values when available. - * - * @param sender the public key of the event creator - * @param kind the event kind (see NIP-15 for marketplace event kinds) - * @param tags the event tags - * @param content the event content (typically JSON-encoded Product) - */ - public NostrMarketplaceEvent(PublicKey sender, Integer kind, List tags, String content) { - super(sender, kind, tags, content); - } - - public Product getProduct() { - try { - return EventJsonMapper.mapper().readValue(getContent(), Product.class); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to parse marketplace product content", ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/NutZapEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/NutZapEvent.java deleted file mode 100644 index ea28de8f3..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/NutZapEvent.java +++ /dev/null @@ -1,106 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.base.json.EventJsonMapper; -import nostr.event.BaseTag; -import nostr.event.entities.CashuMint; -import nostr.event.entities.CashuProof; -import nostr.event.entities.NutZap; -import nostr.event.tag.EventTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PubKeyTag; - -import java.util.List; - -@EqualsAndHashCode(callSuper = true) -@Event(name = "Nut Zap Event", nip = 61) -@Data -public class NutZapEvent extends GenericEvent { - - public NutZapEvent(PublicKey sender, List tags, String content) { - super(sender, Kind.NUTZAP, tags, content); - } - - public NutZap getNutZap() { - - NutZap nutZap = new NutZap(); - - EventTag zappedEvent = - nostr.event.filter.Filterable.getTypeSpecificTags(EventTag.class, this).stream() - .findFirst() - .orElse(null); - - List proofs = - nostr.event.filter.Filterable.getTypeSpecificTags(GenericTag.class, this).stream() - .filter(tag -> "proof".equals(tag.getCode())) - .toList(); - - PubKeyTag recipientTag = - nostr.event.filter.Filterable.getTypeSpecificTags(PubKeyTag.class, this).stream() - .findFirst() - .orElseThrow(() -> new IllegalStateException("No PubKeyTag found in tags")); - - GenericTag mintTag = - nostr.event.filter.Filterable.getTypeSpecificTags(GenericTag.class, this).stream() - .filter(tag -> "u".equals(tag.getCode())) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No mint tag found in tags")); - - nutZap.setMint(getMintFromTag(mintTag)); - proofs.forEach( - proofTag -> { - CashuProof cashuProof = getProofFromTag(proofTag); - nutZap.addProof(cashuProof); - }); - nutZap.setRecipient(recipientTag.getPublicKey()); - nutZap.setNutZappedEvent(zappedEvent); - - return nutZap; - } - - protected void validateTags() { - super.validateTags(); - - // Validate `tags` field for the mint tag - boolean hasMintTag = this.getTags().stream().anyMatch(tag -> "u".equals(tag.getCode())); - if (!hasMintTag) { - throw new AssertionError("Invalid `tags`: Must include a mint tag with code 'u'."); - } - - // Validate `tags` field for at exactly one PubKeyTag - boolean hasValidPubKeyTag = this.getTags().stream().anyMatch(tag -> tag instanceof PubKeyTag); - if (!hasValidPubKeyTag) { - throw new AssertionError("Invalid `tags`: Must include one valid PubKeyTag."); - } - - // Validate `tags` field for at least one Proof tag - boolean hasProofTag = this.getTags().stream().anyMatch(tag -> "proof".equals(tag.getCode())); - if (!hasProofTag) { - throw new AssertionError( - "Invalid `tags`: Must include at least one Proof tag with code 'proof'."); - } - } - - @Override - protected void validateKind() { - if (getKind() != Kind.NUTZAP.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.NUTZAP.getValue()); - } - } - - private CashuMint getMintFromTag(GenericTag mintTag) { - String url = mintTag.getAttributes().get(0).value().toString(); - CashuMint mint = new CashuMint(url); - return mint; - } - - private CashuProof getProofFromTag(GenericTag proofTag) { - String proof = proofTag.getAttributes().get(0).value().toString(); - CashuProof cashuProof = EventJsonMapper.mapper().convertValue(proof, CashuProof.class); - return cashuProof; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/NutZapInformationalEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/NutZapInformationalEvent.java deleted file mode 100644 index 64e114d9c..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/NutZapInformationalEvent.java +++ /dev/null @@ -1,96 +0,0 @@ -package nostr.event.impl; - -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.entities.CashuMint; -import nostr.event.entities.NutZapInformation; -import nostr.event.tag.GenericTag; - -import java.util.List; - -@Event(name = "Nut Zap Informational Event", nip = 61) -public class NutZapInformationalEvent extends ReplaceableEvent { - - public NutZapInformationalEvent(PublicKey pubKey, List tags, String content) { - super(pubKey, Kind.NUTZAP_INFORMATIONAL.getValue(), tags, content); - } - - public NutZapInformation getNutZapInformation() { - NutZapInformation nutZapInformation = new NutZapInformation(); - - List relayTags = - nostr.event.filter.Filterable.getTypeSpecificTags(GenericTag.class, this).stream() - .filter(tag -> "relay".equals(tag.getCode())) - .toList(); - - List mintTags = - nostr.event.filter.Filterable.getTypeSpecificTags(GenericTag.class, this).stream() - .filter(tag -> "u".equals(tag.getCode())) - .toList(); - - GenericTag p2pkTag = - nostr.event.filter.Filterable.getTypeSpecificTags(GenericTag.class, this).stream() - .filter(tag -> "pubkey".equals(tag.getCode())) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No p2pk tag found in tags")); - - nutZapInformation.setRelays(relayTags.stream().map(this::getRelayFromTag).toList()); - nutZapInformation.setMints(mintTags.stream().map(this::getMintFromTag).toList()); - nutZapInformation.setP2pkPubkey(p2pkTag.getAttributes().get(0).value().toString()); - - return nutZapInformation; - } - - @Override - protected void validateTags() { - super.validateTags(); - - // At least one relay - boolean hasValidRelayTag = - this.getTags().stream() - .anyMatch(tag -> tag instanceof GenericTag && "relay".equals(tag.getCode())); - if (!hasValidRelayTag) { - throw new AssertionError("Invalid `tags`: Must include at least one valid relay tag."); - } - - // At least one mint tag - boolean hasValidMintTag = - this.getTags().stream() - .anyMatch(tag -> tag instanceof GenericTag && "u".equals(tag.getCode())); - if (!hasValidMintTag) { - throw new AssertionError( - "Invalid `tags`: Must include at least one valid mint tag with code 'u'."); - } - - // One pubkey tag - boolean hasValidPubKeyTag = - this.getTags().stream() - .anyMatch(tag -> tag instanceof GenericTag && "pubkey".equals(tag.getCode())); - if (!hasValidPubKeyTag) { - throw new AssertionError( - "Invalid `tags`: Must include exactly one pubkey tag with code 'pubkey'."); - } - } - - @Override - protected void validateKind() { - if (getKind() != Kind.NUTZAP_INFORMATIONAL.getValue()) { - throw new AssertionError( - "Invalid kind value. Expected " + Kind.NUTZAP_INFORMATIONAL.getValue()); - } - } - - private Relay getRelayFromTag(@NonNull GenericTag tag) { - String url = tag.getAttributes().get(0).value().toString(); - return new Relay(url); - } - - private CashuMint getMintFromTag(@NonNull GenericTag tag) { - String mintUrl = tag.getAttributes().get(0).value().toString(); - return new CashuMint(mintUrl); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/OtsEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/OtsEvent.java deleted file mode 100644 index 14ea39b15..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/OtsEvent.java +++ /dev/null @@ -1,29 +0,0 @@ -package nostr.event.impl; - -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; - -import java.util.List; - -/** - * @author squirrel - */ -@Event(name = "OpenTimestamps Attestations for Events") -@NoArgsConstructor -public class OtsEvent extends GenericEvent { - - public OtsEvent(@NonNull PublicKey pubKey, @NonNull List tags, @NonNull String content) { - super(pubKey, Kind.OTS_EVENT, tags, content); - } - - @Override - protected void validateKind() { - if (getKind() != Kind.OTS_EVENT.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.OTS_EVENT.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/ReactionEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/ReactionEvent.java deleted file mode 100644 index 22c10c3ce..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/ReactionEvent.java +++ /dev/null @@ -1,42 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.NIP25Event; -import nostr.event.tag.EventTag; - -import java.util.List; - -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Reactions", nip = 25) -@NoArgsConstructor -public class ReactionEvent extends NIP25Event { - - public ReactionEvent(PublicKey pubKey, List tags, String content) { - super(pubKey, Kind.REACTION, tags, content); - } - - public String getReactedEventId() { - return requireTagInstance(EventTag.class).getIdEvent(); - } - - @Override - protected void validateTags() { - super.validateTags(); - - requireTagInstance(EventTag.class); - } - - @Override - protected void validateKind() { - if (getKind() != Kind.REACTION.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.REACTION.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/ReplaceableEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/ReplaceableEvent.java deleted file mode 100644 index 633ee3db3..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/ReplaceableEvent.java +++ /dev/null @@ -1,46 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.Kind; -import nostr.base.NipConstants; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.NIP01Event; - -import java.util.List; - -/** - * @author squirrel - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Replaceable Events") -@NoArgsConstructor -public class ReplaceableEvent extends NIP01Event { - - public ReplaceableEvent(PublicKey sender, Integer kind, List tags, String content) { - super(sender, kind, tags, content); - } - - @Override - protected void validateKind() { - var n = getKind(); - if ((NipConstants.REPLACEABLE_KIND_MIN <= n && n < NipConstants.REPLACEABLE_KIND_MAX) - || n == Kind.SET_METADATA.getValue() - || n == Kind.CONTACT_LIST.getValue()) { - return; - } - - throw new AssertionError( - "Invalid kind value. Must be between %d and %d or equal %d or %d" - .formatted( - NipConstants.REPLACEABLE_KIND_MIN, - NipConstants.REPLACEABLE_KIND_MAX, - Kind.SET_METADATA.getValue(), - Kind.CONTACT_LIST.getValue()), - null); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/TextNoteEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/TextNoteEvent.java deleted file mode 100644 index afe89676f..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/TextNoteEvent.java +++ /dev/null @@ -1,42 +0,0 @@ -package nostr.event.impl; - -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.NIP01Event; -import nostr.event.tag.PubKeyTag; - -import java.util.List; - -/** - * @author squirrel - */ -@Event(name = "Text Note") -@NoArgsConstructor -public class TextNoteEvent extends NIP01Event { - - public TextNoteEvent( - @NonNull PublicKey pubKey, @NonNull List tags, @NonNull String content) { - super(pubKey, Kind.TEXT_NOTE, tags, content); - } - - public List getRecipientPubkeyTags() { - return nostr.event.filter.Filterable.getTypeSpecificTags(PubKeyTag.class, this); - } - - public List getRecipients() { - return nostr.event.filter.Filterable.getTypeSpecificTags(PubKeyTag.class, this).stream() - .map(PubKeyTag::getPublicKey) - .toList(); - } - - @Override - protected void validateKind() { - if (getKind() != Kind.TEXT_NOTE.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.TEXT_NOTE.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/VerifyPaymentOrShippedEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/VerifyPaymentOrShippedEvent.java deleted file mode 100644 index 9a97338af..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/VerifyPaymentOrShippedEvent.java +++ /dev/null @@ -1,45 +0,0 @@ -package nostr.event.impl; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.base.json.EventJsonMapper; -import nostr.event.BaseTag; -import nostr.event.entities.PaymentShipmentStatus; - -import java.util.List; - -/** - * @author eric - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Event(name = "Verify Payment Or Shipped Event", nip = 15) -@NoArgsConstructor -public class VerifyPaymentOrShippedEvent extends CheckoutEvent { - - public VerifyPaymentOrShippedEvent( - PublicKey sender, List tags, @NonNull String content) { - super(sender, tags, content, MessageType.ORDER_STATUS_UPDATE); - } - - public PaymentShipmentStatus getPaymentShipmentStatus() { - return EventJsonMapper.mapper().convertValue(getContent(), PaymentShipmentStatus.class); - } - - protected PaymentShipmentStatus getEntity() { - return getPaymentShipmentStatus(); - } - - @Override - public void validateKind() { - if (getKind() != Kind.ENCRYPTED_DIRECT_MESSAGE.getValue()) { - throw new AssertionError( - "Invalid kind value. Expected " + Kind.ENCRYPTED_DIRECT_MESSAGE.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/ZapReceiptEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/ZapReceiptEvent.java deleted file mode 100644 index c8e42206e..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/ZapReceiptEvent.java +++ /dev/null @@ -1,105 +0,0 @@ -package nostr.event.impl; - -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.entities.ZapReceipt; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PubKeyTag; - -import java.util.List; - -@EqualsAndHashCode(callSuper = false) -@Event(name = "ZapReceiptEvent", nip = 57) -@NoArgsConstructor -public class ZapReceiptEvent extends GenericEvent { - - public ZapReceiptEvent( - @NonNull PublicKey recipientPubKey, @NonNull List tags, @NonNull String content) { - super(recipientPubKey, Kind.ZAP_RECEIPT, tags, content); - } - - public ZapReceipt getZapReceipt() { - var bolt11 = - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "bolt11", this) - .getAttributes() - .get(0) - .value() - .toString(); - var description = - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "description", this) - .getAttributes() - .get(0) - .value() - .toString(); - var preimage = - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "preimage", this) - .getAttributes() - .get(0) - .value() - .toString(); - - return new ZapReceipt(bolt11, description, preimage); - } - - public String getBolt11() { - ZapReceipt zapReceipt = getZapReceipt(); - return zapReceipt.getBolt11(); - } - - public String getDescriptionSha256() { - ZapReceipt zapReceipt = getZapReceipt(); - return zapReceipt.getDescriptionSha256(); - } - - public String getPreimage() { - ZapReceipt zapReceipt = getZapReceipt(); - return zapReceipt.getPreimage(); - } - - public PublicKey getRecipient() { - PubKeyTag recipientPubKeyTag = - nostr.event.filter.Filterable.requireTagOfTypeWithCode(PubKeyTag.class, "p", this); - return recipientPubKeyTag.getPublicKey(); - } - - public PublicKey getSender() { - return nostr.event.filter.Filterable - .firstTagOfTypeWithCode(PubKeyTag.class, "P", this) - .map(PubKeyTag::getPublicKey) - .orElse(null); - } - - public String getEventId() { - return nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GenericTag.class, "e", this) - .map(tag -> tag.getAttributes().get(0).value().toString()) - .orElse(null); - } - - @Override - protected void validateTags() { - super.validateTags(); - - // Validate `tags` field - // Check for required tags - nostr.event.filter.Filterable.requireTagOfTypeWithCode(PubKeyTag.class, "p", this); - nostr.event.filter.Filterable.requireTagOfTypeWithCode(GenericTag.class, "bolt11", this); - nostr.event.filter.Filterable.requireTagOfTypeWithCode(GenericTag.class, "description", this); - nostr.event.filter.Filterable.requireTagOfTypeWithCode(GenericTag.class, "preimage", this); - } - - @Override - protected void validateKind() { - if (getKind() != Kind.ZAP_RECEIPT.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.ZAP_RECEIPT.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/ZapRequestEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/ZapRequestEvent.java deleted file mode 100644 index 45165ad0c..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/ZapRequestEvent.java +++ /dev/null @@ -1,118 +0,0 @@ -package nostr.event.impl; - -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.base.annotation.Event; -import nostr.event.BaseTag; -import nostr.event.entities.ZapRequest; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.RelaysTag; - -import java.util.List; - -@EqualsAndHashCode(callSuper = false) -@Event(name = "ZapRequestEvent", nip = 57) -@NoArgsConstructor -public class ZapRequestEvent extends GenericEvent { - - public ZapRequestEvent( - @NonNull PublicKey recipientPubKey, @NonNull List tags, @NonNull String content) { - super(recipientPubKey, Kind.ZAP_REQUEST, tags, content); - } - - public ZapRequest getZapRequest() { - RelaysTag relaysTag = - nostr.event.filter.Filterable.requireTagOfTypeWithCode(RelaysTag.class, "relays", this); - String amount = - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "amount", this) - .getAttributes() - .get(0) - .value() - .toString(); - String lnurl = - nostr.event.filter.Filterable - .requireTagOfTypeWithCode(GenericTag.class, "lnurl", this) - .getAttributes() - .get(0) - .value() - .toString(); - - return new ZapRequest(relaysTag, Long.parseLong(amount), lnurl); - } - - public PublicKey getRecipientKey() { - PubKeyTag p = - nostr.event.filter.Filterable.requireTagOfTypeWithCode( - PubKeyTag.class, "p", this, "Recipient public key not found in tags"); - return p.getPublicKey(); - } - - public String getEventId() { - return nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GenericTag.class, "e", this) - .map(tag -> tag.getAttributes().get(0).value().toString()) - .orElse(null); - } - - public List getRelays() { - ZapRequest zapRequest = getZapRequest(); - return zapRequest.getRelaysTag() != null ? zapRequest.getRelaysTag().getRelays() : null; - } - - public String getLnUrl() { - ZapRequest zapRequest = getZapRequest(); - return zapRequest.getLnUrl(); - } - - public Long getAmount() { - ZapRequest zapRequest = getZapRequest(); - return zapRequest.getAmount(); - } - - @Override - protected void validateTags() { - super.validateTags(); - - // Validate `tags` field - // Check for required tags - boolean hasRecipientTag = - nostr.event.filter.Filterable - .firstTagOfTypeWithCode(PubKeyTag.class, "p", this) - .isPresent(); - if (!hasRecipientTag) { - throw new AssertionError( - "Invalid `tags`: Must include a `p` tag for the recipient's public key."); - } - - boolean hasAmountTag = - nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GenericTag.class, "amount", this) - .isPresent(); - if (!hasAmountTag) { - throw new AssertionError( - "Invalid `tags`: Must include an `amount` tag specifying the amount in millisatoshis."); - } - - boolean hasLnUrlTag = - nostr.event.filter.Filterable - .firstTagOfTypeWithCode(GenericTag.class, "lnurl", this) - .isPresent(); - if (!hasLnUrlTag) { - throw new AssertionError( - "Invalid `tags`: Must include an `lnurl` tag containing the Lightning Network URL."); - } - } - - @Override - protected void validateKind() { - if (getKind() != Kind.ZAP_REQUEST.getValue()) { - throw new AssertionError("Invalid kind value. Expected " + Kind.ZAP_REQUEST.getValue()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java index 60a0e3211..cb12d0d2a 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java @@ -3,11 +3,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import lombok.Data; import nostr.base.Encoder; -import nostr.event.BaseEvent; +import nostr.event.impl.GenericEvent; import nostr.event.json.EventJsonMapper; @Data -public class BaseEventEncoder implements Encoder { +public class BaseEventEncoder implements Encoder { private final T event; diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java deleted file mode 100644 index 2d4d25507..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java +++ /dev/null @@ -1,39 +0,0 @@ -package nostr.event.json.codec; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.Data; -import nostr.base.IDecoder; -import nostr.event.BaseTag; - -import static nostr.base.json.EventJsonMapper.mapper; - -/** - * @author eric - */ -@Data -public class BaseTagDecoder implements IDecoder { - - private final Class clazz; - - // Generics are erased at runtime; BaseTag.class is the default concrete target for decoding - @SuppressWarnings("unchecked") - public BaseTagDecoder() { - this.clazz = (Class) BaseTag.class; - } - - /** - * Decodes the provided JSON string into a tag instance. - * - * @param jsonString JSON representation of the tag - * @return decoded tag - * @throws nostr.event.json.codec.EventEncodingException if decoding fails - */ - @Override - public T decode(String jsonString) throws EventEncodingException { - try { - return mapper().readValue(jsonString, clazz); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to decode tag", ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagEncoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagEncoder.java index 30a2973b3..a309835e0 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagEncoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagEncoder.java @@ -12,7 +12,7 @@ public record BaseTagEncoder(BaseTag tag) implements Encoder { public static final ObjectMapper BASETAG_ENCODER_MAPPER_BLACKBIRD = EventJsonMapper.getMapper() .copy() - .registerModule(new SimpleModule().addSerializer(new BaseTagSerializer<>())); + .registerModule(new SimpleModule().addSerializer(new BaseTagSerializer())); @Override public String encode() throws nostr.event.json.codec.EventEncodingException { diff --git a/nostr-java-base/src/main/java/nostr/event/json/codec/EventEncodingException.java b/nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java similarity index 100% rename from nostr-java-base/src/main/java/nostr/event/json/codec/EventEncodingException.java rename to nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FilterableProvider.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FilterableProvider.java deleted file mode 100644 index 2e93fc1de..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/FilterableProvider.java +++ /dev/null @@ -1,49 +0,0 @@ -package nostr.event.json.codec; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.NonNull; -import nostr.event.filter.AddressTagFilter; -import nostr.event.filter.AuthorFilter; -import nostr.event.filter.EventFilter; -import nostr.event.filter.Filterable; -import nostr.event.filter.GenericTagQueryFilter; -import nostr.event.filter.GeohashTagFilter; -import nostr.event.filter.HashtagTagFilter; -import nostr.event.filter.IdentifierTagFilter; -import nostr.event.filter.KindFilter; -import nostr.event.filter.ReferencedEventFilter; -import nostr.event.filter.ReferencedPublicKeyFilter; -import nostr.event.filter.SinceFilter; -import nostr.event.filter.UntilFilter; -import nostr.event.filter.VoteTagFilter; - -import java.util.List; -import java.util.function.Function; -import java.util.stream.StreamSupport; - -public class FilterableProvider { - protected static List getFilterFunction( - @NonNull JsonNode node, @NonNull String type) { - return switch (type) { - case ReferencedPublicKeyFilter.FILTER_KEY -> - getFilterable(node, ReferencedPublicKeyFilter.fxn); - case ReferencedEventFilter.FILTER_KEY -> getFilterable(node, ReferencedEventFilter.fxn); - case IdentifierTagFilter.FILTER_KEY -> getFilterable(node, IdentifierTagFilter.fxn); - case AddressTagFilter.FILTER_KEY -> getFilterable(node, AddressTagFilter.fxn); - case GeohashTagFilter.FILTER_KEY -> getFilterable(node, GeohashTagFilter.fxn); - case HashtagTagFilter.FILTER_KEY -> getFilterable(node, HashtagTagFilter.fxn); - case VoteTagFilter.FILTER_KEY -> getFilterable(node, VoteTagFilter.fxn); - case AuthorFilter.FILTER_KEY -> getFilterable(node, AuthorFilter.fxn); - case EventFilter.FILTER_KEY -> getFilterable(node, EventFilter.fxn); - case KindFilter.FILTER_KEY -> getFilterable(node, KindFilter.fxn); - case SinceFilter.FILTER_KEY -> SinceFilter.fxn.apply(node); - case UntilFilter.FILTER_KEY -> UntilFilter.fxn.apply(node); - default -> getFilterable(node, GenericTagQueryFilter.fxn(type)); - }; - } - - private static List getFilterable( - JsonNode jsonNode, Function filterFunction) { - return StreamSupport.stream(jsonNode.spliterator(), false).map(filterFunction).toList(); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java deleted file mode 100644 index 9f3ade744..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java +++ /dev/null @@ -1,49 +0,0 @@ -package nostr.event.json.codec; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Data; -import lombok.NonNull; -import nostr.base.IDecoder; -import nostr.event.filter.Filterable; -import nostr.event.filter.Filters; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static nostr.base.json.EventJsonMapper.mapper; - -/** - * @author eric - */ -@Data -public class FiltersDecoder implements IDecoder { - - /** - * Decodes a JSON string of filters into a {@link Filters} object. - * - * @param jsonFiltersList JSON representation of filters - * @return decoded filters - * @throws nostr.event.json.codec.EventEncodingException if decoding fails - */ - @Override - public Filters decode(@NonNull String jsonFiltersList) throws EventEncodingException { - try { - final List filterables = new ArrayList<>(); - - Map filtersMap = - mapper().readValue( - jsonFiltersList, new TypeReference>() {}); - - for (Map.Entry entry : filtersMap.entrySet()) { - filterables.addAll(FilterableProvider.getFilterFunction(entry.getValue(), entry.getKey())); - } - - return new Filters(filterables); - } catch (JsonProcessingException e) { - throw new EventEncodingException("Failed to decode filters", e); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersEncoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersEncoder.java index 176af9cd8..ccad524d9 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersEncoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersEncoder.java @@ -1,26 +1,12 @@ package nostr.event.json.codec; -import com.fasterxml.jackson.databind.node.ObjectNode; import nostr.base.Encoder; -import nostr.event.filter.Filters; -import nostr.event.json.EventJsonMapper; +import nostr.event.filter.EventFilter; -public record FiltersEncoder(Filters filters) implements Encoder { +public record FiltersEncoder(EventFilter filter) implements Encoder { @Override public String encode() { - ObjectNode root = EventJsonMapper.getMapper().createObjectNode(); - - filters - .getFiltersMap() - .forEach( - (key, filterableList) -> - root.setAll( - filterableList.stream() - .map(filterable -> filterable.toObjectNode(root)) - .toList() - .getFirst())); - - return root.toString(); + return filter.toJson(); } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java deleted file mode 100644 index 496ae9ad7..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java +++ /dev/null @@ -1,41 +0,0 @@ -package nostr.event.json.codec; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.Data; -import nostr.base.IDecoder; -import nostr.event.impl.GenericEvent; - -/** - * @author eric - */ -@Data -public class GenericEventDecoder implements IDecoder { - - private final Class clazz; - - public GenericEventDecoder() { - this.clazz = (Class) GenericEvent.class; - } - - public GenericEventDecoder(Class clazz) { - this.clazz = clazz; - } - - /** - * Decodes a JSON string into a {@link GenericEvent} instance. - * - * @param jsonEvent JSON representation of the event - * @return decoded event - * @throws nostr.event.json.codec.EventEncodingException if decoding fails - */ - @Override - public T decode(String jsonEvent) throws EventEncodingException { - try { - I_DECODER_MAPPER_BLACKBIRD.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); - return I_DECODER_MAPPER_BLACKBIRD.readValue(jsonEvent, clazz); - } catch (JsonProcessingException e) { - throw new EventEncodingException("Failed to decode generic event", e); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java index 0c6c0edfb..b5c754575 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java @@ -4,11 +4,11 @@ import lombok.Data; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import nostr.base.ElementAttribute; import nostr.base.IDecoder; import nostr.event.tag.GenericTag; import java.util.ArrayList; +import java.util.List; @Data @Slf4j @@ -16,7 +16,6 @@ public class GenericTagDecoder implements IDecoder { private final Class clazz; - // Generics are erased at runtime; safe cast because decoder always produces the requested class @SuppressWarnings("unchecked") public GenericTagDecoder() { this((Class) GenericTag.class); @@ -26,27 +25,18 @@ public GenericTagDecoder(@NonNull Class clazz) { this.clazz = clazz; } - /** - * Decodes a JSON array into a {@link GenericTag} instance. - * - * @param json JSON array string representing the tag - * @return decoded tag - * @throws nostr.event.json.codec.EventEncodingException if decoding fails - */ @Override - // Generics are erased at runtime; safe cast because the created GenericTag matches T by contract @SuppressWarnings("unchecked") public T decode(@NonNull String json) throws EventEncodingException { try { String[] jsonElements = I_DECODER_MAPPER_BLACKBIRD.readValue(json, String[].class); - var attributes = new ArrayList(Math.max(0, jsonElements.length - 1)); + List params = new ArrayList<>(Math.max(0, jsonElements.length - 1)); for (int i = 1; i < jsonElements.length; i++) { - ElementAttribute attribute = new ElementAttribute("param" + (i - 1), jsonElements[i]); - if (!attributes.contains(attribute)) { - attributes.add(attribute); + if (!params.contains(jsonElements[i])) { + params.add(jsonElements[i]); } } - GenericTag genericTag = new GenericTag(jsonElements[0], attributes); + GenericTag genericTag = new GenericTag(jsonElements[0], params); log.debug("Decoded GenericTag: {}", genericTag); diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java deleted file mode 100644 index 929d11863..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java +++ /dev/null @@ -1,38 +0,0 @@ -package nostr.event.json.codec; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.Data; -import nostr.base.IDecoder; -import nostr.event.Nip05Content; - -import static nostr.base.json.EventJsonMapper.mapper; - -/** - * @author eric - */ -@Data -public class Nip05ContentDecoder implements IDecoder { - - private final Class clazz; - - @SuppressWarnings("unchecked") - public Nip05ContentDecoder() { - this.clazz = (Class) Nip05Content.class; - } - - /** - * Decodes a JSON representation of NIP-05 content. - * - * @param jsonContent JSON content string - * @return decoded content - * @throws nostr.event.json.codec.EventEncodingException if decoding fails - */ - @Override - public T decode(String jsonContent) throws EventEncodingException { - try { - return mapper().readValue(jsonContent, clazz); - } catch (JsonProcessingException ex) { - throw new EventEncodingException("Failed to decode nip05 content", ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarDateBasedEventDeserializer.java b/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarDateBasedEventDeserializer.java deleted file mode 100644 index cb9f43d92..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarDateBasedEventDeserializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package nostr.event.json.deserializer; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import nostr.base.json.EventJsonMapper; -import nostr.event.impl.CalendarDateBasedEvent; -import nostr.event.impl.GenericEvent; -import nostr.util.NostrException; - -import java.io.IOException; - -public class CalendarDateBasedEventDeserializer extends StdDeserializer { - public CalendarDateBasedEventDeserializer() { - super(CalendarDateBasedEvent.class); - } - - @Override - public CalendarDateBasedEvent deserialize(JsonParser jsonParser, DeserializationContext ctxt) - throws IOException { - JsonNode calendarEventNode = jsonParser.getCodec().readTree(jsonParser); - GenericEvent genericEvent = - EventJsonMapper.mapper().treeToValue(calendarEventNode, GenericEvent.class); - - try { - return GenericEvent.convert(genericEvent, CalendarDateBasedEvent.class); - } catch (NostrException ex) { - throw new IOException("Failed to convert generic event into CalendarDateBasedEvent", ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarEventDeserializer.java b/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarEventDeserializer.java deleted file mode 100644 index 38596488e..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarEventDeserializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package nostr.event.json.deserializer; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import nostr.base.json.EventJsonMapper; -import nostr.event.impl.CalendarEvent; -import nostr.event.impl.GenericEvent; -import nostr.util.NostrException; - -import java.io.IOException; - -public class CalendarEventDeserializer extends StdDeserializer { - public CalendarEventDeserializer() { - super(CalendarEvent.class); - } - - @Override - public CalendarEvent deserialize(JsonParser jsonParser, DeserializationContext ctxt) - throws IOException { - JsonNode calendarEventNode = jsonParser.getCodec().readTree(jsonParser); - GenericEvent genericEvent = - EventJsonMapper.mapper().treeToValue(calendarEventNode, GenericEvent.class); - - try { - return GenericEvent.convert(genericEvent, CalendarEvent.class); - } catch (NostrException ex) { - throw new IOException("Failed to convert generic event into CalendarEvent", ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarRsvpEventDeserializer.java b/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarRsvpEventDeserializer.java deleted file mode 100644 index 3718e8a11..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarRsvpEventDeserializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package nostr.event.json.deserializer; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import nostr.base.json.EventJsonMapper; -import nostr.event.impl.CalendarRsvpEvent; -import nostr.event.impl.GenericEvent; -import nostr.util.NostrException; - -import java.io.IOException; - -public class CalendarRsvpEventDeserializer extends StdDeserializer { - public CalendarRsvpEventDeserializer() { - super(CalendarRsvpEvent.class); - } - - @Override - public CalendarRsvpEvent deserialize(JsonParser jsonParser, DeserializationContext ctxt) - throws IOException { - JsonNode calendarEventNode = jsonParser.getCodec().readTree(jsonParser); - GenericEvent genericEvent = - EventJsonMapper.mapper().treeToValue(calendarEventNode, GenericEvent.class); - - try { - return GenericEvent.convert(genericEvent, CalendarRsvpEvent.class); - } catch (NostrException ex) { - throw new IOException("Failed to convert generic event into CalendarRsvpEvent", ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarTimeBasedEventDeserializer.java b/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarTimeBasedEventDeserializer.java deleted file mode 100644 index c951f74c6..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/deserializer/CalendarTimeBasedEventDeserializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package nostr.event.json.deserializer; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import nostr.base.json.EventJsonMapper; -import nostr.event.impl.CalendarTimeBasedEvent; -import nostr.event.impl.GenericEvent; -import nostr.util.NostrException; - -import java.io.IOException; - -public class CalendarTimeBasedEventDeserializer extends StdDeserializer { - public CalendarTimeBasedEventDeserializer() { - super(CalendarTimeBasedEvent.class); - } - - @Override - public CalendarTimeBasedEvent deserialize(JsonParser jsonParser, DeserializationContext ctxt) - throws IOException { - JsonNode calendarEventNode = jsonParser.getCodec().readTree(jsonParser); - GenericEvent genericEvent = - EventJsonMapper.mapper().treeToValue(calendarEventNode, GenericEvent.class); - - try { - return GenericEvent.convert(genericEvent, CalendarTimeBasedEvent.class); - } catch (NostrException ex) { - throw new IOException("Failed to convert generic event into CalendarTimeBasedEvent", ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/deserializer/ClassifiedListingEventDeserializer.java b/nostr-java-event/src/main/java/nostr/event/json/deserializer/ClassifiedListingEventDeserializer.java deleted file mode 100644 index 377137e9f..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/deserializer/ClassifiedListingEventDeserializer.java +++ /dev/null @@ -1,56 +0,0 @@ -package nostr.event.json.deserializer; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.node.ArrayNode; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.base.json.EventJsonMapper; -import nostr.event.BaseTag; -import nostr.event.impl.ClassifiedListingEvent; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.StreamSupport; - -public class ClassifiedListingEventDeserializer extends StdDeserializer { - public ClassifiedListingEventDeserializer() { - super(ClassifiedListingEvent.class); - } - - @Override - public ClassifiedListingEvent deserialize(JsonParser jsonParser, DeserializationContext ctxt) - throws IOException { - JsonNode classifiedListingEventNode = jsonParser.getCodec().readTree(jsonParser); - ArrayNode tags = (ArrayNode) classifiedListingEventNode.get("tags"); - - List baseTags = - StreamSupport.stream(tags.spliterator(), false).toList().stream() - .map(JsonNode::elements) - .map(element -> EventJsonMapper.mapper().convertValue(element, BaseTag.class)) - .toList(); - Map generalMap = new HashMap<>(); - var fieldNames = classifiedListingEventNode.fieldNames(); - while (fieldNames.hasNext()) { - String key = fieldNames.next(); - generalMap.put(key, classifiedListingEventNode.get(key).asText()); - } - - ClassifiedListingEvent classifiedListingEvent = - new ClassifiedListingEvent( - new PublicKey(generalMap.get("pubkey")), - Kind.valueOfStrict(Integer.parseInt(generalMap.get("kind"))), - baseTags, - generalMap.get("content")); - classifiedListingEvent.setId(generalMap.get("id")); - classifiedListingEvent.setCreatedAt(Long.valueOf(generalMap.get("created_at"))); - classifiedListingEvent.setSignature(Signature.fromString(generalMap.get("sig"))); - - return classifiedListingEvent; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/deserializer/TagDeserializer.java b/nostr-java-event/src/main/java/nostr/event/json/deserializer/TagDeserializer.java index a23f10a14..63a61e24e 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/deserializer/TagDeserializer.java +++ b/nostr-java-event/src/main/java/nostr/event/json/deserializer/TagDeserializer.java @@ -6,63 +6,21 @@ import com.fasterxml.jackson.databind.JsonNode; import nostr.event.BaseTag; import nostr.event.json.codec.GenericTagDecoder; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EmojiTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.ExpirationTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.LabelNamespaceTag; -import nostr.event.tag.LabelTag; -import nostr.event.tag.NonceTag; -import nostr.event.tag.PriceTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.ReferenceTag; -import nostr.event.tag.RelaysTag; -import nostr.event.tag.SubjectTag; -import nostr.event.tag.UrlTag; -import nostr.event.tag.VoteTag; import java.io.IOException; -import java.util.Map; -import java.util.function.Function; public class TagDeserializer extends JsonDeserializer { - private static final Map> TAG_DECODERS = - Map.ofEntries( - Map.entry("a", AddressTag::deserialize), - Map.entry("d", IdentifierTag::deserialize), - Map.entry("e", EventTag::deserialize), - Map.entry("g", GeohashTag::deserialize), - Map.entry("l", LabelTag::deserialize), - Map.entry("L", LabelNamespaceTag::deserialize), - Map.entry("p", PubKeyTag::deserialize), - Map.entry("r", ReferenceTag::deserialize), - Map.entry("t", HashtagTag::deserialize), - Map.entry("u", UrlTag::deserialize), - Map.entry("v", VoteTag::deserialize), - Map.entry("emoji", EmojiTag::deserialize), - Map.entry("expiration", ExpirationTag::deserialize), - Map.entry("nonce", NonceTag::deserialize), - Map.entry("price", PriceTag::deserialize), - Map.entry("relays", RelaysTag::deserialize), - Map.entry("subject", SubjectTag::deserialize)); - @Override public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - if (!node.isArray() || node.size() == 0 || node.get(0) == null) { + if (!node.isArray() || node.isEmpty() || node.get(0) == null) { throw new IOException("Malformed JSON: Expected a non-empty array."); } - String code = node.get(0).asText(); - Function decoder = TAG_DECODERS.get(code); - BaseTag tag = - decoder != null ? decoder.apply(node) : new GenericTagDecoder<>().decode(node.toString()); + BaseTag tag = new GenericTagDecoder<>().decode(node.toString()); @SuppressWarnings("unchecked") T typed = (T) tag; diff --git a/nostr-java-event/src/main/java/nostr/event/json/serializer/AbstractTagSerializer.java b/nostr-java-event/src/main/java/nostr/event/json/serializer/AbstractTagSerializer.java deleted file mode 100644 index 83cd0e13c..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/serializer/AbstractTagSerializer.java +++ /dev/null @@ -1,41 +0,0 @@ -package nostr.event.json.serializer; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import nostr.event.BaseTag; - -import java.io.IOException; - -import static nostr.event.json.codec.BaseTagEncoder.BASETAG_ENCODER_MAPPER_BLACKBIRD; - -abstract class AbstractTagSerializer extends StdSerializer { - protected AbstractTagSerializer(Class t) { - super(t); - } - - public void serialize(T value, JsonGenerator gen, SerializerProvider serializers) { - try { - final ObjectNode node = BASETAG_ENCODER_MAPPER_BLACKBIRD.getNodeFactory().objectNode(); - value - .getSupportedFields() - .forEach(f -> value.getFieldValue(f).ifPresent(s -> node.put(f.getName(), s))); - - applyCustomAttributes(node, value); - - ArrayNode arrayNode = node.objectNode().putArray("values").add(value.getCode()); - var fieldNames = node.fieldNames(); - while (fieldNames.hasNext()) { - String key = fieldNames.next(); - arrayNode.add(node.get(key).asText()); - } - gen.writePOJO(arrayNode); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - protected void applyCustomAttributes(ObjectNode node, T value) {} -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/serializer/AddressTagSerializer.java b/nostr-java-event/src/main/java/nostr/event/json/serializer/AddressTagSerializer.java deleted file mode 100644 index 53c0263f4..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/serializer/AddressTagSerializer.java +++ /dev/null @@ -1,38 +0,0 @@ -package nostr.event.json.serializer; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import nostr.event.tag.AddressTag; - -import java.io.IOException; - -/** - * @author eric - */ -public class AddressTagSerializer extends JsonSerializer { - - @Override - public void serialize( - AddressTag value, JsonGenerator jsonGenerator, SerializerProvider serializers) - throws IOException { - jsonGenerator.writeStartArray(); - jsonGenerator.writeString("a"); - jsonGenerator.writeString( - value.getKind() - + ":" - + value.getPublicKey().toString() - + ":" - + value.getIdentifierTag().getUuid()); - - value.getRelayOptional() - .ifPresent(relay -> { - try { - jsonGenerator.writeString("," + relay.getUri()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - jsonGenerator.writeEndArray(); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/serializer/BaseTagSerializer.java b/nostr-java-event/src/main/java/nostr/event/json/serializer/BaseTagSerializer.java index a1004a397..7bc83b6a0 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/serializer/BaseTagSerializer.java +++ b/nostr-java-event/src/main/java/nostr/event/json/serializer/BaseTagSerializer.java @@ -1,12 +1,31 @@ package nostr.event.json.serializer; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import nostr.event.BaseTag; +import nostr.event.tag.GenericTag; -public class BaseTagSerializer extends AbstractTagSerializer { +import java.io.IOException; + +/** + * Serializer for BaseTag. Delegates to GenericTagSerializer for GenericTag instances. + */ +public class BaseTagSerializer extends StdSerializer { + + private static final GenericTagSerializer GENERIC_TAG_SERIALIZER = new GenericTagSerializer(); - // Generics are erased at runtime; serializer is intentionally bound to BaseTag.class - @SuppressWarnings("unchecked") public BaseTagSerializer() { - super((Class) BaseTag.class); + super(BaseTag.class); + } + + @Override + public void serialize(BaseTag value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + if (value instanceof GenericTag genericTag) { + GENERIC_TAG_SERIALIZER.serialize(genericTag, gen, serializers); + } else { + throw new IOException("Unknown BaseTag subclass: " + value.getClass().getName()); + } } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/serializer/CashuTokenSerializer.java b/nostr-java-event/src/main/java/nostr/event/json/serializer/CashuTokenSerializer.java deleted file mode 100644 index 080c1f1ed..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/serializer/CashuTokenSerializer.java +++ /dev/null @@ -1,42 +0,0 @@ -package nostr.event.json.serializer; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import nostr.event.entities.CashuToken; - -public class CashuTokenSerializer extends JsonSerializer { - - @Override - public void serialize( - CashuToken value, JsonGenerator jsonGenerator, SerializerProvider serializers) - throws java.io.IOException { - jsonGenerator.writeStartObject(); - - // Write the mint field - jsonGenerator.writeStringField("mint", value.getMint().getUrl()); - - // Write the proofs array - jsonGenerator.writeArrayFieldStart("proofs"); - for (var proof : value.getProofs()) { - jsonGenerator.writeStartObject(); - jsonGenerator.writeStringField("id", proof.getId()); - jsonGenerator.writeNumberField("amount", proof.getAmount()); - jsonGenerator.writeStringField("secret", proof.getSecret()); - jsonGenerator.writeStringField("C", proof.getC()); - jsonGenerator.writeEndObject(); - } - jsonGenerator.writeEndArray(); - - // Write the del array if not empty - if (!value.getDestroyed().isEmpty()) { - jsonGenerator.writeArrayFieldStart("del"); - for (var destroyed : value.getDestroyed()) { - jsonGenerator.writeString(destroyed); - } - jsonGenerator.writeEndArray(); - } - - jsonGenerator.writeEndObject(); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/serializer/ExpirationTagSerializer.java b/nostr-java-event/src/main/java/nostr/event/json/serializer/ExpirationTagSerializer.java deleted file mode 100644 index df9189033..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/serializer/ExpirationTagSerializer.java +++ /dev/null @@ -1,24 +0,0 @@ -package nostr.event.json.serializer; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import nostr.event.tag.ExpirationTag; - -import java.io.IOException; - -/** - * @author eric - */ -public class ExpirationTagSerializer extends JsonSerializer { - - @Override - public void serialize( - ExpirationTag value, JsonGenerator jsonGenerator, SerializerProvider serializers) - throws IOException { - jsonGenerator.writeStartArray(); - jsonGenerator.writeString("expiration"); - jsonGenerator.writeString(Integer.toString(value.getExpiration())); - jsonGenerator.writeEndArray(); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/serializer/GenericTagSerializer.java b/nostr-java-event/src/main/java/nostr/event/json/serializer/GenericTagSerializer.java index 2c4448b9a..6b398911b 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/serializer/GenericTagSerializer.java +++ b/nostr-java-event/src/main/java/nostr/event/json/serializer/GenericTagSerializer.java @@ -1,18 +1,29 @@ package nostr.event.json.serializer; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import nostr.event.tag.GenericTag; -public class GenericTagSerializer extends AbstractTagSerializer { +import java.io.IOException; + +import static nostr.event.json.codec.BaseTagEncoder.BASETAG_ENCODER_MAPPER_BLACKBIRD; + +/** + * Serializes a GenericTag as a JSON array: ["code", "param0", "param1", ...] + */ +public class GenericTagSerializer extends StdSerializer { - // Generics are erased at runtime; serializer is intentionally bound to GenericTag.class - @SuppressWarnings("unchecked") public GenericTagSerializer() { - super((Class) GenericTag.class); + super(GenericTag.class); } @Override - protected void applyCustomAttributes(ObjectNode node, T value) { - value.getAttributes().forEach(a -> node.put(a.name(), a.value().toString())); + public void serialize(GenericTag value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + var arrayNode = BASETAG_ENCODER_MAPPER_BLACKBIRD.getNodeFactory().arrayNode(); + arrayNode.add(value.getCode()); + value.getParams().forEach(arrayNode::add); + gen.writePOJO(arrayNode); } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/serializer/IdentifierTagSerializer.java b/nostr-java-event/src/main/java/nostr/event/json/serializer/IdentifierTagSerializer.java deleted file mode 100644 index f6393a2ce..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/serializer/IdentifierTagSerializer.java +++ /dev/null @@ -1,21 +0,0 @@ -package nostr.event.json.serializer; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import nostr.event.tag.IdentifierTag; - -import java.io.IOException; - -public class IdentifierTagSerializer extends JsonSerializer { - - @Override - public void serialize( - IdentifierTag value, JsonGenerator jsonGenerator, SerializerProvider serializers) - throws IOException { - jsonGenerator.writeStartArray(); - jsonGenerator.writeString("d"); - jsonGenerator.writeString(value.getUuid()); - jsonGenerator.writeEndArray(); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/serializer/ReferenceTagSerializer.java b/nostr-java-event/src/main/java/nostr/event/json/serializer/ReferenceTagSerializer.java deleted file mode 100644 index 1d9fc7332..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/serializer/ReferenceTagSerializer.java +++ /dev/null @@ -1,31 +0,0 @@ -package nostr.event.json.serializer; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import nostr.event.tag.ReferenceTag; - -import java.io.IOException; - -/** - * @author eric - */ -public class ReferenceTagSerializer extends JsonSerializer { - - @Override - public void serialize( - ReferenceTag refTag, JsonGenerator jsonGenerator, SerializerProvider serializers) - throws IOException { - jsonGenerator.writeStartArray(); - jsonGenerator.writeString("r"); - jsonGenerator.writeString(refTag.getUri().toString()); - refTag.getMarkerOptional().ifPresent(m -> { - try { - jsonGenerator.writeString(m.getValue()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - jsonGenerator.writeEndArray(); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/serializer/RelaysTagSerializer.java b/nostr-java-event/src/main/java/nostr/event/json/serializer/RelaysTagSerializer.java deleted file mode 100644 index 4944a7aad..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/serializer/RelaysTagSerializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package nostr.event.json.serializer; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import lombok.NonNull; -import nostr.event.tag.RelaysTag; - -import java.io.IOException; - -public class RelaysTagSerializer extends JsonSerializer { - - @Override - public void serialize( - @NonNull RelaysTag relaysTag, - @NonNull JsonGenerator jsonGenerator, - @NonNull SerializerProvider serializerProvider) - throws IOException { - jsonGenerator.writeStartArray(); - jsonGenerator.writeString("relays"); - for (var relay : relaysTag.getRelays()) { - jsonGenerator.writeString(relay.getUri()); - } - jsonGenerator.writeEndArray(); - } - - private static void writeString(JsonGenerator jsonGenerator, String json) throws IOException { - jsonGenerator.writeString(json); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/serializer/TagSerializer.java b/nostr-java-event/src/main/java/nostr/event/json/serializer/TagSerializer.java deleted file mode 100644 index 6975774c7..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/serializer/TagSerializer.java +++ /dev/null @@ -1,24 +0,0 @@ -package nostr.event.json.serializer; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; -import nostr.event.BaseTag; -import nostr.event.tag.GenericTag; - -/** - * @author guilhermegps - */ -@Slf4j -public class TagSerializer extends AbstractTagSerializer { - - public TagSerializer() { - super(BaseTag.class); - } - - @Override - protected void applyCustomAttributes(ObjectNode node, BaseTag value) { - if (value instanceof GenericTag genericTag) { - genericTag.getAttributes().forEach(a -> node.put(a.name(), a.value().toString())); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/message/BaseAuthMessage.java b/nostr-java-event/src/main/java/nostr/event/message/BaseAuthMessage.java deleted file mode 100644 index 52bb12d41..000000000 --- a/nostr-java-event/src/main/java/nostr/event/message/BaseAuthMessage.java +++ /dev/null @@ -1,18 +0,0 @@ -package nostr.event.message; - -import nostr.event.BaseMessage; - -/** - * @author eric - */ -public abstract class BaseAuthMessage extends BaseMessage { - - public BaseAuthMessage(String command) { - super(command); - } - - @Override - public String getNip() { - return "42"; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java b/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java index 0d4089fc4..3696fb12c 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java @@ -9,15 +9,11 @@ import lombok.Setter; import nostr.base.Command; import nostr.event.BaseMessage; -import nostr.event.BaseTag; -import nostr.event.impl.CanonicalAuthenticationEvent; import nostr.event.impl.GenericEvent; import nostr.event.json.EventJsonMapper; import nostr.event.json.codec.BaseEventEncoder; import nostr.event.json.codec.EventEncodingException; -import nostr.event.tag.GenericTag; -import java.util.List; import java.util.Map; import static nostr.base.IDecoder.I_DECODER_MAPPER_BLACKBIRD; @@ -27,11 +23,11 @@ */ @Setter @Getter -public class CanonicalAuthenticationMessage extends BaseAuthMessage { +public class CanonicalAuthenticationMessage extends BaseMessage { - @JsonProperty private final CanonicalAuthenticationEvent event; + @JsonProperty private final GenericEvent event; - public CanonicalAuthenticationMessage(CanonicalAuthenticationEvent event) { + public CanonicalAuthenticationMessage(GenericEvent event) { super(Command.AUTH.name()); this.event = event; } @@ -49,31 +45,13 @@ public String encode() throws EventEncodingException { } } - /** - * Decodes a map representation into a CanonicalAuthenticationMessage. - * - *

    This method converts the map (typically from JSON deserialization) into - * a properly typed CanonicalAuthenticationMessage with a CanonicalAuthenticationEvent. - * - * @param map the map containing event data - * @param the message type (must be BaseMessage) - * @return the decoded CanonicalAuthenticationMessage - * @throws EventEncodingException if decoding fails - */ public static T decode(@NonNull Map map) { try { var event = I_DECODER_MAPPER_BLACKBIRD.convertValue(map, new TypeReference() {}); - List baseTags = event.getTags().stream().filter(GenericTag.class::isInstance).toList(); - - CanonicalAuthenticationEvent canonEvent = - new CanonicalAuthenticationEvent(event.getPubKey(), baseTags, ""); - - canonEvent.setId(String.valueOf(map.get("id"))); - @SuppressWarnings("unchecked") - T result = (T) new CanonicalAuthenticationMessage(canonEvent); + T result = (T) new CanonicalAuthenticationMessage(event); return result; } catch (IllegalArgumentException ex) { throw new EventEncodingException("Failed to decode canonical authentication message", ex); diff --git a/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java b/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java index 0a38f1615..d49314b2a 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java @@ -9,8 +9,6 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import nostr.base.Command; -import nostr.base.IEvent; -import nostr.event.BaseEvent; import nostr.event.BaseMessage; import nostr.event.impl.GenericEvent; import nostr.event.json.EventJsonMapper; @@ -32,16 +30,16 @@ public class EventMessage extends BaseMessage { private static final Function isEventWoSig = (objArr) -> Objects.equals(SIZE_JSON_EVENT_wo_SIG_ID, objArr.length); - @JsonProperty private final IEvent event; + @JsonProperty private final GenericEvent event; @JsonProperty private String subscriptionId; - public EventMessage(@NonNull IEvent event) { + public EventMessage(@NonNull GenericEvent event) { super(Command.EVENT.name()); this.event = event; } - public EventMessage(@NonNull IEvent event, @NonNull String subscriptionId) { + public EventMessage(@NonNull GenericEvent event, @NonNull String subscriptionId) { this(event); this.subscriptionId = subscriptionId; } @@ -53,7 +51,7 @@ public String encode() throws EventEncodingException { try { arrayNode.add( EventJsonMapper.getMapper().readTree( - new BaseEventEncoder<>((BaseEvent) getEvent()).encode())); + new BaseEventEncoder<>(getEvent()).encode())); return EventJsonMapper.getMapper().writeValueAsString(arrayNode); } catch (JsonProcessingException e) { throw new EventEncodingException("Failed to encode event message", e); @@ -70,14 +68,12 @@ public static T decode(@NonNull String jsonString) } } - // Generics are erased at runtime; BaseMessage subtype is determined by caller context private static T processEvent(Object o) { @SuppressWarnings("unchecked") T result = (T) new EventMessage(convertValue((Map) o)); return result; } - // Generics are erased at runtime; BaseMessage subtype is determined by caller context private static T processEvent(Object[] msgArr) { @SuppressWarnings("unchecked") T result = (T) new EventMessage(convertValue((Map) msgArr[2]), msgArr[1].toString()); diff --git a/nostr-java-event/src/main/java/nostr/event/message/GenericMessage.java b/nostr-java-event/src/main/java/nostr/event/message/GenericMessage.java deleted file mode 100644 index eeb07d34a..000000000 --- a/nostr-java-event/src/main/java/nostr/event/message/GenericMessage.java +++ /dev/null @@ -1,73 +0,0 @@ -package nostr.event.message; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; -import nostr.base.ElementAttribute; -import nostr.base.IElement; -import nostr.base.IGenericElement; -import nostr.event.BaseMessage; -import nostr.event.json.EventJsonMapper; -import nostr.event.json.codec.EventEncodingException; - -import java.util.ArrayList; -import java.util.List; - -/** - * @author squirrel - */ -@Setter -@Getter -public class GenericMessage extends BaseMessage implements IGenericElement, IElement { - - @JsonIgnore private final List attributes; - - public GenericMessage(String command) { - this(command, new ArrayList<>()); - } - - public GenericMessage(String command, List attributes) { - super(command); - this.attributes = attributes; - } - - @Override - public void addAttribute(ElementAttribute... attribute) { - addAttributes(List.of(attribute)); - } - - @Override - public void addAttributes(List attributes) { - this.attributes.addAll(attributes); - } - - @Override - public String encode() throws EventEncodingException { - var encoderArrayNode = JsonNodeFactory.instance.arrayNode(); - encoderArrayNode.add(getCommand()); - getAttributes().stream() - .map(ElementAttribute::value) - .forEach(v -> encoderArrayNode.add(v.toString())); - try { - return EventJsonMapper.getMapper().writeValueAsString(encoderArrayNode); - } catch (JsonProcessingException e) { - throw new EventEncodingException("Failed to encode generic message", e); - } - } - - // Generics are erased at runtime; BaseMessage subtype is determined by caller context - public static T decode(@NonNull Object[] msgArr) { - GenericMessage gm = new GenericMessage(msgArr[0].toString()); - for (int i = 1; i < msgArr.length; i++) { - if (msgArr[i] instanceof String) { - gm.addAttribute(new ElementAttribute(null, msgArr[i])); - } - } - @SuppressWarnings("unchecked") - T result = (T) gm; - return result; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/message/RelayAuthenticationMessage.java b/nostr-java-event/src/main/java/nostr/event/message/RelayAuthenticationMessage.java index 155745b90..118a86bf3 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/RelayAuthenticationMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/RelayAuthenticationMessage.java @@ -16,7 +16,7 @@ */ @Setter @Getter -public class RelayAuthenticationMessage extends BaseAuthMessage { +public class RelayAuthenticationMessage extends BaseMessage { @JsonProperty private final String challenge; diff --git a/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java b/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java index 88c79241e..3df264b83 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java @@ -10,10 +10,9 @@ import lombok.ToString; import nostr.base.Command; import nostr.event.BaseMessage; -import nostr.event.filter.Filters; +import nostr.event.filter.EventFilter; import nostr.event.json.EventJsonMapper; import nostr.event.json.codec.EventEncodingException; -import nostr.event.json.codec.FiltersDecoder; import nostr.event.json.codec.FiltersEncoder; import java.time.temporal.ValueRange; @@ -33,13 +32,13 @@ public class ReqMessage extends BaseMessage { @JsonProperty private final String subscriptionId; - @JsonProperty private final List filtersList; + @JsonProperty private final List filtersList; - public ReqMessage(@NonNull String subscriptionId, @NonNull Filters... filtersList) { + public ReqMessage(@NonNull String subscriptionId, @NonNull EventFilter... filtersList) { this(subscriptionId, List.of(filtersList)); } - public ReqMessage(@NonNull String subscriptionId, @NonNull List filtersList) { + public ReqMessage(@NonNull String subscriptionId, @NonNull List filtersList) { super(Command.REQ.name()); validateSubscriptionId(subscriptionId); this.subscriptionId = subscriptionId; @@ -73,7 +72,7 @@ public static T decode( new ReqMessage( subscriptionId.toString(), getJsonFiltersList(jsonString).stream() - .map(filtersList -> new FiltersDecoder().decode(filtersList)) + .map(EventFilter::fromJson) .toList()); return result; } diff --git a/nostr-java-event/src/main/java/nostr/event/support/GenericEventConverter.java b/nostr-java-event/src/main/java/nostr/event/support/GenericEventConverter.java deleted file mode 100644 index 48e621f25..000000000 --- a/nostr-java-event/src/main/java/nostr/event/support/GenericEventConverter.java +++ /dev/null @@ -1,34 +0,0 @@ -package nostr.event.support; - -import lombok.NonNull; -import nostr.event.impl.GenericEvent; -import nostr.util.NostrException; - -import java.lang.reflect.InvocationTargetException; - -/** - * Converts {@link GenericEvent} instances to concrete event subtypes. - */ -public final class GenericEventConverter { - - private GenericEventConverter() {} - - public static T convert( - @NonNull GenericEvent source, @NonNull Class target) throws NostrException { - try { - T event = target.getConstructor().newInstance(); - event.setContent(source.getContent()); - event.setTags(source.getTags()); - event.setPubKey(source.getPubKey()); - event.setId(source.getId()); - event.setSerializedEventCache(source.getSerializedEventCache()); - event.setNip(source.getNip()); - event.setKind(source.getKind()); - event.setSignature(source.getSignature()); - event.setCreatedAt(source.getCreatedAt()); - return event; - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new NostrException("Failed to convert GenericEvent", e); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/support/GenericEventSerializer.java b/nostr-java-event/src/main/java/nostr/event/support/GenericEventSerializer.java deleted file mode 100644 index 268166cdd..000000000 --- a/nostr-java-event/src/main/java/nostr/event/support/GenericEventSerializer.java +++ /dev/null @@ -1,32 +0,0 @@ -package nostr.event.support; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import nostr.event.impl.GenericEvent; -import nostr.event.json.EventJsonMapper; -import nostr.util.NostrException; - -/** - * Serializes {@link GenericEvent} instances into the canonical signing array form. - */ -public final class GenericEventSerializer { - - private GenericEventSerializer() {} - - public static String serialize(GenericEvent event) throws NostrException { - ObjectMapper mapper = EventJsonMapper.getMapper(); - var arrayNode = JsonNodeFactory.instance.arrayNode(); - try { - arrayNode.add(0); - arrayNode.add(event.getPubKey().toString()); - arrayNode.add(event.getCreatedAt()); - arrayNode.add(event.getKind()); - arrayNode.add(mapper.valueToTree(event.getTags())); - arrayNode.add(event.getContent()); - return mapper.writeValueAsString(arrayNode); - } catch (JsonProcessingException e) { - throw new NostrException(e.getMessage()); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/support/GenericEventTypeClassifier.java b/nostr-java-event/src/main/java/nostr/event/support/GenericEventTypeClassifier.java deleted file mode 100644 index a94c78ca1..000000000 --- a/nostr-java-event/src/main/java/nostr/event/support/GenericEventTypeClassifier.java +++ /dev/null @@ -1,29 +0,0 @@ -package nostr.event.support; - -import nostr.base.NipConstants; - -/** - * Utility to classify generic events according to NIP-01 ranges. - */ -public final class GenericEventTypeClassifier { - - private GenericEventTypeClassifier() {} - - public static boolean isReplaceable(Integer kind) { - return kind != null - && kind >= NipConstants.REPLACEABLE_KIND_MIN - && kind < NipConstants.REPLACEABLE_KIND_MAX; - } - - public static boolean isEphemeral(Integer kind) { - return kind != null - && kind >= NipConstants.EPHEMERAL_KIND_MIN - && kind < NipConstants.EPHEMERAL_KIND_MAX; - } - - public static boolean isAddressable(Integer kind) { - return kind != null - && kind >= NipConstants.ADDRESSABLE_KIND_MIN - && kind < NipConstants.ADDRESSABLE_KIND_MAX; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/support/GenericEventUpdater.java b/nostr-java-event/src/main/java/nostr/event/support/GenericEventUpdater.java deleted file mode 100644 index c613e7efc..000000000 --- a/nostr-java-event/src/main/java/nostr/event/support/GenericEventUpdater.java +++ /dev/null @@ -1,35 +0,0 @@ -package nostr.event.support; - -import nostr.event.impl.GenericEvent; -import nostr.util.NostrException; -import nostr.util.NostrUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; -import java.time.Instant; - -/** - * Refreshes derived fields (serialized payload, id, timestamp) for {@link GenericEvent}. - */ -public final class GenericEventUpdater { - - private static final Logger LOGGER = LoggerFactory.getLogger(GenericEventUpdater.class); - - private GenericEventUpdater() {} - - public static void refresh(GenericEvent event) { - try { - event.setCreatedAt(Instant.now().getEpochSecond()); - byte[] serialized = GenericEventSerializer.serialize(event).getBytes(StandardCharsets.UTF_8); - event.setSerializedEventCache(serialized); - event.setId(NostrUtil.bytesToHex(NostrUtil.sha256(serialized))); - } catch (NostrException | NoSuchAlgorithmException ex) { - throw new RuntimeException(ex); - } catch (AssertionError ex) { - LOGGER.warn("Failed to update event during serialization: {}", ex.getMessage(), ex); - throw new RuntimeException(ex); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/support/GenericEventValidator.java b/nostr-java-event/src/main/java/nostr/event/support/GenericEventValidator.java deleted file mode 100644 index c8af01e8c..000000000 --- a/nostr-java-event/src/main/java/nostr/event/support/GenericEventValidator.java +++ /dev/null @@ -1,61 +0,0 @@ -package nostr.event.support; - -import lombok.NonNull; -import nostr.base.NipConstants; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.util.validator.HexStringValidator; - -import java.util.List; -import java.util.Objects; - -/** - * Performs NIP-01 validation on {@link GenericEvent} instances. - */ -public final class GenericEventValidator { - - private GenericEventValidator() {} - - public static void validate(@NonNull GenericEvent event) { - requireHex(event.getId(), NipConstants.EVENT_ID_HEX_LENGTH, "Missing required `id` field."); - requireHex( - event.getPubKey() != null ? event.getPubKey().toString() : null, - NipConstants.PUBLIC_KEY_HEX_LENGTH, - "Missing required `pubkey` field."); - requireHex( - event.getSignature() != null ? event.getSignature().toString() : null, - NipConstants.SIGNATURE_HEX_LENGTH, - "Missing required `sig` field."); - - if (event.getCreatedAt() == null || event.getCreatedAt() < 0) { - throw new AssertionError("Invalid `created_at`: Must be a non-negative integer."); - } - - validateKind(event.getKind()); - validateTags(event.getTags()); - validateContent(event.getContent()); - } - - private static void requireHex(String value, int length, String missingMessage) { - Objects.requireNonNull(value, missingMessage); - HexStringValidator.validateHex(value, length); - } - - public static void validateKind(Integer kind) { - if (kind == null || kind < 0) { - throw new AssertionError("Invalid `kind`: Must be a non-negative integer."); - } - } - - public static void validateTags(List tags) { - if (tags == null) { - throw new AssertionError("Invalid `tags`: Must be a non-null array."); - } - } - - public static void validateContent(String content) { - if (content == null) { - throw new AssertionError("Invalid `content`: Must be a string."); - } - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/AddressTag.java b/nostr-java-event/src/main/java/nostr/event/tag/AddressTag.java deleted file mode 100644 index d4aa4f7b8..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/AddressTag.java +++ /dev/null @@ -1,85 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.ElementAttribute; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; -import nostr.event.json.serializer.AddressTagSerializer; - -import java.util.List; -import java.util.Optional; - -/** Represents an 'a' addressable/parameterized replaceable tag (NIP-33). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@Tag(code = "a", nip = 33) -@NoArgsConstructor -@AllArgsConstructor -@JsonSerialize(using = AddressTagSerializer.class) -public class AddressTag extends BaseTag { - - private Integer kind; - private PublicKey publicKey; - private IdentifierTag identifierTag; - @JsonInclude(JsonInclude.Include.NON_NULL) - private Relay relay; - - /** Optional accessor for relay. */ - public Optional getRelayOptional() { - return Optional.ofNullable(relay); - } - - /** Optional accessor for identifierTag. */ - public Optional getIdentifierTagOptional() { - return Optional.ofNullable(identifierTag); - } - - public static AddressTag deserialize(@NonNull JsonNode node) { - AddressTag tag = new AddressTag(); - - String[] parts = node.get(1).asText().split(":"); - tag.setKind(Integer.valueOf(parts[0])); - tag.setPublicKey(new PublicKey(parts[1])); - if (parts.length == 3) { - tag.setIdentifierTag(new IdentifierTag(parts[2])); - } - - if (node.size() == 3) { - tag.setRelay(new Relay(node.get(2).asText())); - } - return tag; - } - - public static AddressTag updateFields(@NonNull GenericTag tag) { - if (!"a".equals(tag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for AddressTag"); - } - - AddressTag addressTag = new AddressTag(); - List attributes = tag.getAttributes(); - String attr0 = attributes.get(0).value().toString(); - String[] parts = attr0.split(":"); - Integer kind = Integer.parseInt(parts[0]); - PublicKey publicKey = new PublicKey(parts[1]); - String id = parts.length == 3 ? parts[2] : null; - - addressTag.setKind(kind); - addressTag.setPublicKey(publicKey); - addressTag.setIdentifierTag(id != null ? new IdentifierTag(id) : null); - if (tag.getAttributes().size() == 2) { - addressTag.setRelay(new Relay(tag.getAttributes().get(1).value().toString())); - } - return addressTag; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/DelegationTag.java b/nostr-java-event/src/main/java/nostr/event/tag/DelegationTag.java deleted file mode 100644 index 6fcb9a9d5..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/DelegationTag.java +++ /dev/null @@ -1,64 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import nostr.base.ISignable; -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -import java.beans.Transient; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * @author squirrel - */ -@Data -@EqualsAndHashCode(callSuper = false) -@Tag(code = "delegation", nip = 26) -@AllArgsConstructor -@NoArgsConstructor -@JsonPropertyOrder({"pubkey", "conditions", "signature"}) -public class DelegationTag extends BaseTag implements ISignable { - - @Key - @JsonProperty("delegator") - private PublicKey delegator; - - @Key - @JsonProperty("conditions") - private String conditions; - - @Key - @JsonProperty("token") - private Signature signature; - - public DelegationTag(PublicKey delegator, String conditions) { - this.delegator = delegator; - this.conditions = conditions == null ? "" : conditions; - } - - @Transient - public String getToken() { - return "nostr:" + getCode() + ":" + delegator.toString() + ":" + conditions; - } - - @Override - public Consumer getSignatureConsumer() { - return this::setSignature; - } - - @Override - public Supplier getByteArraySupplier() { - return () -> ByteBuffer.wrap(this.getToken().getBytes(StandardCharsets.UTF_8)); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/EmojiTag.java b/nostr-java-event/src/main/java/nostr/event/tag/EmojiTag.java deleted file mode 100644 index 22a87e8f5..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/EmojiTag.java +++ /dev/null @@ -1,49 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -/** Represents an 'emoji' custom emoji tag (NIP-30). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"shortcode", "image-url"}) -@Tag(code = "emoji", nip = 30) -@AllArgsConstructor -@NoArgsConstructor -public class EmojiTag extends BaseTag { - - @Key private String shortcode; - - @Key - @JsonProperty("image-url") - private String url; - - public static EmojiTag deserialize(@NonNull JsonNode node) { - EmojiTag tag = new EmojiTag(); - setRequiredField(node.get(1), (n, t) -> tag.setShortcode(n.asText()), tag); - setRequiredField(node.get(2), (n, t) -> tag.setUrl(n.asText()), tag); - return tag; - } - - public static EmojiTag updateFields(@NonNull GenericTag tag) { - if (!"emoji".equals(tag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for EmojiTag"); - } - - String shortcode = tag.getAttributes().get(0).value().toString(); - String url = tag.getAttributes().get(1).value().toString(); - EmojiTag emojiTag = new EmojiTag(shortcode, url); - return emojiTag; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/EventTag.java b/nostr-java-event/src/main/java/nostr/event/tag/EventTag.java deleted file mode 100644 index ccc8e1850..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/EventTag.java +++ /dev/null @@ -1,80 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Marker; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -import java.util.Optional; - -/** Represents an 'e' event reference tag (NIP-01). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@Tag(code = "e", name = "event") -@JsonPropertyOrder({"idEvent", "recommendedRelayUrl", "marker"}) -@NoArgsConstructor -@AllArgsConstructor -public class EventTag extends BaseTag { - - @Key @JsonProperty private String idEvent; - - @Key - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - private String recommendedRelayUrl; - - @Key(nip = 10) - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - private Marker marker; - - public EventTag(String idEvent) { - this.idEvent = idEvent; - } - - /** Optional accessor for recommendedRelayUrl. */ - public Optional getRecommendedRelayUrlOptional() { - return Optional.ofNullable(recommendedRelayUrl); - } - - /** Optional accessor for marker. */ - public Optional getMarkerOptional() { - return Optional.ofNullable(marker); - } - - public static EventTag deserialize(@NonNull JsonNode node) { - EventTag tag = new EventTag(); - setRequiredField(node.get(1), (n, t) -> tag.setIdEvent(n.asText()), tag); - setOptionalField(node.get(2), (n, t) -> tag.setRecommendedRelayUrl(n.asText()), tag); - setOptionalField( - node.get(3), (n, t) -> tag.setMarker(Marker.valueOf(n.asText().toUpperCase())), tag); - return tag; - } - - public static EventTag updateFields(@NonNull GenericTag tag) { - if (!"e".equals(tag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for EventTag"); - } - EventTag eventTag = new EventTag(tag.getAttributes().get(0).value().toString()); - if (tag.getAttributes().size() > 1) { - eventTag.setRecommendedRelayUrl(tag.getAttributes().get(1).value().toString()); - } - if (tag.getAttributes().size() > 2) { - eventTag.setMarker( - Marker.valueOf(tag.getAttributes().get(2).value().toString().toUpperCase())); - } - - return eventTag; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/ExpirationTag.java b/nostr-java-event/src/main/java/nostr/event/tag/ExpirationTag.java deleted file mode 100644 index 00f678728..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/ExpirationTag.java +++ /dev/null @@ -1,42 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; -import nostr.event.json.serializer.ExpirationTagSerializer; - -/** Represents an 'expiration' tag (NIP-40). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@AllArgsConstructor -@Tag(code = "expiration", name = "Expiration Timestamp", nip = 40) -@NoArgsConstructor -@JsonSerialize(using = ExpirationTagSerializer.class) -public class ExpirationTag extends BaseTag { - - @Key @JsonProperty private Integer expiration; - - public static ExpirationTag deserialize(@NonNull JsonNode node) { - ExpirationTag tag = new ExpirationTag(); - setRequiredField(node.get(1), (n, t) -> tag.setExpiration(Integer.valueOf(n.asText())), tag); - return tag; - } - - public static ExpirationTag updateFields(@NonNull GenericTag tag) { - if (!"expiration".equals(tag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for ExpirationTag"); - } - String expiration = tag.getAttributes().get(0).value().toString(); - return new ExpirationTag(Integer.parseInt(expiration)); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/GenericTag.java b/nostr-java-event/src/main/java/nostr/event/tag/GenericTag.java index 69ba339f1..1d4effe4e 100644 --- a/nostr-java-event/src/main/java/nostr/event/tag/GenericTag.java +++ b/nostr-java-event/src/main/java/nostr/event/tag/GenericTag.java @@ -4,12 +4,11 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NonNull; -import nostr.base.ElementAttribute; -import nostr.base.IGenericElement; import nostr.event.BaseTag; import nostr.event.json.serializer.GenericTagSerializer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -18,11 +17,11 @@ @Data @EqualsAndHashCode(callSuper = false) @JsonSerialize(using = GenericTagSerializer.class) -public class GenericTag extends BaseTag implements IGenericElement { +public class GenericTag extends BaseTag { private String code; - private final List attributes; + private final List params; public GenericTag() { this(""); @@ -32,15 +31,13 @@ public GenericTag(@NonNull String code) { this(code, new ArrayList<>()); } - // Removed deprecated compatibility constructor GenericTag(String, Integer) in 1.0.0. - - public GenericTag(@NonNull String code, @NonNull ElementAttribute... attribute) { - this(code, List.of(attribute)); + public GenericTag(@NonNull String code, @NonNull List params) { + this.code = code; + this.params = new ArrayList<>(params); } - public GenericTag(@NonNull String code, @NonNull List attributes) { - this.code = code; - this.attributes = attributes; + public static GenericTag of(@NonNull String code, @NonNull String... params) { + return new GenericTag(code, List.of(params)); } @Override @@ -48,13 +45,22 @@ public String getCode() { return "".equals(this.code) ? super.getCode() : this.code; } - @Override - public void addAttribute(@NonNull ElementAttribute... attribute) { - this.addAttributes(List.of(attribute)); + public List getParams() { + return Collections.unmodifiableList(this.params); } - @Override - public void addAttributes(@NonNull List attributes) { - this.attributes.addAll(attributes); + public void addParam(@NonNull String param) { + this.params.add(param); + } + + public void addParams(@NonNull List params) { + this.params.addAll(params); + } + + public String[] toArray() { + List all = new ArrayList<>(); + all.add(code); + all.addAll(params); + return all.toArray(new String[0]); } } diff --git a/nostr-java-event/src/main/java/nostr/event/tag/GeohashTag.java b/nostr-java-event/src/main/java/nostr/event/tag/GeohashTag.java deleted file mode 100644 index c724622e6..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/GeohashTag.java +++ /dev/null @@ -1,49 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -/** Represents a 'g' geohash location tag (NIP-12). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"g"}) -@Tag(code = "g", nip = 12) -@NoArgsConstructor -@AllArgsConstructor -public class GeohashTag extends BaseTag { - - @Key - @JsonProperty("g") - private String location; - - public static GeohashTag deserialize(@NonNull JsonNode node) { - GeohashTag tag = new GeohashTag(); - setRequiredField(node.get(1), (n, t) -> tag.setLocation(n.asText()), tag); - return tag; - } - - public static GeohashTag updateFields(@NonNull GenericTag genericTag) { - if (!"g".equals(genericTag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for GeohashTag"); - } - - if (genericTag.getAttributes().size() != 1) { - throw new IllegalArgumentException("Invalid number of attributes for GeohashTag"); - } - - GeohashTag tag = new GeohashTag(); - tag.setLocation(genericTag.getAttributes().get(0).value().toString()); - return tag; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/HashtagTag.java b/nostr-java-event/src/main/java/nostr/event/tag/HashtagTag.java deleted file mode 100644 index ee94adfe4..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/HashtagTag.java +++ /dev/null @@ -1,46 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -/** Represents a 't' hashtag tag (NIP-12). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"t"}) -@Tag(code = "t", nip = 12) -@NoArgsConstructor -@AllArgsConstructor -public class HashtagTag extends BaseTag { - - @Key - @JsonProperty("t") - private String hashTag; - - public static HashtagTag deserialize(@NonNull JsonNode node) { - HashtagTag tag = new HashtagTag(); - setRequiredField(node.get(1), (n, t) -> tag.setHashTag(n.asText()), tag); - return tag; - } - - public static HashtagTag updateFields(@NonNull GenericTag genericTag) { - if (!"t".equals(genericTag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for HashtagTag"); - } - - if (genericTag.getAttributes().size() != 1) { - throw new IllegalArgumentException("Invalid number of attributes for HashtagTag"); - } - return new HashtagTag(genericTag.getAttributes().get(0).value().toString()); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/IdentifierTag.java b/nostr-java-event/src/main/java/nostr/event/tag/IdentifierTag.java deleted file mode 100644 index bbec3a2d3..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/IdentifierTag.java +++ /dev/null @@ -1,38 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -/** Represents a 'd' identifier tag (NIP-33). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = false) -@JsonPropertyOrder({"uuid"}) -@Tag(code = "d", nip = 33) -@NoArgsConstructor -@AllArgsConstructor -public class IdentifierTag extends BaseTag { - - @Key @JsonProperty private String uuid; - - public static IdentifierTag deserialize(@NonNull JsonNode node) { - IdentifierTag tag = new IdentifierTag(); - setRequiredField(node.get(1), (n, t) -> tag.setUuid(n.asText()), tag); - return tag; - } - - public static IdentifierTag updateFields(@NonNull GenericTag tag) { - IdentifierTag identifierTag = new IdentifierTag(tag.getAttributes().get(0).value().toString()); - return identifierTag; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/LabelNamespaceTag.java b/nostr-java-event/src/main/java/nostr/event/tag/LabelNamespaceTag.java deleted file mode 100644 index f24a244e1..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/LabelNamespaceTag.java +++ /dev/null @@ -1,39 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -@Data -/** Represents an 'L' label namespace tag (NIP-32). */ -@EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"L"}) -@Tag(code = "L", nip = 32) -@NoArgsConstructor -@AllArgsConstructor -public class LabelNamespaceTag extends BaseTag { - - @Key - @JsonProperty("L") - private String nameSpace; - - public static LabelNamespaceTag deserialize(@NonNull JsonNode node) { - LabelNamespaceTag tag = new LabelNamespaceTag(); - setRequiredField(node.get(1), (n, t) -> tag.setNameSpace(n.asText()), tag); - return tag; - } - - public static LabelNamespaceTag updateFields(@NonNull GenericTag tag) { - LabelNamespaceTag labelNamespaceTag = - new LabelNamespaceTag(tag.getAttributes().get(0).value().toString()); - return labelNamespaceTag; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/LabelTag.java b/nostr-java-event/src/main/java/nostr/event/tag/LabelTag.java deleted file mode 100644 index dec74a87e..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/LabelTag.java +++ /dev/null @@ -1,51 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -@Data -/** Represents an 'l' label tag (NIP-32). */ -@EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"l", "L"}) -@Tag(code = "l", nip = 32) -@NoArgsConstructor -@AllArgsConstructor -public class LabelTag extends BaseTag { - - @Key - @JsonProperty("l") - private String label; - - @Key - @JsonProperty("L") - private String nameSpace; - - public LabelTag(@NonNull String label, @NonNull LabelNamespaceTag labelNamespaceTag) { - this(label, labelNamespaceTag.getNameSpace()); - } - - public static LabelTag deserialize(@NonNull JsonNode node) { - LabelTag tag = new LabelTag(); - setRequiredField(node.get(1), (n, t) -> tag.setLabel(n.asText()), tag); - setRequiredField(node.get(2), (n, t) -> tag.setNameSpace(n.asText()), tag); - return tag; - } - - public static LabelTag updateFields(@NonNull GenericTag tag) { - if (!"l".equals(tag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for LabelTag"); - } - return new LabelTag( - tag.getAttributes().get(0).value().toString(), - tag.getAttributes().get(1).value().toString()); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/NonceTag.java b/nostr-java-event/src/main/java/nostr/event/tag/NonceTag.java deleted file mode 100644 index fd27802ec..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/NonceTag.java +++ /dev/null @@ -1,55 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -/** Represents a 'nonce' proof-of-work tag (NIP-13). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@Tag(code = "nonce", nip = 13) -@JsonPropertyOrder({"nonce", "difficulty"}) -@NoArgsConstructor -public class NonceTag extends BaseTag { - - @Key - @JsonProperty("nonce") - private Integer nonce; - - @Key - @JsonProperty("difficulty") - private Integer difficulty; - - public NonceTag(@NonNull Integer nonce, @NonNull Integer difficulty) { - this.nonce = nonce; - this.difficulty = difficulty; - } - - public static NonceTag deserialize(@NonNull JsonNode node) { - NonceTag tag = new NonceTag(); - setRequiredField(node.get(1), (n, t) -> tag.setNonce(n.asInt()), tag); - setRequiredField(node.get(2), (n, t) -> tag.setDifficulty(n.asInt()), tag); - return tag; - } - - public static NonceTag updateFields(@NonNull GenericTag genericTag) { - if (!"nonce".equals(genericTag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for NonceTag"); - } - if (genericTag.getAttributes().size() != 2) { - throw new IllegalArgumentException("Invalid number of attributes for NonceTag"); - } - return new NonceTag( - Integer.valueOf(genericTag.getAttributes().get(0).value().toString()), - Integer.valueOf(genericTag.getAttributes().get(1).value().toString())); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/PriceTag.java b/nostr-java-event/src/main/java/nostr/event/tag/PriceTag.java deleted file mode 100644 index 85727be24..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/PriceTag.java +++ /dev/null @@ -1,86 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -import java.math.BigDecimal; -import java.util.Objects; -import java.util.Optional; - -/** Represents a 'price' tag (NIP-99). */ -@Builder -@Data -@Tag(code = "price", nip = 99) -@RequiredArgsConstructor -@AllArgsConstructor -@JsonPropertyOrder({"number", "currency", "frequency"}) -public class PriceTag extends BaseTag { - - @Key - @JsonProperty - @JsonFormat(shape = JsonFormat.Shape.STRING) - private BigDecimal number; - - @Key @JsonProperty private String currency; - - @Key - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - private String frequency; - - /** Optional accessor for frequency. */ - public Optional getFrequencyOptional() { - return Optional.ofNullable(frequency); - } - - public static PriceTag deserialize(@NonNull JsonNode node) { - PriceTag tag = new PriceTag(); - setRequiredField(node.get(1), (n, t) -> tag.setNumber(new BigDecimal(n.asText())), tag); - setOptionalField(node.get(2), (n, t) -> tag.setCurrency(n.asText()), tag); - setOptionalField(node.get(3), (n, t) -> tag.setFrequency(n.asText()), tag); - return tag; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - PriceTag priceTag = (PriceTag) o; - return Objects.equals(number.stripTrailingZeros(), priceTag.number.stripTrailingZeros()) - && Objects.equals(currency, priceTag.currency) - && Objects.equals(frequency, priceTag.frequency); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), number.stripTrailingZeros(), currency, frequency); - } - - public static PriceTag updateFields(@NonNull GenericTag genericTag) { - if (!"price".equals(genericTag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for PriceTag"); - } - - if (genericTag.getAttributes().size() < 2 || genericTag.getAttributes().size() > 3) { - throw new IllegalArgumentException("Invalid number of attributes for PriceTag"); - } - BigDecimal number = new BigDecimal(genericTag.getAttributes().get(0).value().toString()); - String currency = genericTag.getAttributes().get(1).value().toString(); - String frequency = - genericTag.getAttributes().size() > 2 - ? genericTag.getAttributes().get(2).value().toString() - : null; - return new PriceTag(number, currency, frequency); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/PubKeyTag.java b/nostr-java-event/src/main/java/nostr/event/tag/PubKeyTag.java deleted file mode 100644 index 01e03f662..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/PubKeyTag.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.PublicKey; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -import java.util.Optional; - -/** Represents a 'p' public key reference tag (NIP-01). */ -@JsonPropertyOrder({"pubKey", "mainRelayUrl", "petName"}) -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@Tag(code = "p") -@NoArgsConstructor -public class PubKeyTag extends BaseTag { - - @Key - @JsonProperty("publicKey") - private PublicKey publicKey; - - @Key - @JsonProperty("mainRelayUrl") - @JsonInclude(JsonInclude.Include.NON_NULL) - private String mainRelayUrl; - - @Key(nip = 2) - @JsonProperty("petName") - @JsonInclude(JsonInclude.Include.NON_NULL) - private String petName; - - public PubKeyTag(@NonNull PublicKey publicKey) { - this.publicKey = publicKey; - } - - public PubKeyTag(@NonNull PublicKey publicKey, String mainRelayUrl, String petName) { - this.publicKey = publicKey; - this.mainRelayUrl = mainRelayUrl; - this.petName = petName; - } - - /** Optional accessor for mainRelayUrl. */ - public Optional getMainRelayUrlOptional() { - return Optional.ofNullable(mainRelayUrl); - } - - /** Optional accessor for petName. */ - public Optional getPetNameOptional() { - return Optional.ofNullable(petName); - } - - public static PubKeyTag deserialize(@NonNull JsonNode node) { - PubKeyTag tag = new PubKeyTag(); - setRequiredField(node.get(1), (n, t) -> tag.setPublicKey(new PublicKey(n.asText())), tag); - setOptionalField(node.get(2), (n, t) -> tag.setMainRelayUrl(n.asText()), tag); - setOptionalField(node.get(3), (n, t) -> tag.setPetName(n.asText()), tag); - return tag; - } - - public static PubKeyTag updateFields(@NonNull GenericTag tag) { - if (!"p".equals(tag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for PubKeyTag"); - } - - PublicKey pubKey = new PublicKey(tag.getAttributes().get(0).value().toString()); - - String mainRelayUrl = - tag.getAttributes().size() > 1 ? tag.getAttributes().get(1).value().toString() : null; - String petName = - tag.getAttributes().size() > 2 ? tag.getAttributes().get(2).value().toString() : null; - PubKeyTag pubKeyTag = new PubKeyTag(pubKey, mainRelayUrl, petName); - return pubKeyTag; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/ReferenceTag.java b/nostr-java-event/src/main/java/nostr/event/tag/ReferenceTag.java deleted file mode 100644 index 5b484fdd6..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/ReferenceTag.java +++ /dev/null @@ -1,74 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Marker; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; -import nostr.event.json.serializer.ReferenceTagSerializer; - -import java.net.URI; -import java.util.Optional; - -/** Represents an 'r' reference tag (NIP-12). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@Tag(code = "r", nip = 12) -@NoArgsConstructor -@AllArgsConstructor -@JsonSerialize(using = ReferenceTagSerializer.class) -public class ReferenceTag extends BaseTag { - - @Key - @JsonProperty("uri") - private URI uri; - - @Key - @JsonInclude(JsonInclude.Include.NON_NULL) - private Marker marker; - - public ReferenceTag(@NonNull URI uri) { - this.uri = uri; - } - public Optional getUrl() { - return Optional.ofNullable(this.uri); - } - - /** Optional accessor for marker. */ - public Optional getMarkerOptional() { - return Optional.ofNullable(marker); - } - - public static ReferenceTag deserialize(@NonNull JsonNode node) { - ReferenceTag tag = new ReferenceTag(); - setRequiredField(node.get(1), (n, t) -> tag.setUri(URI.create(n.asText())), tag); - setOptionalField( - node.get(2), (n, t) -> tag.setMarker(Marker.valueOf(n.asText().toUpperCase())), tag); - return tag; - } - - public static ReferenceTag updateFields(@NonNull GenericTag genericTag) { - if (!"r".equals(genericTag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for ReferenceTag"); - } - - if (genericTag.getAttributes().size() < 1 || genericTag.getAttributes().size() > 2) { - throw new IllegalArgumentException("Invalid number of attributes for ReferenceTag"); - } - return new ReferenceTag( - URI.create(genericTag.getAttributes().get(0).value().toString()), - genericTag.getAttributes().size() == 2 - ? Marker.valueOf(genericTag.getAttributes().get(1).value().toString().toUpperCase()) - : null); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/RelaysTag.java b/nostr-java-event/src/main/java/nostr/event/tag/RelaysTag.java deleted file mode 100644 index 27601226f..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/RelaysTag.java +++ /dev/null @@ -1,56 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import nostr.base.ElementAttribute; -import nostr.base.Relay; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; -import nostr.event.json.serializer.RelaysTagSerializer; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -/** Represents a 'relays' tag (NIP-57). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = true) -@Tag(code = "relays", nip = 57) -@JsonSerialize(using = RelaysTagSerializer.class) -public class RelaysTag extends BaseTag { - private final List relays; - - public RelaysTag() { - this.relays = new ArrayList<>(); - } - - public RelaysTag(@NonNull List relays) { - this.relays = relays; - } - - public RelaysTag(@NonNull Relay... relays) { - this(List.of(relays)); - } - - public static RelaysTag deserialize(JsonNode node) { - return new RelaysTag( - Optional.ofNullable(node).map(jsonNode -> new Relay(jsonNode.get(1).asText())).orElseThrow()); - } - - public static RelaysTag updateFields(@NonNull GenericTag genericTag) { - if (!"relays".equals(genericTag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for RelaysTag"); - } - - List relays = new ArrayList<>(); - for (ElementAttribute attribute : genericTag.getAttributes()) { - relays.add(new Relay(attribute.value().toString())); - } - return new RelaysTag(relays); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/SubjectTag.java b/nostr-java-event/src/main/java/nostr/event/tag/SubjectTag.java deleted file mode 100644 index c5196f441..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/SubjectTag.java +++ /dev/null @@ -1,53 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -import java.util.Optional; - -/** Represents a 'subject' tag (NIP-14). */ -@Builder -@Data -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode(callSuper = true) -@Tag(code = "subject", nip = 14) -@JsonPropertyOrder({"subject"}) -public final class SubjectTag extends BaseTag { - - @Key - @JsonProperty("subject") - @JsonInclude(JsonInclude.Include.NON_NULL) - private String subject; - - /** Optional accessor for subject. */ - public Optional getSubjectOptional() { - return Optional.ofNullable(subject); - } - - public static SubjectTag deserialize(@NonNull JsonNode node) { - SubjectTag tag = new SubjectTag(); - setOptionalField(node.get(1), (n, t) -> tag.setSubject(n.asText()), tag); - return tag; - } - - public static SubjectTag updateFields(@NonNull GenericTag genericTag) { - if (!"subject".equals(genericTag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for SubjectTag"); - } - - SubjectTag subjectTag = new SubjectTag(genericTag.getAttributes().get(0).value().toString()); - return subjectTag; - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/TagRegistry.java b/nostr-java-event/src/main/java/nostr/event/tag/TagRegistry.java deleted file mode 100644 index f45cd82bf..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/TagRegistry.java +++ /dev/null @@ -1,59 +0,0 @@ -package nostr.event.tag; - -import nostr.event.BaseTag; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; - -/** - * Registry of tag factory functions keyed by tag code. Allows new tag types to be registered - * without modifying {@link BaseTag}. - */ -public final class TagRegistry { - - private static final Map> REGISTRY = - new ConcurrentHashMap<>(); - - static { - register("a", AddressTag::updateFields); - register("d", IdentifierTag::updateFields); - register("e", EventTag::updateFields); - register("g", GeohashTag::updateFields); - register("l", LabelTag::updateFields); - register("L", LabelNamespaceTag::updateFields); - register("p", PubKeyTag::updateFields); - register("r", ReferenceTag::updateFields); - register("t", HashtagTag::updateFields); - register("u", UrlTag::updateFields); - register("v", VoteTag::updateFields); - register("emoji", EmojiTag::updateFields); - register("expiration", ExpirationTag::updateFields); - register("nonce", NonceTag::updateFields); - register("price", PriceTag::updateFields); - register("relays", RelaysTag::updateFields); - register("subject", SubjectTag::updateFields); - } - - private TagRegistry() {} - - /** - * Register a factory function for the supplied tag code. - * - * @param code the tag code - * @param factory factory that creates a {@link BaseTag} from a {@link GenericTag} - */ - public static void register(String code, Function factory) { - REGISTRY.put(code, factory); - } - - /** - * Retrieve the factory function for the given tag code. - * - * @param code tag code - * @return registered factory or {@code null} if none exists - */ - public static Function get(String code) { - return REGISTRY.get(code); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/UrlTag.java b/nostr-java-event/src/main/java/nostr/event/tag/UrlTag.java deleted file mode 100644 index 602db00c3..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/UrlTag.java +++ /dev/null @@ -1,44 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -/** Represents a 'u' URL tag (NIP-61). */ -@EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"u"}) -@Data -@NoArgsConstructor -@AllArgsConstructor -@Tag(code = "u", nip = 61) -public class UrlTag extends BaseTag { - - @Key - @JsonProperty("u") - private String url; - - public static UrlTag deserialize(@NonNull JsonNode node) { - UrlTag tag = new UrlTag(); - setRequiredField(node.get(1), (n, t) -> tag.setUrl(n.asText()), tag); - return tag; - } - - public static UrlTag updateFields(@NonNull GenericTag tag) { - if (!"u".equals(tag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for UrlTag"); - } - - if (tag.getAttributes().size() != 1) { - throw new IllegalArgumentException("Invalid number of attributes for UrlTag"); - } - return new UrlTag(tag.getAttributes().get(0).value().toString()); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/tag/VoteTag.java b/nostr-java-event/src/main/java/nostr/event/tag/VoteTag.java deleted file mode 100644 index d55e970de..000000000 --- a/nostr-java-event/src/main/java/nostr/event/tag/VoteTag.java +++ /dev/null @@ -1,38 +0,0 @@ -package nostr.event.tag; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; - -/** Represents a 'v' vote tag (NIP-2112). */ -@Builder -@Data -@EqualsAndHashCode(callSuper = false) -@Tag(code = "v", nip = 2112) -@NoArgsConstructor -@AllArgsConstructor -public class VoteTag extends BaseTag { - - @Key @JsonProperty private Integer vote; - - public static VoteTag deserialize(@NonNull JsonNode node) { - VoteTag tag = new VoteTag(); - setRequiredField(node.get(1), (n, t) -> tag.setVote(n.asInt()), tag); - return tag; - } - - public static VoteTag updateFields(@NonNull GenericTag genericTag) { - if (!"v".equals(genericTag.getCode())) { - throw new IllegalArgumentException("Invalid tag code for VoteTag"); - } - return new VoteTag(Integer.valueOf(genericTag.getAttributes().get(0).value().toString())); - } -} diff --git a/nostr-java-base/src/test/java/nostr/base/BaseKeyTest.java b/nostr-java-event/src/test/java/nostr/base/BaseKeyTest.java similarity index 100% rename from nostr-java-base/src/test/java/nostr/base/BaseKeyTest.java rename to nostr-java-event/src/test/java/nostr/base/BaseKeyTest.java diff --git a/nostr-java-base/src/test/java/nostr/base/CommandTest.java b/nostr-java-event/src/test/java/nostr/base/CommandTest.java similarity index 100% rename from nostr-java-base/src/test/java/nostr/base/CommandTest.java rename to nostr-java-event/src/test/java/nostr/base/CommandTest.java diff --git a/nostr-java-base/src/test/java/nostr/base/RelayTest.java b/nostr-java-event/src/test/java/nostr/base/RelayTest.java similarity index 100% rename from nostr-java-base/src/test/java/nostr/base/RelayTest.java rename to nostr-java-event/src/test/java/nostr/base/RelayTest.java diff --git a/nostr-java-event/src/test/java/nostr/event/impl/AddressableEventTest.java b/nostr-java-event/src/test/java/nostr/event/impl/AddressableEventTest.java deleted file mode 100644 index 3c20b87e4..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/AddressableEventTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** Unit tests for AddressableEvent kind validation per NIP-01. */ -public class AddressableEventTest { - - @Test - void validKind_30000_shouldPass() { - PublicKey pubKey = createDummyPublicKey(); - AddressableEvent event = new AddressableEvent(pubKey, 30_000, new ArrayList<>(), ""); - assertDoesNotThrow(event::validateKind); - } - - @Test - void validKind_35000_shouldPass() { - PublicKey pubKey = createDummyPublicKey(); - AddressableEvent event = new AddressableEvent(pubKey, 35_000, new ArrayList<>(), ""); - assertDoesNotThrow(event::validateKind); - } - - @Test - void validKind_39999_shouldPass() { - PublicKey pubKey = createDummyPublicKey(); - AddressableEvent event = new AddressableEvent(pubKey, 39_999, new ArrayList<>(), ""); - assertDoesNotThrow(event::validateKind); - } - - @Test - void invalidKind_29999_shouldFail() { - PublicKey pubKey = createDummyPublicKey(); - AddressableEvent event = new AddressableEvent(pubKey, 29_999, new ArrayList<>(), ""); - AssertionError error = assertThrows(AssertionError.class, event::validateKind); - assertTrue(error.getMessage().contains("30000") && error.getMessage().contains("40000")); - } - - @Test - void invalidKind_40000_shouldFail() { - PublicKey pubKey = createDummyPublicKey(); - AddressableEvent event = new AddressableEvent(pubKey, 40_000, new ArrayList<>(), ""); - AssertionError error = assertThrows(AssertionError.class, event::validateKind); - assertTrue(error.getMessage().contains("30000") && error.getMessage().contains("40000")); - } - - @Test - void invalidKind_0_shouldFail() { - PublicKey pubKey = createDummyPublicKey(); - AddressableEvent event = new AddressableEvent(pubKey, 0, new ArrayList<>(), ""); - AssertionError error = assertThrows(AssertionError.class, event::validateKind); - assertTrue(error.getMessage().contains("30000") && error.getMessage().contains("40000")); - } - - private PublicKey createDummyPublicKey() { - byte[] keyBytes = new byte[32]; - for (int i = 0; i < 32; i++) { - keyBytes[i] = (byte) i; - } - return new PublicKey(keyBytes); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/AddressableEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/AddressableEventValidateTest.java deleted file mode 100644 index 79aa22017..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/AddressableEventValidateTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class AddressableEventValidateTest { - private static final String HEX_64 = "a".repeat(64); - private static final String SIG_HEX = "b".repeat(128); - - private AddressableEvent createEvent(int kind) { - AddressableEvent event = - new AddressableEvent(new PublicKey(HEX_64), kind, new ArrayList(), ""); - event.setId(HEX_64); - event.setCreatedAt(Instant.now().getEpochSecond()); - event.setSignature(Signature.fromString(SIG_HEX)); - return event; - } - - // Valid kind within the 30000-39999 range passes validation - @Test - public void testValidateKindSuccess() { - AddressableEvent event = createEvent(30000); - assertDoesNotThrow(event::validate); - } - - // Kind outside the allowed range triggers validation failure - @Test - public void testValidateKindFailure() { - AddressableEvent event = createEvent(1000); - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/ChannelMessageEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/ChannelMessageEventValidateTest.java deleted file mode 100644 index 0d3224401..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/ChannelMessageEventValidateTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class ChannelMessageEventValidateTest { - private static final String HEX_64_A = "a".repeat(64); - private static final String HEX_64_B = "b".repeat(64); - private static final String SIG_HEX = "c".repeat(128); - private static final String CHANNEL_JSON = - "{\"name\":\"chan\",\"about\":\"desc\",\"picture\":\"http://example.com/img.png\"}"; - - private ChannelCreateEvent createRootEvent() { - ChannelCreateEvent root = new ChannelCreateEvent(new PublicKey(HEX_64_A), CHANNEL_JSON); - root.setId(HEX_64_B); - root.setCreatedAt(Instant.now().getEpochSecond()); - root.setSignature(Signature.fromString(SIG_HEX)); - return root; - } - - private ChannelMessageEvent createValidEvent() { - ChannelCreateEvent root = createRootEvent(); - ChannelMessageEvent event = new ChannelMessageEvent(new PublicKey(HEX_64_A), root, "hi", null); - event.setId(HEX_64_A); - event.setCreatedAt(Instant.now().getEpochSecond()); - event.setSignature(Signature.fromString(SIG_HEX)); - return event; - } - - // Channel message referencing its root event validates successfully - @Test - public void testValidateSuccess() { - ChannelMessageEvent event = createValidEvent(); - assertDoesNotThrow(event::validate); - } - - // Missing root event tag results in validation failure - @Test - public void testValidateMissingRootTag() { - ChannelMessageEvent event = - new ChannelMessageEvent(new PublicKey(HEX_64_A), new ArrayList(), "hi"); - event.setId(HEX_64_A); - event.setCreatedAt(Instant.now().getEpochSecond()); - event.setSignature(Signature.fromString(SIG_HEX)); - assertThrows(AssertionError.class, event::validate); - } - - // Wrong kind value triggers validation error - @Test - public void testValidateWrongKind() { - ChannelMessageEvent event = createValidEvent(); - event.setKind(-1); - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/ClassifiedListingEventTest.java b/nostr-java-event/src/test/java/nostr/event/impl/ClassifiedListingEventTest.java deleted file mode 100644 index def650b18..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/ClassifiedListingEventTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package nostr.event.impl; - -import nostr.base.Kind; -import nostr.base.PublicKey; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class ClassifiedListingEventTest { - - // Verifies only allowed kinds (30402, 30403) pass validation. - @Test - void validateKindAllowsOnlyNip99Values() { - PublicKey pk = new PublicKey("e4343c157d026999e106b3bc4245b6c87f52cc8050c4c3b2f34b3567a04ccf95"); - - ClassifiedListingEvent active = - new ClassifiedListingEvent(pk, Kind.CLASSIFIED_LISTING, List.of(), ""); - ClassifiedListingEvent inactive = - new ClassifiedListingEvent(pk, Kind.CLASSIFIED_LISTING_INACTIVE, List.of(), ""); - - assertDoesNotThrow(active::validateKind); - assertDoesNotThrow(inactive::validateKind); - } - - // Ensures other kinds fail validation. - @Test - void validateKindRejectsInvalidValues() { - PublicKey pk = new PublicKey("e4343c157d026999e106b3bc4245b6c87f52cc8050c4c3b2f34b3567a04ccf95"); - ClassifiedListingEvent invalid = - new ClassifiedListingEvent(pk, Kind.TEXT_NOTE, List.of(), ""); - assertThrows(AssertionError.class, invalid::validateKind); - } -} - diff --git a/nostr-java-event/src/test/java/nostr/event/impl/ContactListEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/ContactListEventValidateTest.java deleted file mode 100644 index 5eae1afe9..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/ContactListEventValidateTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import nostr.event.tag.PubKeyTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class ContactListEventValidateTest { - private static final String HEX_64_A = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - private static final String HEX_64_B = - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - private static final String SIG_HEX = "c".repeat(128); - - private ContactListEvent createValidEvent() { - PublicKey pubKey = new PublicKey(HEX_64_A); - List tags = new ArrayList<>(); - tags.add(new PubKeyTag(new PublicKey(HEX_64_B))); - ContactListEvent event = new ContactListEvent(pubKey, tags); - event.setId(HEX_64_A); - event.setSignature(Signature.fromString(SIG_HEX)); - event.setCreatedAt(Instant.now().getEpochSecond()); - return event; - } - - @Test - public void testValidateSuccess() { - ContactListEvent event = createValidEvent(); - assertDoesNotThrow(event::validate); - } - - @Test - public void testValidateMissingPTag() { - ContactListEvent event = createValidEvent(); - event.setTags(new ArrayList<>()); - assertThrows(AssertionError.class, event::validate); - } - - @Test - public void testValidateWrongKind() { - ContactListEvent event = createValidEvent(); - event.setKind(-1); - assertThrows(AssertionError.class, event::validate); - } - - @Test - public void testValidateInvalidContent() { - ContactListEvent event = createValidEvent(); - event.setContent(null); - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/DeletionEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/DeletionEventValidateTest.java deleted file mode 100644 index 883d286ae..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/DeletionEventValidateTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import nostr.event.tag.EventTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class DeletionEventValidateTest { - private static final String HEX_64_A = "a".repeat(64); - private static final String HEX_64_B = "b".repeat(64); - private static final String SIG_HEX = "c".repeat(128); - - private DeletionEvent createValidEvent() { - PublicKey pubKey = new PublicKey(HEX_64_A); - List tags = new ArrayList<>(); - tags.add(new EventTag(HEX_64_B)); - tags.add(BaseTag.create("k", "1")); - DeletionEvent event = new DeletionEvent(pubKey, tags); - event.setId(HEX_64_A); - event.setSignature(Signature.fromString(SIG_HEX)); - event.setCreatedAt(Instant.now().getEpochSecond()); - return event; - } - - // Valid deletion event with required tags passes validation - @Test - public void testValidateSuccess() { - DeletionEvent event = createValidEvent(); - assertDoesNotThrow(event::validate); - } - - // Validation fails when event or author tag is missing - @Test - public void testValidateMissingEventOrAuthorTag() { - DeletionEvent event = createValidEvent(); - List tags = new ArrayList<>(); - tags.add(BaseTag.create("k", "1")); - event.setTags(tags); - assertThrows(AssertionError.class, event::validate); - } - - // Validation fails when the kind tag is absent - @Test - public void testValidateMissingKindTag() { - DeletionEvent event = createValidEvent(); - List tags = new ArrayList<>(); - tags.add(new EventTag(HEX_64_B)); - event.setTags(tags); - assertThrows(AssertionError.class, event::validate); - } - - // Validation fails if the event kind is incorrect - @Test - public void testValidateWrongKind() { - DeletionEvent event = createValidEvent(); - event.setKind(-1); - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/DirectMessageEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/DirectMessageEventValidateTest.java deleted file mode 100644 index 8ba6804ac..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/DirectMessageEventValidateTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class DirectMessageEventValidateTest { - private static final String HEX_64_A = "a".repeat(64); - private static final String HEX_64_B = "b".repeat(64); - private static final String SIG_HEX = "c".repeat(128); - - private DirectMessageEvent createValidEvent() { - DirectMessageEvent event = - new DirectMessageEvent(new PublicKey(HEX_64_A), new PublicKey(HEX_64_B), "hello"); - event.setId(HEX_64_A); - event.setCreatedAt(Instant.now().getEpochSecond()); - event.setSignature(Signature.fromString(SIG_HEX)); - return event; - } - - private DirectMessageEvent createEventWithoutRecipient() { - DirectMessageEvent event = - new DirectMessageEvent(new PublicKey(HEX_64_A), new ArrayList(), "hello"); - event.setId(HEX_64_A); - event.setCreatedAt(Instant.now().getEpochSecond()); - event.setSignature(Signature.fromString(SIG_HEX)); - return event; - } - - // Direct message with recipient tag validates successfully - @Test - public void testValidateSuccess() { - DirectMessageEvent event = createValidEvent(); - assertDoesNotThrow(event::validate); - } - - // Missing recipient public key tag causes validation failure - @Test - public void testValidateMissingRecipient() { - DirectMessageEvent event = createEventWithoutRecipient(); - assertThrows(AssertionError.class, event::validate); - } - - // Incorrect kind value is rejected during validation - @Test - public void testValidateWrongKind() { - DirectMessageEvent event = createValidEvent(); - event.setKind(-1); - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventTest.java b/nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventTest.java deleted file mode 100644 index aa2506982..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class EphemeralEventTest { - - // Validates that kinds in [20000, 30000) are accepted. - @Test - void validateKindAllowsEphemeralRange() { - PublicKey pk = new PublicKey("e4343c157d026999e106b3bc4245b6c87f52cc8050c4c3b2f34b3567a04ccf95"); - - EphemeralEvent k20000 = new EphemeralEvent(pk, 20_000, List.of(), ""); - EphemeralEvent k29999 = new EphemeralEvent(pk, 29_999, List.of(), ""); - - assertDoesNotThrow(k20000::validateKind); - assertDoesNotThrow(k29999::validateKind); - } - - // Ensures values outside the range are rejected. - @Test - void validateKindRejectsOutOfRange() { - PublicKey pk = new PublicKey("e4343c157d026999e106b3bc4245b6c87f52cc8050c4c3b2f34b3567a04ccf95"); - EphemeralEvent below = new EphemeralEvent(pk, 19_999, List.of(), ""); - EphemeralEvent atUpper = new EphemeralEvent(pk, 30_000, List.of(), ""); - - assertThrows(AssertionError.class, below::validateKind); - assertThrows(AssertionError.class, atUpper::validateKind); - } -} - diff --git a/nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventValidateTest.java deleted file mode 100644 index 4e2b1ad43..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventValidateTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class EphemeralEventValidateTest { - private static final String HEX_64 = "a".repeat(64); - private static final String SIG_HEX = "b".repeat(128); - - private EphemeralEvent createEvent(int kind) { - EphemeralEvent event = - new EphemeralEvent(new PublicKey(HEX_64), kind, new ArrayList(), ""); - event.setId(HEX_64); - event.setCreatedAt(Instant.now().getEpochSecond()); - event.setSignature(Signature.fromString(SIG_HEX)); - return event; - } - - // Kind within the 20000-29999 range validates successfully - @Test - public void testValidateKindSuccess() { - EphemeralEvent event = createEvent(20000); - assertDoesNotThrow(event::validate); - } - - // Kind outside the 20000-29999 range fails validation - @Test - public void testValidateKindFailure() { - EphemeralEvent event = createEvent(1000); - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/GenericEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/GenericEventValidateTest.java deleted file mode 100644 index d5f99b661..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/GenericEventValidateTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import org.junit.jupiter.api.Test; - -import java.time.Instant; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class GenericEventValidateTest { - - private static final String HEX_64_A = "a".repeat(64); - private static final String HEX_64_B = "b".repeat(64); - private static final String SIG_HEX = "c".repeat(128); - - @Test - public void testValidateMissingId() { - GenericEvent event = new GenericEvent(new PublicKey(HEX_64_A), 1); - event.setSignature(Signature.fromString(SIG_HEX)); - event.setCreatedAt(Instant.now().getEpochSecond()); - - NullPointerException ex = assertThrows(NullPointerException.class, event::validate); - assertEquals("Missing required `id` field.", ex.getMessage()); - } - - @Test - public void testValidateMissingPubKey() { - GenericEvent event = new GenericEvent(); - event.setId(HEX_64_A); - event.setSignature(Signature.fromString(SIG_HEX)); - event.setCreatedAt(Instant.now().getEpochSecond()); - event.setKind(1); - - NullPointerException ex = assertThrows(NullPointerException.class, event::validate); - assertEquals("Missing required `pubkey` field.", ex.getMessage()); - } - - @Test - public void testValidateMissingSignature() { - GenericEvent event = new GenericEvent(new PublicKey(HEX_64_A), 1); - event.setId(HEX_64_B); - event.setCreatedAt(Instant.now().getEpochSecond()); - - NullPointerException ex = assertThrows(NullPointerException.class, event::validate); - assertEquals("Missing required `sig` field.", ex.getMessage()); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/HideMessageEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/HideMessageEventValidateTest.java deleted file mode 100644 index 4072114c7..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/HideMessageEventValidateTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import nostr.event.tag.EventTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class HideMessageEventValidateTest { - private static final String HEX_64_A = "a".repeat(64); - private static final String HEX_64_B = "b".repeat(64); - private static final String SIG_HEX = "c".repeat(128); - - private HideMessageEvent createValidEvent() { - List tags = new ArrayList<>(); - tags.add(new EventTag(HEX_64_B)); - HideMessageEvent event = new HideMessageEvent(new PublicKey(HEX_64_A), tags, ""); - event.setId(HEX_64_A); - event.setCreatedAt(Instant.now().getEpochSecond()); - event.setSignature(Signature.fromString(SIG_HEX)); - return event; - } - - // Hide message event with at least one event tag validates successfully - @Test - public void testValidateSuccess() { - HideMessageEvent event = createValidEvent(); - assertDoesNotThrow(event::validate); - } - - // Missing event tag causes validation to fail - @Test - public void testValidateMissingEventTag() { - HideMessageEvent event = createValidEvent(); - event.setTags(new ArrayList<>()); - assertThrows(AssertionError.class, event::validate); - } - - // Wrong kind value triggers validation error - @Test - public void testValidateWrongKind() { - HideMessageEvent event = createValidEvent(); - event.setKind(-1); - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/MuteUserEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/MuteUserEventValidateTest.java deleted file mode 100644 index 585f525a2..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/MuteUserEventValidateTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import nostr.event.tag.PubKeyTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class MuteUserEventValidateTest { - private static final String HEX_64_A = "a".repeat(64); - private static final String HEX_64_B = "b".repeat(64); - private static final String SIG_HEX = "c".repeat(128); - - private MuteUserEvent createValidEvent() { - PublicKey pubKey = new PublicKey(HEX_64_A); - List tags = new ArrayList<>(); - tags.add(new PubKeyTag(new PublicKey(HEX_64_B))); - MuteUserEvent event = new MuteUserEvent(pubKey, tags, "mute"); - event.setId(HEX_64_A); - event.setSignature(Signature.fromString(SIG_HEX)); - event.setCreatedAt(Instant.now().getEpochSecond()); - return event; - } - - // Valid mute user event should pass validation - @Test - public void testValidateSuccess() { - MuteUserEvent event = createValidEvent(); - assertDoesNotThrow(event::validate); - } - - // Validation fails when the pubkey tag is missing - @Test - public void testValidateMissingPubKeyTag() { - MuteUserEvent event = createValidEvent(); - event.setTags(new ArrayList<>()); - assertThrows(AssertionError.class, event::validate); - } - - // Validation fails if the event kind is incorrect - @Test - public void testValidateWrongKind() { - MuteUserEvent event = createValidEvent(); - event.setKind(-1); - assertThrows(AssertionError.class, event::validate); - } - - // Retrieves the muted user's public key from tags - @Test - public void testGetMutedUser() { - MuteUserEvent event = createValidEvent(); - assertEquals(HEX_64_B, event.getMutedUser().toString()); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/ReactionEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/ReactionEventValidateTest.java deleted file mode 100644 index 1b6e5f98c..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/ReactionEventValidateTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import nostr.event.tag.EventTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class ReactionEventValidateTest { - private static final String HEX_64_A = "a".repeat(64); - private static final String HEX_64_B = "b".repeat(64); - private static final String SIG_HEX = "c".repeat(128); - - private ReactionEvent createValidEvent() { - PublicKey pubKey = new PublicKey(HEX_64_A); - List tags = new ArrayList<>(); - tags.add(new EventTag(HEX_64_B)); - ReactionEvent event = new ReactionEvent(pubKey, tags, "+"); - event.setId(HEX_64_A); - event.setSignature(Signature.fromString(SIG_HEX)); - event.setCreatedAt(Instant.now().getEpochSecond()); - return event; - } - - // Valid reaction event should pass validation - @Test - public void testValidateSuccess() { - ReactionEvent event = createValidEvent(); - assertDoesNotThrow(event::validate); - } - - // Validation fails when the required event tag is missing - @Test - public void testValidateMissingEventTag() { - ReactionEvent event = createValidEvent(); - event.setTags(new ArrayList<>()); - assertThrows(AssertionError.class, event::validate); - } - - // Validation fails if the event kind is incorrect - @Test - public void testValidateWrongKind() { - ReactionEvent event = createValidEvent(); - event.setKind(-1); - assertThrows(AssertionError.class, event::validate); - } - - // Retrieves the ID of the reacted event from tags - @Test - public void testGetReactedEventId() { - ReactionEvent event = createValidEvent(); - assertEquals(HEX_64_B, event.getReactedEventId()); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/ReplaceableEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/ReplaceableEventValidateTest.java deleted file mode 100644 index dab5b3167..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/ReplaceableEventValidateTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class ReplaceableEventValidateTest { - private static final String HEX_64_A = "a".repeat(64); - private static final String SIG_HEX = "c".repeat(128); - - private ReplaceableEvent createEventWithKind(int kind) { - PublicKey pubKey = new PublicKey(HEX_64_A); - List tags = new ArrayList<>(); - ReplaceableEvent event = new ReplaceableEvent(pubKey, kind, tags, ""); - event.setId(HEX_64_A); - event.setSignature(Signature.fromString(SIG_HEX)); - event.setCreatedAt(Instant.now().getEpochSecond()); - return event; - } - - // Validation succeeds when kind falls within replaceable range - @Test - public void testValidateKindInRange() { - ReplaceableEvent event = createEventWithKind(10_000); - assertDoesNotThrow(event::validate); - } - - // Kind zero is allowed for replaceable events - @Test - public void testValidateKindZero() { - ReplaceableEvent event = createEventWithKind(0); - assertDoesNotThrow(event::validate); - } - - // Kind three is permitted under replaceable rules - @Test - public void testValidateKindThree() { - ReplaceableEvent event = createEventWithKind(3); - assertDoesNotThrow(event::validate); - } - - // Validation fails when kind is outside the replaceable range - @Test - public void testValidateKindInvalid() { - ReplaceableEvent event = createEventWithKind(9_999); - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/TextNoteEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/TextNoteEventValidateTest.java deleted file mode 100644 index 0bb9f2e65..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/TextNoteEventValidateTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package nostr.event.impl; - -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import nostr.event.tag.PubKeyTag; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Field; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class TextNoteEventValidateTest { - private static final String HEX_64_A = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - private static final String HEX_64_B = - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - private static final String SIG_HEX = "e".repeat(128); - - private TextNoteEvent createValidEvent() { - PublicKey pubKey = new PublicKey(HEX_64_A); - List tags = new ArrayList<>(); - tags.add(new PubKeyTag(new PublicKey(HEX_64_B))); - TextNoteEvent event = new TextNoteEvent(pubKey, tags, "note content"); - event.setId(HEX_64_A); - event.setSignature(Signature.fromString(SIG_HEX)); - event.setCreatedAt(Instant.now().getEpochSecond()); - return event; - } - - private void clearTags(TextNoteEvent event) { - try { - Field f = GenericEvent.class.getDeclaredField("tags"); - f.setAccessible(true); - f.set(event, null); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - public void testValidateSuccess() { - TextNoteEvent event = createValidEvent(); - assertDoesNotThrow(event::validate); - } - - @Test - public void testValidateMissingTags() { - TextNoteEvent event = createValidEvent(); - clearTags(event); - assertThrows(AssertionError.class, event::validate); - } - - @Test - public void testValidateWrongKind() { - TextNoteEvent event = createValidEvent(); - event.setKind(-1); - assertThrows(AssertionError.class, event::validate); - } - - @Test - public void testValidateInvalidContent() { - TextNoteEvent event = createValidEvent(); - event.setContent(null); - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/ZapRequestEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/ZapRequestEventValidateTest.java deleted file mode 100644 index 1a3840bbe..000000000 --- a/nostr-java-event/src/test/java/nostr/event/impl/ZapRequestEventValidateTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package nostr.event.impl; - -import nostr.base.ElementAttribute; -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.BaseTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PubKeyTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class ZapRequestEventValidateTest { - private static final String HEX_64_A = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - private static final String HEX_64_B = - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - private static final String SIG_HEX = "d".repeat(128); - - private ZapRequestEvent createValidEvent() { - PublicKey pubKey = new PublicKey(HEX_64_A); - List tags = new ArrayList<>(); - tags.add(new PubKeyTag(new PublicKey(HEX_64_B))); - GenericTag amountTag = new GenericTag("amount"); - amountTag.addAttribute(new ElementAttribute("amount", "1000")); - tags.add(amountTag); - GenericTag lnurlTag = new GenericTag("lnurl"); - lnurlTag.addAttribute(new ElementAttribute("lnurl", "lnurl-value")); - tags.add(lnurlTag); - ZapRequestEvent event = new ZapRequestEvent(pubKey, tags, "content"); - event.setId(HEX_64_A); - event.setSignature(Signature.fromString(SIG_HEX)); - event.setCreatedAt(Instant.now().getEpochSecond()); - return event; - } - - @Test - public void testValidateSuccess() { - ZapRequestEvent event = createValidEvent(); - assertDoesNotThrow(event::validate); - } - - @Test - public void testValidateMissingPTag() { - ZapRequestEvent event = createValidEvent(); - event.setTags(event.getTags().subList(1, event.getTags().size())); - assertThrows(AssertionError.class, event::validate); - } - - @Test - public void testValidateMissingAmountTag() { - ZapRequestEvent event = createValidEvent(); - List tags = new ArrayList<>(event.getTags()); - tags.removeIf(t -> "amount".equals(t.getCode())); - event.setTags(tags); - assertThrows(AssertionError.class, event::validate); - } - - @Test - public void testValidateMissingLnurlTag() { - ZapRequestEvent event = createValidEvent(); - List tags = new ArrayList<>(event.getTags()); - tags.removeIf(t -> "lnurl".equals(t.getCode())); - event.setTags(tags); - assertThrows(AssertionError.class, event::validate); - } - - @Test - public void testValidateWrongKind() { - ZapRequestEvent event = createValidEvent(); - event.setKind(-1); - assertThrows(AssertionError.class, event::validate); - } - - @Test - public void testValidateInvalidContent() { - ZapRequestEvent event = createValidEvent(); - event.setContent(null); - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/json/codec/BaseEventEncoderTest.java b/nostr-java-event/src/test/java/nostr/event/json/codec/BaseEventEncoderTest.java index 9cbab220f..e64937967 100644 --- a/nostr-java-event/src/test/java/nostr/event/json/codec/BaseEventEncoderTest.java +++ b/nostr-java-event/src/test/java/nostr/event/json/codec/BaseEventEncoderTest.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import nostr.event.BaseEvent; +import nostr.event.impl.GenericEvent; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -21,25 +21,14 @@ public void serialize(Object value, JsonGenerator gen, SerializerProvider serial } } - static class FailingEvent extends BaseEvent { + static class FailingEvent extends GenericEvent { @JsonSerialize(using = FailingSerializer.class) public String getAttr() { return "boom"; } - - @Override - public String getId() { - return ""; - } - - @Override - public String toBech32() { - return ""; - } } @Test - // Ensures encode throws EventEncodingException when serialization fails void encodeThrowsEventEncodingException() { var encoder = new BaseEventEncoder<>(new FailingEvent()); assertThrows(EventEncodingException.class, encoder::encode); diff --git a/nostr-java-event/src/test/java/nostr/event/serializer/EventSerializerTest.java b/nostr-java-event/src/test/java/nostr/event/serializer/EventSerializerTest.java index 5109b6f86..051bfac99 100644 --- a/nostr-java-event/src/test/java/nostr/event/serializer/EventSerializerTest.java +++ b/nostr-java-event/src/test/java/nostr/event/serializer/EventSerializerTest.java @@ -1,6 +1,6 @@ package nostr.event.serializer; -import nostr.base.Kind; +import nostr.base.Kinds; import nostr.base.PublicKey; import nostr.event.BaseTag; import org.junit.jupiter.api.Test; @@ -21,13 +21,13 @@ public class EventSerializerTest { void serializeAndComputeIdStable() throws Exception { PublicKey pk = new PublicKey(HEX64); long ts = 1700000000L; - String json = EventSerializer.serialize(pk, ts, Kind.TEXT_NOTE.getValue(), List.of(), "hello"); + String json = EventSerializer.serialize(pk, ts, Kinds.TEXT_NOTE, List.of(), "hello"); assertTrue(json.startsWith("[")); byte[] bytes = json.getBytes(StandardCharsets.UTF_8); String id = EventSerializer.computeEventId(bytes); // compute again should match - String id2 = EventSerializer.serializeAndComputeId(pk, ts, Kind.TEXT_NOTE.getValue(), List.of(), "hello"); + String id2 = EventSerializer.serializeAndComputeId(pk, ts, Kinds.TEXT_NOTE, List.of(), "hello"); assertEquals(id, id2); } @@ -35,14 +35,14 @@ void serializeAndComputeIdStable() throws Exception { void serializeIncludesGenericTag() { PublicKey pk = new PublicKey(HEX64); // Use an unregistered tag code to force GenericTag path - assertDoesNotThrow(() -> EventSerializer.serialize(pk, 1700000000L, Kind.TEXT_NOTE.getValue(), List.of(BaseTag.create("zzz")), "")); + assertDoesNotThrow(() -> EventSerializer.serialize(pk, 1700000000L, Kinds.TEXT_NOTE, List.of(BaseTag.create("zzz")), "")); } @Test void computeEventIdThrowsForInvalidAlgorithmIsWrapped() { // We cannot force NoSuchAlgorithmException easily without changing code; ensure basic path works PublicKey pk = new PublicKey(HEX64); - assertDoesNotThrow(() -> EventSerializer.serializeAndComputeId(pk, null, Kind.TEXT_NOTE.getValue(), List.of(), "")); + assertDoesNotThrow(() -> EventSerializer.serializeAndComputeId(pk, null, Kinds.TEXT_NOTE, List.of(), "")); } @Test @@ -50,7 +50,7 @@ void serializeIncludesTagsArray() throws Exception { PublicKey pk = new PublicKey(HEX64); long ts = 1700000001L; BaseTag e = BaseTag.create("e", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - String json = EventSerializer.serialize(pk, ts, Kind.TEXT_NOTE.getValue(), List.of(e), ""); + String json = EventSerializer.serialize(pk, ts, Kinds.TEXT_NOTE, List.of(e), ""); assertTrue(json.contains("\"e\"")); assertTrue(json.contains("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); // ensure tag array wrapper present diff --git a/nostr-java-event/src/test/java/nostr/event/support/GenericEventSupportTest.java b/nostr-java-event/src/test/java/nostr/event/support/GenericEventSupportTest.java deleted file mode 100644 index 62d3bf3e3..000000000 --- a/nostr-java-event/src/test/java/nostr/event/support/GenericEventSupportTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package nostr.event.support; - -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.impl.GenericEvent; -import nostr.util.NostrUtil; -import org.junit.jupiter.api.Test; - -import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** Tests for GenericEventSerializer, Updater and Validator utility classes. */ -public class GenericEventSupportTest { - - private static final String HEX64 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; - private static final String HEX128 = HEX64 + HEX64; - - private GenericEvent newEvent() { - return GenericEvent.builder() - .pubKey(new PublicKey(HEX64)) - .kind(Kind.TEXT_NOTE) - .content("hello") - .build(); - } - - @Test - void serializerProducesCanonicalArray() throws Exception { - GenericEvent event = newEvent(); - String json = GenericEventSerializer.serialize(event); - // Expect leading 0, pubkey, created_at (may be null), kind, tags array, content string - assertTrue(json.startsWith("[")); - assertTrue(json.contains("\"" + event.getPubKey().toString() + "\"")); - assertTrue(json.contains("\"hello\"")); - } - - @Test - void updaterComputesIdAndSerializedCache() throws NoSuchAlgorithmException { - GenericEvent event = newEvent(); - GenericEventUpdater.refresh(event); - assertNotNull(event.getId()); - assertNotNull(event.getSerializedEventCache()); - // Recompute hash from serializer and compare - String serialized = new String(event.getSerializedEventCache(), StandardCharsets.UTF_8); - String expected = NostrUtil.bytesToHex(NostrUtil.sha256(serialized.getBytes(StandardCharsets.UTF_8))); - assertEquals(expected, event.getId()); - } - - @Test - void validatorAcceptsWellFormedEvent() throws Exception { - GenericEvent event = newEvent(); - // set required id and signature fields (hex format only) - GenericEventUpdater.refresh(event); - event.setSignature(Signature.fromString(HEX128)); - // serialize to produce id - event.setId(NostrUtil.bytesToHex(NostrUtil.sha256(GenericEventSerializer.serialize(event).getBytes(StandardCharsets.UTF_8)))); - assertDoesNotThrow(() -> GenericEventValidator.validate(event)); - } - - @Test - void validatorRejectsInvalidFields() { - GenericEvent event = newEvent(); - // Missing id/signature triggers NPE from requireNonNull with clear message - NullPointerException npe = assertThrows(NullPointerException.class, () -> GenericEventValidator.validate(event)); - assertTrue(String.valueOf(npe.getMessage()).contains("Missing required `id` field.")); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/BaseTagTest.java b/nostr-java-event/src/test/java/nostr/event/unit/BaseTagTest.java deleted file mode 100644 index 2166880d6..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/BaseTagTest.java +++ /dev/null @@ -1,248 +0,0 @@ -package nostr.event.unit; - -import nostr.event.BaseTag; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EmojiTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.ExpirationTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.LabelNamespaceTag; -import nostr.event.tag.LabelTag; -import nostr.event.tag.NonceTag; -import nostr.event.tag.PriceTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.ReferenceTag; -import nostr.event.tag.RelaysTag; -import nostr.event.tag.SubjectTag; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - -class BaseTagTest { - - BaseTag genericTag = BaseTag.create("id", "value"); - - @Test - public void testCreateAddressTag() { - String publicKey = "bbbd79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984"; - - String code = "a"; - List params = List.of("30023:" + publicKey + ":test"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(AddressTag.class, tag); - assertEquals(code, tag.getCode()); - - AddressTag addressTag = (AddressTag) tag; - assertEquals("30023", addressTag.getKind().toString()); - assertEquals(publicKey, addressTag.getPublicKey().toString()); - assertEquals("test", addressTag.getIdentifierTag().getUuid()); - } - - @Test - public void testCreateEventTag() { - String code = "e"; - List params = List.of("123abc", "wss://relay.example.com", "ROOT"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(EventTag.class, tag); - assertEquals(code, tag.getCode()); - - EventTag eventTag = (EventTag) tag; - assertEquals("123abc", eventTag.getIdEvent()); - assertEquals("wss://relay.example.com", eventTag.getRecommendedRelayUrl()); - assertEquals("root", eventTag.getMarker().getValue()); - } - - @Test - public void testCreatePubKeyTag() { - String publicKey = "bbbd79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984"; - - String code = "p"; - List params = List.of(publicKey, "wss://relay.example.com"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(PubKeyTag.class, tag); - assertEquals(code, tag.getCode()); - - PubKeyTag pubKeyTag = (PubKeyTag) tag; - assertEquals(publicKey, pubKeyTag.getPublicKey().toString()); - assertEquals("wss://relay.example.com", pubKeyTag.getMainRelayUrl()); - } - - @Test - public void testCreateEmojiTag() { - String code = "emoji"; - List params = List.of("😊", "http://smile.com/icon.gif"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(EmojiTag.class, tag); - assertEquals(code, tag.getCode()); - - EmojiTag emojiTag = (EmojiTag) tag; - assertEquals("http://smile.com/icon.gif", emojiTag.getUrl()); - assertEquals("😊", emojiTag.getShortcode()); - } - - @Test - public void testCreatePriceTag() { - String code = "price"; - List params = List.of("10.99", "USD"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(PriceTag.class, tag); - assertEquals(code, tag.getCode()); - - PriceTag priceTag = (PriceTag) tag; - assertEquals("10.99", priceTag.getNumber().toString()); - assertEquals("USD", priceTag.getCurrency()); - } - - @Test - public void testCreateExpirationTag() { - String code = "expiration"; - int timestamp = 1735689600; - List params = List.of(String.valueOf(timestamp)); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(ExpirationTag.class, tag); - assertEquals(code, tag.getCode()); - - ExpirationTag expirationTag = (ExpirationTag) tag; - assertEquals(timestamp, expirationTag.getExpiration()); - } - - @Test - public void testCreateRelaysTag() { - String code = "relays"; - List params = List.of("wss://relay1.com", "wss://relay2.com"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(RelaysTag.class, tag); - assertEquals(code, tag.getCode()); - - RelaysTag relaysTag = (RelaysTag) tag; - assertEquals(2, relaysTag.getRelays().size()); - assertEquals("wss://relay1.com", relaysTag.getRelays().get(0).getUri()); - assertEquals("wss://relay2.com", relaysTag.getRelays().get(1).getUri()); - } - - @Test - public void testCreateIdentifierTag() { - String code = "d"; - List params = List.of("test-identifier"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(IdentifierTag.class, tag); - assertEquals(code, tag.getCode()); - - IdentifierTag identifierTag = (IdentifierTag) tag; - assertEquals("test-identifier", identifierTag.getUuid()); - } - - @Test - public void testCreateGeohashTag() { - String code = "g"; - List params = List.of("u4pruydqqvj"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(GeohashTag.class, tag); - assertEquals(code, tag.getCode()); - - GeohashTag geohashTag = (GeohashTag) tag; - assertEquals("u4pruydqqvj", geohashTag.getLocation()); - } - - @Test - public void testCreateLabelTag() { - String code = "l"; - List params = List.of("test-label", "namespace"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(LabelTag.class, tag); - assertEquals(code, tag.getCode()); - - LabelTag labelTag = (LabelTag) tag; - assertEquals("test-label", labelTag.getLabel()); - assertEquals("namespace", labelTag.getNameSpace()); - } - - @Test - public void testCreateLabelNameSpaceTag() { - String code = "L"; - List params = List.of("namespace"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(LabelNamespaceTag.class, tag); - assertEquals(code, tag.getCode()); - - LabelNamespaceTag labelNamespaceTag = (LabelNamespaceTag) tag; - assertEquals("namespace", labelNamespaceTag.getNameSpace()); - } - - @Test - public void testCreateReferenceTag() { - String code = "r"; - List params = List.of("wss://relay.example.com"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(ReferenceTag.class, tag); - assertEquals(code, tag.getCode()); - - ReferenceTag referenceTag = (ReferenceTag) tag; - assertEquals("wss://relay.example.com", referenceTag.getUri().toString()); - } - - @Test - public void testCreateHashtagTag() { - String code = "t"; - List params = List.of("nostr"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(HashtagTag.class, tag); - assertEquals(code, tag.getCode()); - - HashtagTag hashtagTag = (HashtagTag) tag; - assertEquals("nostr", hashtagTag.getHashTag()); - } - - @Test - public void testCreateNonceTag() { - String code = "nonce"; - List params = List.of("123456", "20"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(NonceTag.class, tag); - assertEquals(code, tag.getCode()); - - NonceTag nonceTag = (NonceTag) tag; - assertEquals("123456", nonceTag.getNonce().toString()); - assertEquals(20, nonceTag.getDifficulty()); - } - - @Test - public void testCreateSubjectTag() { - String code = "subject"; - List params = List.of("Test Subject"); - BaseTag tag = BaseTag.create(code, params); - - assertInstanceOf(SubjectTag.class, tag); - assertEquals(code, tag.getCode()); - - SubjectTag subjectTag = (SubjectTag) tag; - assertEquals("Test Subject", subjectTag.getSubject()); - } - - @Test - void testToString() { - String result = "GenericTag(code=id, attributes=[ElementAttribute[name=param0, value=value]])"; - assertInstanceOf(GenericTag.class, genericTag); - assertEquals(result, genericTag.toString()); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/CalendarContentAddTagTest.java b/nostr-java-event/src/test/java/nostr/event/unit/CalendarContentAddTagTest.java deleted file mode 100644 index 7d38196a7..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/CalendarContentAddTagTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package nostr.event.unit; - -import nostr.base.PublicKey; -import nostr.event.entities.CalendarContent; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class CalendarContentAddTagTest { - - @Test - // Ensures adding two hashtag tags results in exactly two items without duplication. - void testAddTwoHashtagTagsNoDuplication() { - CalendarContent content = new CalendarContent<>(new IdentifierTag("id-1"), "title", 1L); - - HashtagTag t1 = new HashtagTag("tag1"); - HashtagTag t2 = new HashtagTag("tag2"); - - content.addHashtagTag(t1); - content.addHashtagTag(t2); - - List tags = content.getHashtagTags(); - assertEquals(2, tags.size()); - assertEquals("tag1", tags.get(0).getHashTag()); - assertEquals("tag2", tags.get(1).getHashTag()); - } - - @Test - // Verifies adding a participant PubKeyTag produces a single entry with the expected key. - void testAddParticipantPubKeyTagNoDuplication() { - CalendarContent content = new CalendarContent<>(new IdentifierTag("id-2"), "title", 1L); - - String hex = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; - PubKeyTag p = new PubKeyTag(new PublicKey(hex)); - content.addParticipantPubKeyTag(p); - - List pTags = content.getParticipantPubKeyTags(); - assertEquals(1, pTags.size()); - assertEquals(hex, pTags.get(0).getPublicKey().toString()); - } - - @Test - // Confirms different tag types are tracked independently with correct counts. - void testAddMultipleTagTypesIndependent() { - CalendarContent content = new CalendarContent<>(new IdentifierTag("id-3"), "title", 1L); - - // Add two hashtags - content.addHashtagTag(new HashtagTag("a")); - content.addHashtagTag(new HashtagTag("b")); - - // Add one participant pubkey - content.addParticipantPubKeyTag( - new PubKeyTag( - new PublicKey("2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984"))); - - assertEquals(2, content.getHashtagTags().size()); - assertEquals(1, content.getParticipantPubKeyTags().size()); - assertTrue(content.getGeohashTag().isEmpty()); - } -} - diff --git a/nostr-java-event/src/test/java/nostr/event/unit/CalendarContentDecodeTest.java b/nostr-java-event/src/test/java/nostr/event/unit/CalendarContentDecodeTest.java deleted file mode 100644 index d4ce3e195..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/CalendarContentDecodeTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package nostr.event.unit; - -import nostr.event.impl.CalendarTimeBasedEvent; -import nostr.event.json.codec.GenericEventDecoder; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - -public class CalendarContentDecodeTest { - String eventFullJson = - """ - { - "id": "299ab85049a7923e9cd82329c0fa489ca6fd6d21feeeac33543b1237e14a9e07", - "kind": 30402, - "content": "calendar content", - "pubkey": "cccd79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984", - "created_at": 1726114798510, - "tags": [ - [ "d", "UUID-CalendarTimeBasedEventTest" ], - [ "title", "calendar content title" ], - [ "start", "1726114798510" ], - [ "end", "1726114799510" ], - [ "start_tzid", "America/Costa_Rica" ], - [ "end_tzid", "America/Costa_Rica" ], - [ "summary", "calendar summary" ], - [ "image", "http://www.imm.org/Images/fineMotionS.jpg" ], - [ "location", "calendar content location" ], - [ "g", "calendar content geo-tag-1" ], - [ "p", "444d79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984", "ws://localhost:5555", "PAYER" ], - [ "p", "555d79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984", "ws://localhost:5555", "PAYEE" ], - [ "l", "calendar content label 1 of 2", "calendar content label 2 of 2" ], - [ "t", "calendar content hash-tag-1111" ], - [ "r", "http://www.imm.org/" ] - ], - "sig": "86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546" - } - """; - - String eventMinimalJson = - """ - { - "id": "299ab85049a7923e9cd82329c0fa489ca6fd6d21feeeac33543b1237e14a9e07", - "kind": 30402, - "content": "classified content", - "pubkey": "cccd79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984", - "created_at": 1726114798510, - "tags": [ - [ "d", "UUID-CalendarTimeBasedEventTest" ], - [ "title", "calendar content title" ], - [ "start", "1726114798510" ] - ], - "sig": "86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546" - } - """; - - String problematicBarchettaJson = - """ - { - "id": "a21f990312c06e063af233935a1b7021e2824cedd0c5a46e160acb182e07637c", - "kind": 31923, - "content": "CALENDAR-EVENT CONTENT", - "pubkey": "111df01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6f", - "created_at": 1727482684, - "tags": [ - [ - "d", - "UUID-NEEDS-COMPLETION-001" - ], - [ - "end", - "1727482683878" - ], - [ - "title", - "1111111" - ], - [ - "start", - "1727482683878" - ] - ], - "sig": "c326e782307d740416bf5cb8c9635f7d1b93dec75e61b1ad8d7214a4b61d724230c9f3adb71dfc054b1f77c1ad3b73a2a7802205c64928cf0bbbcfbaf60e8552" - } - """; - - @Test - void testCalendarContentMinimalJsonDecoding() { - assertDoesNotThrow( - () -> new GenericEventDecoder<>(CalendarTimeBasedEvent.class).decode(eventMinimalJson)); - } - - @Test - void testCalendarContentFullJsonDecoding() { - assertDoesNotThrow( - () -> new GenericEventDecoder<>(CalendarTimeBasedEvent.class).decode(eventFullJson)); - } - - @Test - void testCalendarContentProblemBarchettaJsonDecoding() { - assertDoesNotThrow( - () -> new GenericEventDecoder<>(CalendarTimeBasedEvent.class).decode(eventFullJson)); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/CalendarDeserializerTest.java b/nostr-java-event/src/test/java/nostr/event/unit/CalendarDeserializerTest.java deleted file mode 100644 index d00347c8d..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/CalendarDeserializerTest.java +++ /dev/null @@ -1,159 +0,0 @@ -package nostr.event.unit; - -import com.fasterxml.jackson.core.JsonProcessingException; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.base.json.EventJsonMapper; -import nostr.event.BaseTag; -import nostr.event.impl.CalendarDateBasedEvent; -import nostr.event.impl.CalendarEvent; -import nostr.event.impl.CalendarRsvpEvent; -import nostr.event.impl.CalendarTimeBasedEvent; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.ReferenceTag; -import nostr.event.tag.SubjectTag; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class CalendarDeserializerTest { - - private static final PublicKey AUTHOR = - new PublicKey("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"); - private static final String EVENT_ID = - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; - private static final Signature SIGNATURE = - Signature.fromString("c".repeat(128)); - - private GenericEvent baseEvent(int kind, List tags) { - return GenericEvent.builder() - .id(EVENT_ID) - .pubKey(AUTHOR) - .customKind(kind) - .tags(tags) - .content("calendar payload") - .createdAt(1_700_000_111L) - .signature(SIGNATURE) - .build(); - } - - private static BaseTag identifier(String value) { - return IdentifierTag.builder().uuid(value).build(); - } - - private static BaseTag generic(String code, String value) { - return BaseTag.create(code, value); - } - - // Verifies the calendar event deserializer reconstructs identifier and title tags correctly. - @Test - void shouldDeserializeCalendarEvent() throws JsonProcessingException { - AddressTag addressTag = - AddressTag.builder() - .kind(Kind.CALENDAR_EVENT.getValue()) - .publicKey(AUTHOR) - .identifierTag(new IdentifierTag("event-123")) - .build(); - - GenericEvent genericEvent = - baseEvent( - Kind.CALENDAR_EVENT.getValue(), - List.of( - identifier("root-calendar"), - generic("title", "Team calendar"), - generic("start", "1700000100"), - addressTag, - new SubjectTag("planning"))); - - String json = EventJsonMapper.mapper().writeValueAsString(genericEvent); - CalendarEvent calendarEvent = EventJsonMapper.mapper().readValue(json, CalendarEvent.class); - - assertEquals("root-calendar", calendarEvent.getId()); - assertEquals("Team calendar", calendarEvent.getTitle()); - assertTrue(calendarEvent.getCalendarEventIds().contains("event-123")); - } - - // Verifies date-based events expose optional metadata after round-trip deserialization. - @Test - void shouldDeserializeCalendarDateBasedEvent() throws JsonProcessingException { - GenericEvent genericEvent = - baseEvent( - Kind.CALENDAR_DATE_BASED_EVENT.getValue(), - List.of( - identifier("date-calendar"), - generic("title", "Date event"), - generic("start", "1700000200"), - generic("end", "1700000300"), - generic("location", "Room 101"), - new ReferenceTag(java.net.URI.create("https://relay.example")))); - - String json = EventJsonMapper.mapper().writeValueAsString(genericEvent); - CalendarDateBasedEvent calendarEvent = - EventJsonMapper.mapper().readValue(json, CalendarDateBasedEvent.class); - - assertEquals("date-calendar", calendarEvent.getId()); - assertEquals("Room 101", calendarEvent.getLocation().orElse("")); - assertTrue(calendarEvent.getReferences().stream().anyMatch(tag -> tag.getUrl().isPresent())); - } - - // Verifies time-based events deserialize timezone and summary tags. - @Test - void shouldDeserializeCalendarTimeBasedEvent() throws JsonProcessingException { - GenericEvent genericEvent = - baseEvent( - Kind.CALENDAR_TIME_BASED_EVENT.getValue(), - List.of( - identifier("time-calendar"), - generic("title", "Time event"), - generic("start", "1700000400"), - generic("start_tzid", "Europe/Amsterdam"), - generic("end_tzid", "Europe/Amsterdam"), - generic("summary", "Sync"), - generic("location", "HQ"))); - - String json = EventJsonMapper.mapper().writeValueAsString(genericEvent); - CalendarTimeBasedEvent calendarEvent = - EventJsonMapper.mapper().readValue(json, CalendarTimeBasedEvent.class); - - assertEquals("Europe/Amsterdam", calendarEvent.getStartTzid().orElse("")); - assertEquals("Sync", calendarEvent.getSummary().orElse("")); - } - - // Verifies RSVP events deserialize status, address, and optional event references. - @Test - void shouldDeserializeCalendarRsvpEvent() throws JsonProcessingException { - AddressTag addressTag = - AddressTag.builder() - .kind(Kind.CALENDAR_EVENT.getValue()) - .publicKey(AUTHOR) - .identifierTag(new IdentifierTag("calendar")) - .build(); - - GenericEvent genericEvent = - baseEvent( - Kind.CALENDAR_RSVP_EVENT.getValue(), - List.of( - identifier("rsvp-id"), - addressTag, - generic("status", "accepted"), - new EventTag(EVENT_ID), - new PubKeyTag(AUTHOR), - generic("fb", "free"))); - - String json = EventJsonMapper.mapper().writeValueAsString(genericEvent); - CalendarRsvpEvent calendarEvent = - EventJsonMapper.mapper().readValue(json, CalendarRsvpEvent.class); - - assertEquals(CalendarRsvpEvent.Status.ACCEPTED, calendarEvent.getStatus()); - assertEquals(EVENT_ID, calendarEvent.getEventId().orElse("")); - assertTrue(calendarEvent.getFB().isPresent()); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/ClassifiedListingDecodeTest.java b/nostr-java-event/src/test/java/nostr/event/unit/ClassifiedListingDecodeTest.java deleted file mode 100644 index 3bcd58caa..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/ClassifiedListingDecodeTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package nostr.event.unit; - -import nostr.event.impl.ClassifiedListingEvent; -import nostr.event.json.codec.GenericEventDecoder; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - -public class ClassifiedListingDecodeTest { - String eventJson = - """ - { - "id": "299ab85049a7923e9cd82329c0fa489ca6fd6d21feeeac33543b1237e14a9e07", - "kind": 30402, - "content": "classified content", - "pubkey": "cccd79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984", - "created_at": 1726114798510, - "tags": [ - [ "e", "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346" ], - [ "g", "classified geo-tag-1" ], - [ "t", "classified hash-tag-1111" ], - [ "price", "271.00", "BTC", "1" ], - [ "p", "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984" ], - [ "subject", "classified subject" ], - [ "title", "classified title" ], - [ "published_at", "1726114798510" ], - [ "summary", "classified summary" ], - [ "location", "classified peroulades" ] - ], - "sig": "86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546" - } - """; - - @Test - void testClassifiedListingDecoding() { - assertDoesNotThrow( - () -> new GenericEventDecoder<>(ClassifiedListingEvent.class).decode(eventJson)); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/DecodeTest.java b/nostr-java-event/src/test/java/nostr/event/unit/DecodeTest.java deleted file mode 100644 index 4dcf58c10..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/DecodeTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package nostr.event.unit; - -import com.fasterxml.jackson.core.JsonProcessingException; -import nostr.base.Marker; -import nostr.base.PublicKey; -import nostr.event.BaseMessage; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.message.EventMessage; -import nostr.event.tag.EventTag; -import nostr.event.tag.PubKeyTag; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.fail; - -public class DecodeTest { - - @Test - public void decodeTest() throws JsonProcessingException { - - String json = - "[\"EVENT\",\"temp20230627\",{" - + "\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\"," - + "\"kind\":1," - + "\"pubkey\":\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"," - + "\"created_at\":1687765220,\"content\":\"手順書が間違ってたら作業者は無理だな\",\"tags\":[" - + "[\"e\",\"494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346\",\"\",\"root\"]," - + "[\"p\",\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"]]," - + "\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" - + "}]"; - - BaseMessage message = new BaseMessageDecoder<>().decode(json); - - assertEquals("EVENT", message.getCommand()); - assertInstanceOf(EventMessage.class, message); - - EventMessage eventMessage = (EventMessage) message; - - assertEquals("temp20230627", eventMessage.getSubscriptionId()); - GenericEvent eventImpl = (GenericEvent) eventMessage.getEvent(); - - assertEquals( - "28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a", eventImpl.getId()); - assertEquals(1, eventImpl.getKind()); - assertEquals( - "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984", - eventImpl.getPubKey().toString()); - assertEquals(1687765220, eventImpl.getCreatedAt()); - assertEquals("手順書が間違ってたら作業者は無理だな", eventImpl.getContent()); - assertEquals( - "86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546", - eventImpl.getSignature().toString()); - - List expectedTags = new ArrayList<>(); - EventTag eventTag = - new EventTag("494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"); - eventTag.setRecommendedRelayUrl(""); - eventTag.setMarker(Marker.ROOT); - expectedTags.add(eventTag); - PubKeyTag pubKeyTag = new PubKeyTag(); - pubKeyTag.setPublicKey( - new PublicKey("2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984")); - expectedTags.add(pubKeyTag); - - List actualTags = eventImpl.getTags(); - - for (int i = 0; i < expectedTags.size(); i++) { - BaseTag expected = expectedTags.get(i); - if (expected instanceof EventTag expetedEventTag) { - EventTag actualEventTag = (EventTag) actualTags.get(i); - assertEquals(expetedEventTag.getIdEvent(), actualEventTag.getIdEvent()); - assertEquals( - expetedEventTag.getRecommendedRelayUrl(), actualEventTag.getRecommendedRelayUrl()); - assertEquals(expetedEventTag.getMarker(), actualEventTag.getMarker()); - } else if (expected instanceof PubKeyTag expectedPublicKeyTag) { - PubKeyTag actualPublicKeyTag = (PubKeyTag) actualTags.get(i); - assertEquals( - expectedPublicKeyTag.getPublicKey().toString(), - actualPublicKeyTag.getPublicKey().toString()); - } else { - fail(); - } - } - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/EventTagTest.java b/nostr-java-event/src/test/java/nostr/event/unit/EventTagTest.java deleted file mode 100644 index 6f9e32a13..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/EventTagTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package nostr.event.unit; - -import nostr.base.Marker; -import nostr.event.BaseTag; -import nostr.event.json.codec.BaseTagEncoder; -import nostr.event.tag.EventTag; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Field; -import java.util.List; -import java.util.UUID; -import java.util.function.Predicate; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class EventTagTest { - - @Test - // Verifies that getSupportedFields returns expected fields and values. - void getSupportedFields() { - String eventId = - UUID.randomUUID().toString().concat(UUID.randomUUID().toString()).substring(0, 64); - String recommendedRelayUrl = "ws://localhost:5555"; - - EventTag eventTag = new EventTag(eventId); - eventTag.setMarker(Marker.REPLY); - eventTag.setRecommendedRelayUrl(recommendedRelayUrl); - - List fields = eventTag.getSupportedFields(); - anyFieldNameMatch(fields, field -> field.getName().equals("idEvent")); - anyFieldNameMatch(fields, field -> field.getName().equals("recommendedRelayUrl")); - anyFieldNameMatch(fields, field -> field.getName().equals("marker")); - - anyFieldValueMatch(fields, eventTag, fieldValue -> fieldValue.equals(eventId)); - anyFieldValueMatch( - fields, eventTag, fieldValue -> fieldValue.equalsIgnoreCase(Marker.REPLY.getValue())); - anyFieldValueMatch(fields, eventTag, fieldValue -> fieldValue.equals(recommendedRelayUrl)); - - assertFalse(fields.stream().anyMatch(field -> field.getName().equals("idEventXXX"))); - assertFalse( - fields.stream() - .flatMap(field -> eventTag.getFieldValue(field).stream()) - .anyMatch(fieldValue -> fieldValue.equals(eventId + "x"))); - } - - @Test - // Ensures that a newly created EventTag has a null marker and serializes without it. - void serializeWithoutMarker() throws Exception { - String eventId = "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"; - EventTag eventTag = new EventTag(eventId); - - assertNull(eventTag.getMarker()); - - String json = new BaseTagEncoder(eventTag).encode(); - assertEquals("[\"e\",\"" + eventId + "\"]", json); - - BaseTag decoded = mapper().readValue(json, BaseTag.class); - assertInstanceOf(EventTag.class, decoded); - assertNull(((EventTag) decoded).getMarker()); - } - - @Test - // Checks that an explicit marker is serialized and restored on deserialization. - void serializeWithMarker() throws Exception { - String eventId = "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"; - EventTag eventTag = - EventTag.builder() - .idEvent(eventId) - .recommendedRelayUrl("wss://relay.example.com") - .marker(Marker.ROOT) - .build(); - - String json = new BaseTagEncoder(eventTag).encode(); - assertEquals("[\"e\",\"" + eventId + "\",\"wss://relay.example.com\",\"ROOT\"]", json); - - BaseTag decoded = mapper().readValue(json, BaseTag.class); - assertInstanceOf(EventTag.class, decoded); - EventTag decodedEventTag = (EventTag) decoded; - assertEquals(Marker.ROOT, decodedEventTag.getMarker()); - assertEquals("wss://relay.example.com", decodedEventTag.getRecommendedRelayUrl()); - } - - private static void anyFieldNameMatch(List fields, Predicate predicate) { - assertTrue(fields.stream().anyMatch(predicate)); - } - - private static void anyFieldValueMatch( - List fields, EventTag eventTag, Predicate predicate) { - assertTrue( - fields.stream() - .flatMap(field -> eventTag.getFieldValue(field).stream()) - .anyMatch(predicate)); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/EventWithAddressTagTest.java b/nostr-java-event/src/test/java/nostr/event/unit/EventWithAddressTagTest.java deleted file mode 100644 index 5e0620091..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/EventWithAddressTagTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package nostr.event.unit; - -import com.fasterxml.jackson.core.JsonProcessingException; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.event.BaseMessage; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.message.EventMessage; -import nostr.event.tag.AddressTag; -import nostr.event.tag.IdentifierTag; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.fail; - -public class EventWithAddressTagTest { - @Test - public void decodeTestWithRelay() throws JsonProcessingException { - - String json = - "[\"EVENT\",{\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\"," - + "\"kind\":1," - + "\"pubkey\":\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"," - + "\"created_at\":1687765220,\"content\":\"手順書が間違ってたら作業者は無理だな\",\"tags\":[" - + "[\"a\",\"1:f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75:UUID-1\",\"ws://localhost:8080\"]" - + "],\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" - + "}]"; - - BaseMessage message = new BaseMessageDecoder<>().decode(json); - - assertEquals("EVENT", message.getCommand()); - assertInstanceOf(EventMessage.class, message); - - EventMessage eventMessage = (EventMessage) message; - - GenericEvent eventImpl = (GenericEvent) eventMessage.getEvent(); - - assertEquals( - "28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a", eventImpl.getId()); - assertEquals(1, eventImpl.getKind()); - assertEquals( - "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984", - eventImpl.getPubKey().toString()); - assertEquals(1687765220, eventImpl.getCreatedAt()); - assertEquals("手順書が間違ってたら作業者は無理だな", eventImpl.getContent()); - assertEquals( - "86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546", - eventImpl.getSignature().toString()); - - List expectedTags = new ArrayList<>(); - - Integer kind = 1; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - PublicKey publicKey = new PublicKey(author); - IdentifierTag identifierTag = new IdentifierTag("UUID-1"); - Relay relay = new Relay("ws://localhost:8080"); - - AddressTag addressTag = new AddressTag(); - addressTag.setKind(kind); - addressTag.setPublicKey(publicKey); - addressTag.setIdentifierTag(identifierTag); - addressTag.setRelay(relay); - expectedTags.add(addressTag); - - List actualTags = eventImpl.getTags(); - - for (int i = 0; i < expectedTags.size(); i++) { - BaseTag expected = expectedTags.get(i); - if (expected instanceof AddressTag expectedAddressTag) { - AddressTag actualAddressTag = (AddressTag) actualTags.get(i); - assertEquals(expectedAddressTag.getKind(), actualAddressTag.getKind()); - assertEquals(expectedAddressTag.getPublicKey(), actualAddressTag.getPublicKey()); - assertEquals(expectedAddressTag.getIdentifierTag(), actualAddressTag.getIdentifierTag()); - assertEquals(expectedAddressTag.getRelay(), actualAddressTag.getRelay()); - } else { - fail(); - } - } - } - - @Test - public void decodeTestWithoutRelay() throws JsonProcessingException { - - String json = - "[\"EVENT\",{\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\"," - + "\"kind\":1," - + "\"pubkey\":\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"," - + "\"created_at\":1687765220,\"content\":\"手順書が間違ってたら作業者は無理だな\",\"tags\":[" - + "[\"a\",\"1:f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75:UUID-1\"]" - + "],\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" - + "}]"; - - BaseMessage message = new BaseMessageDecoder<>().decode(json); - - assertEquals("EVENT", message.getCommand()); - assertInstanceOf(EventMessage.class, message); - - EventMessage eventMessage = (EventMessage) message; - - GenericEvent eventImpl = (GenericEvent) eventMessage.getEvent(); - - assertEquals( - "28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a", eventImpl.getId()); - assertEquals(1, eventImpl.getKind()); - assertEquals( - "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984", - eventImpl.getPubKey().toString()); - assertEquals(1687765220, eventImpl.getCreatedAt()); - assertEquals("手順書が間違ってたら作業者は無理だな", eventImpl.getContent()); - assertEquals( - "86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546", - eventImpl.getSignature().toString()); - - List expectedTags = new ArrayList<>(); - - Integer kind = 1; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - PublicKey publicKey = new PublicKey(author); - IdentifierTag identifierTag = new IdentifierTag("UUID-1"); - - AddressTag addressTag = new AddressTag(); - addressTag.setKind(kind); - addressTag.setPublicKey(publicKey); - addressTag.setIdentifierTag(identifierTag); - expectedTags.add(addressTag); - - List actualTags = eventImpl.getTags(); - - for (int i = 0; i < expectedTags.size(); i++) { - BaseTag expected = expectedTags.get(i); - if (expected instanceof AddressTag expectedAddressTag) { - AddressTag actualAddressTag = (AddressTag) actualTags.get(i); - assertEquals(expectedAddressTag.getKind(), actualAddressTag.getKind()); - assertEquals(expectedAddressTag.getPublicKey(), actualAddressTag.getPublicKey()); - assertEquals(expectedAddressTag.getIdentifierTag(), actualAddressTag.getIdentifierTag()); - } else { - fail(); - } - } - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/FiltersDecoderTest.java b/nostr-java-event/src/test/java/nostr/event/unit/FiltersDecoderTest.java deleted file mode 100644 index 2143f260f..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/FiltersDecoderTest.java +++ /dev/null @@ -1,394 +0,0 @@ -package nostr.event.unit; - -import lombok.extern.slf4j.Slf4j; -import nostr.base.GenericTagQuery; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.filter.AddressTagFilter; -import nostr.event.filter.EventFilter; -import nostr.event.filter.Filters; -import nostr.event.filter.GenericTagQueryFilter; -import nostr.event.filter.GeohashTagFilter; -import nostr.event.filter.HashtagTagFilter; -import nostr.event.filter.IdentifierTagFilter; -import nostr.event.filter.KindFilter; -import nostr.event.filter.ReferencedEventFilter; -import nostr.event.filter.ReferencedPublicKeyFilter; -import nostr.event.filter.SinceFilter; -import nostr.event.filter.UntilFilter; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.FiltersDecoder; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.Date; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@Slf4j -public class FiltersDecoderTest { - - @Test - public void testEventFiltersDecoder() { - - String filterKey = "ids"; - String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - - String expected = "{\"" + filterKey + "\":[\"" + eventId + "\"]}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals(new Filters(new EventFilter<>(new GenericEvent(eventId))), decodedFilters); - } - - @Test - public void testMultipleEventFiltersDecoder() { - - String filterKey = "ids"; - String eventId1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String eventId2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - - String joined = String.join("\",\"", eventId1, eventId2); - - String expected = "{\"" + filterKey + "\":[\"" + joined + "\"]}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals( - new Filters( - new EventFilter<>(new GenericEvent(eventId1)), - new EventFilter<>(new GenericEvent(eventId2))), - decodedFilters); - } - - @Test - public void testAddressableTagFiltersDecoder() { - - Integer kind = 1; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String uuidValue1 = "UUID-1"; - - String joined = String.join(":", String.valueOf(kind), author, uuidValue1); - - AddressTag addressTag = new AddressTag(); - addressTag.setKind(kind); - addressTag.setPublicKey(new PublicKey(author)); - addressTag.setIdentifierTag(new IdentifierTag(uuidValue1)); - - String expected = "{\"#a\":[\"" + joined + "\"]}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals(new Filters(new AddressTagFilter<>(addressTag)), decodedFilters); - } - - @Test - public void testMultipleAddressableTagFiltersDecoder() { - - Integer kind1 = 1; - String author1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String uuidValue1 = "UUID-1"; - - Integer kind2 = 1; - String author2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - String uuidValue2 = "UUID-2"; - - AddressTag addressTag1 = new AddressTag(); - addressTag1.setKind(kind1); - addressTag1.setPublicKey(new PublicKey(author1)); - addressTag1.setIdentifierTag(new IdentifierTag(uuidValue1)); - - AddressTag addressTag2 = new AddressTag(); - addressTag2.setKind(kind2); - addressTag2.setPublicKey(new PublicKey(author2)); - addressTag2.setIdentifierTag(new IdentifierTag(uuidValue2)); - - String joined1 = String.join(":", String.valueOf(kind1), author1, uuidValue1); - String joined2 = String.join(":", String.valueOf(kind2), author2, uuidValue2); - - String joined3 = String.join("\",\"", joined1, joined2); - - String expected = "{\"#a\":[\"" + joined3 + "\"]}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals( - new Filters(new AddressTagFilter<>(addressTag1), new AddressTagFilter<>(addressTag2)), - decodedFilters); - } - - @Test - public void testKindFiltersDecoder() { - - String filterKey = KindFilter.FILTER_KEY; - Kind kind = Kind.valueOf(1); - - String expected = "{\"" + filterKey + "\":[" + kind.toString() + "]}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals(new Filters(new KindFilter<>(kind)), decodedFilters); - } - - @Test - public void testMultipleKindFiltersDecoder() { - - String filterKey = KindFilter.FILTER_KEY; - Kind kind1 = Kind.valueOf(1); - Kind kind2 = Kind.valueOf(2); - - String join = String.join(",", kind1.toString(), kind2.toString()); - - String expected = "{\"" + filterKey + "\":[" + join + "]}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals(new Filters(new KindFilter<>(kind1), new KindFilter<>(kind2)), decodedFilters); - } - - @Test - public void testIdentifierTagFilterDecoder() { - - String uuidValue1 = "UUID-1"; - - String expected = "{\"#d\":[\"" + uuidValue1 + "\"]}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals( - new Filters(new IdentifierTagFilter<>(new IdentifierTag(uuidValue1))), decodedFilters); - } - - @Test - public void testMultipleIdentifierTagFilterDecoder() { - - String uuidValue1 = "UUID-1"; - String uuidValue2 = "UUID-2"; - - String joined = String.join("\",\"", uuidValue1, uuidValue2); - String expected = "{\"#d\":[\"" + joined + "\"]}"; - - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals( - new Filters( - new IdentifierTagFilter<>(new IdentifierTag(uuidValue1)), - new IdentifierTagFilter<>(new IdentifierTag(uuidValue2))), - decodedFilters); - } - - @Test - public void testReferencedEventFilterDecoder() { - - String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - - String expected = "{\"#e\":[\"" + eventId + "\"]}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals(new Filters(new ReferencedEventFilter<>(new EventTag(eventId))), decodedFilters); - } - - @Test - public void testMultipleReferencedEventFilterDecoder() { - - String eventId1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String eventId2 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - - String joined = String.join("\",\"", eventId1, eventId2); - String expected = "{\"#e\":[\"" + joined + "\"]}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals( - new Filters( - new ReferencedEventFilter<>(new EventTag(eventId1)), - new ReferencedEventFilter<>(new EventTag(eventId2))), - decodedFilters); - } - - @Test - public void testReferencedPublicKeyFilterDecofder() { - - String pubkeyString = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - - String expected = "{\"#p\":[\"" + pubkeyString + "\"]}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals( - new Filters(new ReferencedPublicKeyFilter<>(new PubKeyTag(new PublicKey(pubkeyString)))), - decodedFilters); - } - - @Test - public void testMultipleReferencedPublicKeyFilterDecoder() { - - String pubkeyString1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String pubkeyString2 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - - String joined = String.join("\",\"", pubkeyString1, pubkeyString2); - String expected = "{\"#p\":[\"" + joined + "\"]}"; - - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals( - new Filters( - new ReferencedPublicKeyFilter<>(new PubKeyTag(new PublicKey(pubkeyString1))), - new ReferencedPublicKeyFilter<>(new PubKeyTag(new PublicKey(pubkeyString2)))), - decodedFilters); - } - - @Test - public void testGeohashTagFiltersDecoder() { - - String geohashKey = "#g"; - String geohashValue = "2vghde"; - String reqJsonWithCustomTagQueryFilterToDecode = - "{\"" + geohashKey + "\":[\"" + geohashValue + "\"]}"; - - Filters decodedFilters = new FiltersDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - assertEquals(new Filters(new GeohashTagFilter<>(new GeohashTag(geohashValue))), decodedFilters); - } - - @Test - public void testMultipleGeohashTagFiltersDecoder() { - - String geohashKey = "#g"; - String geohashValue1 = "2vghde"; - String geohashValue2 = "3abcde"; - String reqJsonWithCustomTagQueryFilterToDecode = - "{\"" + geohashKey + "\":[\"" + geohashValue1 + "\",\"" + geohashValue2 + "\"]}"; - - Filters decodedFilters = new FiltersDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - assertEquals( - new Filters( - new GeohashTagFilter<>(new GeohashTag(geohashValue1)), - new GeohashTagFilter<>(new GeohashTag(geohashValue2))), - decodedFilters); - } - - @Test - public void testHashtagTagFiltersDecoder() { - - String hashtagKey = "#t"; - String hashtagValue = "2vghde"; - String reqJsonWithCustomTagQueryFilterToDecode = - "{\"" + hashtagKey + "\":[\"" + hashtagValue + "\"]}"; - - Filters decodedFilters = new FiltersDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - assertEquals(new Filters(new HashtagTagFilter<>(new HashtagTag(hashtagValue))), decodedFilters); - } - - @Test - public void testMultipleHashtagTagFiltersDecoder() { - - String hashtagKey = "#t"; - String hashtagValue1 = "2vghde"; - String hashtagValue2 = "3abcde"; - String reqJsonWithCustomTagQueryFilterToDecode = - "{\"" + hashtagKey + "\":[\"" + hashtagValue1 + "\",\"" + hashtagValue2 + "\"]}"; - - Filters decodedFilters = new FiltersDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - assertEquals( - new Filters( - new HashtagTagFilter<>(new HashtagTag(hashtagValue1)), - new HashtagTagFilter<>(new HashtagTag(hashtagValue2))), - decodedFilters); - } - - @Test - public void testGenericTagFiltersDecoder() { - - String customTagKey = "#b"; - String customTagValue = "2vghde"; - String reqJsonWithCustomTagQueryFilterToDecode = - "{\"" + customTagKey + "\":[\"" + customTagValue + "\"]}"; - - Filters decodedFilters = new FiltersDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - assertEquals( - new Filters(new GenericTagQueryFilter<>(new GenericTagQuery(customTagKey, customTagValue))), - decodedFilters); - } - - @Test - public void testMultipleGenericTagFiltersDecoder() { - - String customTagKey = "#b"; - String customTagValue1 = "2vghde"; - String customTagValue2 = "3abcde"; - - String reqJsonWithCustomTagQueryFilterToDecode = - "{\"" + customTagKey + "\":[\"" + customTagValue1 + "\",\"" + customTagValue2 + "\"]}"; - - Filters decodedFilters = new FiltersDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - assertEquals( - new Filters( - new GenericTagQueryFilter<>(new GenericTagQuery(customTagKey, customTagValue1)), - new GenericTagQueryFilter<>(new GenericTagQuery(customTagKey, customTagValue2))), - decodedFilters); - } - - @Test - public void testSinceFiltersDecoder() { - - Long since = Date.from(Instant.now()).getTime(); - - String expected = "{\"since\":" + since + "}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals(new Filters(new SinceFilter(since)), decodedFilters); - } - - @Test - public void testUntilFiltersDecoder() { - - Long until = Date.from(Instant.now()).getTime(); - - String expected = "{\"until\":" + until + "}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals(new Filters(new UntilFilter(until)), decodedFilters); - } - - @Test - public void testDecoderMultipleFilterTypes() { - - String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - Kind kind = Kind.valueOf(1); - Long since = Date.from(Instant.now()).getTime(); - - String expected = - "{\"ids\":[\"" - + eventId - + "\"],\"kinds\":[" - + kind.toString() - + "],\"since\":" - + since - + "}"; - Filters decodedFilters = new FiltersDecoder().decode(expected); - - assertEquals( - new Filters( - new EventFilter<>(new GenericEvent(eventId)), - new KindFilter<>(kind), - new SinceFilter(since)), - decodedFilters); - } - - @Test - public void testFailedAddressableTagMalformedSeparator() { - - Integer kind = 1; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String uuidValue1 = "UUID-1"; - - String malformedJoin = String.join(",", String.valueOf(kind), author, uuidValue1); - String expected = "{\"#a\":[\"" + malformedJoin + "\"]}"; - - assertThrows(ArrayIndexOutOfBoundsException.class, () -> new FiltersDecoder().decode(expected)); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/FiltersEncoderTest.java b/nostr-java-event/src/test/java/nostr/event/unit/FiltersEncoderTest.java deleted file mode 100644 index 30654903b..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/FiltersEncoderTest.java +++ /dev/null @@ -1,389 +0,0 @@ -package nostr.event.unit; - -import lombok.extern.slf4j.Slf4j; -import nostr.base.GenericTagQuery; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.filter.AddressTagFilter; -import nostr.event.filter.AuthorFilter; -import nostr.event.filter.EventFilter; -import nostr.event.filter.Filters; -import nostr.event.filter.GenericTagQueryFilter; -import nostr.event.filter.GeohashTagFilter; -import nostr.event.filter.HashtagTagFilter; -import nostr.event.filter.IdentifierTagFilter; -import nostr.event.filter.KindFilter; -import nostr.event.filter.ReferencedEventFilter; -import nostr.event.filter.ReferencedPublicKeyFilter; -import nostr.event.filter.SinceFilter; -import nostr.event.filter.UntilFilter; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.FiltersEncoder; -import nostr.event.message.ReqMessage; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.IdentifierTag; -import nostr.event.tag.PubKeyTag; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.Date; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@Slf4j -public class FiltersEncoderTest { - - @Test - public void testEventFilterEncoder() { - - String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - - FiltersEncoder encoder = - new FiltersEncoder(new Filters(new EventFilter<>(new GenericEvent(eventId)))); - - String encodedFilters = encoder.encode(); - assertEquals("{\"ids\":[\"" + eventId + "\"]}", encodedFilters); - } - - @Test - public void testMultipleEventFilterEncoder() { - - String eventId1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String eventId2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - - FiltersEncoder encoder = - new FiltersEncoder( - new Filters( - new EventFilter<>(new GenericEvent(eventId1)), - new EventFilter<>(new GenericEvent(eventId2)))); - String encodedFilters = encoder.encode(); - - String events = String.join("\",\"", eventId1, eventId2); - assertEquals("{\"ids\":[\"" + events + "\"]}", encodedFilters); - } - - @Test - public void testKindFiltersEncoder() { - - Kind kind = Kind.valueOf(1); - FiltersEncoder encoder = new FiltersEncoder(new Filters(new KindFilter<>(kind))); - - String encodedFilters = encoder.encode(); - assertEquals("{\"kinds\":[" + kind.toString() + "]}", encodedFilters); - } - - @Test - public void testAuthorFilterEncoder() { - - String pubKeyString = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - FiltersEncoder encoder = - new FiltersEncoder(new Filters(new AuthorFilter<>(new PublicKey(pubKeyString)))); - - String encodedFilters = encoder.encode(); - assertEquals("{\"authors\":[\"" + pubKeyString + "\"]}", encodedFilters); - } - - @Test - public void testMultipleAuthorFilterEncoder() { - - String pubKeyString1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String pubKeyString2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - FiltersEncoder encoder = - new FiltersEncoder( - new Filters( - List.of( - new AuthorFilter<>(new PublicKey(pubKeyString1)), - new AuthorFilter<>(new PublicKey(pubKeyString2))))); - - String encodedFilters = encoder.encode(); - String authorPubKeys = String.join("\",\"", pubKeyString1, pubKeyString2); - - assertEquals("{\"authors\":[\"" + authorPubKeys + "\"]}", encodedFilters); - } - - @Test - public void testMultipleKindFiltersEncoder() { - - Kind kind1 = Kind.valueOf(1); - Kind kind2 = Kind.valueOf(2); - - FiltersEncoder encoder = - new FiltersEncoder(new Filters(List.of(new KindFilter<>(kind1), new KindFilter<>(kind2)))); - - String encodedFilters = encoder.encode(); - String kinds = String.join(",", kind1.toString(), kind2.toString()); - assertEquals("{\"kinds\":[" + kinds + "]}", encodedFilters); - } - - @Test - public void testAddressableTagFilterEncoder() { - - Integer kind = 1; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String uuidValue1 = "UUID-1"; - - AddressTag addressTag = new AddressTag(); - addressTag.setKind(kind); - addressTag.setPublicKey(new PublicKey(author)); - addressTag.setIdentifierTag(new IdentifierTag(uuidValue1)); - - FiltersEncoder encoder = new FiltersEncoder(new Filters(new AddressTagFilter<>(addressTag))); - String encodedFilters = encoder.encode(); - String addressableTag = String.join(":", String.valueOf(kind), author, uuidValue1); - - assertEquals("{\"#a\":[\"" + addressableTag + "\"]}", encodedFilters); - } - - @Test - public void testIdentifierTagFilterEncoder() { - - String uuidValue1 = "UUID-1"; - - FiltersEncoder encoder = - new FiltersEncoder(new Filters(new IdentifierTagFilter<>(new IdentifierTag(uuidValue1)))); - String encodedFilters = encoder.encode(); - assertEquals("{\"#d\":[\"" + uuidValue1 + "\"]}", encodedFilters); - } - - @Test - public void testMultipleIdentifierTagFilterEncoder() { - - String uuidValue1 = "UUID-1"; - String uuidValue2 = "UUID-2"; - - FiltersEncoder encoder = - new FiltersEncoder( - new Filters( - List.of( - new IdentifierTagFilter<>(new IdentifierTag(uuidValue1)), - new IdentifierTagFilter<>(new IdentifierTag(uuidValue2))))); - - String encodedFilters = encoder.encode(); - String dTags = String.join("\",\"", uuidValue1, uuidValue2); - assertEquals("{\"#d\":[\"" + dTags + "\"]}", encodedFilters); - } - - @Test - public void testReferencedEventFilterEncoder() { - - String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - - FiltersEncoder encoder = - new FiltersEncoder(new Filters(new ReferencedEventFilter<>(new EventTag(eventId)))); - String encodedFilters = encoder.encode(); - assertEquals("{\"#e\":[\"" + eventId + "\"]}", encodedFilters); - } - - @Test - public void testMultipleReferencedEventFilterEncoder() { - - String eventId1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String eventId2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - - FiltersEncoder encoder = - new FiltersEncoder( - new Filters( - List.of( - new ReferencedEventFilter<>(new EventTag(eventId1)), - new ReferencedEventFilter<>(new EventTag(eventId2))))); - - String encodedFilters = encoder.encode(); - String eventIds = String.join("\",\"", eventId1, eventId2); - assertEquals("{\"#e\":[\"" + eventIds + "\"]}", encodedFilters); - } - - @Test - public void testReferencedPublicKeyFilterEncoder() { - - String pubKeyString = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - - FiltersEncoder encoder = - new FiltersEncoder( - new Filters( - new ReferencedPublicKeyFilter<>(new PubKeyTag(new PublicKey(pubKeyString))))); - - String encodedFilters = encoder.encode(); - assertEquals("{\"#p\":[\"" + pubKeyString + "\"]}", encodedFilters); - } - - @Test - public void testMultipleReferencedPublicKeyFilterEncoder() { - - String pubKeyString1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String pubKeyString2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - - FiltersEncoder encoder = - new FiltersEncoder( - new Filters( - new ReferencedPublicKeyFilter<>(new PubKeyTag(new PublicKey(pubKeyString1))), - new ReferencedPublicKeyFilter<>(new PubKeyTag(new PublicKey(pubKeyString2))))); - - String encodedFilters = encoder.encode(); - String pubKeyTags = String.join("\",\"", pubKeyString1, pubKeyString2); - assertEquals("{\"#p\":[\"" + pubKeyTags + "\"]}", encodedFilters); - } - - @Test - public void testSingleGeohashTagFiltersEncoder() { - - String new_geohash = "2vghde"; - - FiltersEncoder encoder = - new FiltersEncoder(new Filters(new GeohashTagFilter<>(new GeohashTag(new_geohash)))); - - String encodedFilters = encoder.encode(); - assertEquals("{\"#g\":[\"2vghde\"]}", encodedFilters); - } - - @Test - public void testMultipleGeohashTagFiltersEncoder() { - - String geohashValue1 = "2vghde"; - String geohashValue2 = "3abcde"; - - FiltersEncoder encoder = - new FiltersEncoder( - new Filters( - new GeohashTagFilter<>(new GeohashTag(geohashValue1)), - new GeohashTagFilter<>(new GeohashTag(geohashValue2)))); - - String encodedFilters = encoder.encode(); - assertEquals("{\"#g\":[\"2vghde\",\"3abcde\"]}", encodedFilters); - } - - @Test - public void testSingleHashtagTagFiltersEncoder() { - - String hashtag_target = "2vghde"; - - FiltersEncoder encoder = - new FiltersEncoder(new Filters(new HashtagTagFilter<>(new HashtagTag(hashtag_target)))); - - String encodedFilters = encoder.encode(); - assertEquals("{\"#t\":[\"2vghde\"]}", encodedFilters); - } - - @Test - public void testMultipleHashtagTagFiltersEncoder() { - - String hashtagValue1 = "2vghde"; - String hashtagValue2 = "3abcde"; - - FiltersEncoder encoder = - new FiltersEncoder( - new Filters( - new HashtagTagFilter<>(new HashtagTag(hashtagValue1)), - new HashtagTagFilter<>(new HashtagTag(hashtagValue2)))); - - String encodedFilters = encoder.encode(); - assertEquals("{\"#t\":[\"2vghde\",\"3abcde\"]}", encodedFilters); - } - - @Test - public void testSingleCustomGenericTagQueryFiltersEncoder() { - - String customKey = "#b"; - String customValue = "2vghde"; - - FiltersEncoder encoder = - new FiltersEncoder( - new Filters(new GenericTagQueryFilter<>(new GenericTagQuery(customKey, customValue)))); - - String encodedFilters = encoder.encode(); - assertEquals("{\"#b\":[\"2vghde\"]}", encodedFilters); - } - - @Test - public void testMultipleCustomGenericTagQueryFiltersEncoder() { - - String customKey = "#b"; - String customValue1 = "2vghde"; - String customValue2 = "3abcde"; - - FiltersEncoder encoder = - new FiltersEncoder( - new Filters( - new GenericTagQueryFilter<>(new GenericTagQuery(customKey, customValue1)), - new GenericTagQueryFilter<>(new GenericTagQuery(customKey, customValue2)))); - - String encodedFilters = encoder.encode(); - assertEquals("{\"#b\":[\"2vghde\",\"3abcde\"]}", encodedFilters); - } - - @Test - public void testMultipleAddressableTagFilterEncoder() { - - Integer kind = 1; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String uuidValue1 = "UUID-1"; - String uuidValue2 = "UUID-2"; - - String addressableTag1 = String.join(":", String.valueOf(kind), author, uuidValue1); - String addressableTag2 = String.join(":", String.valueOf(kind), author, uuidValue2); - - AddressTag addressTag1 = new AddressTag(); - addressTag1.setKind(kind); - addressTag1.setPublicKey(new PublicKey(author)); - addressTag1.setIdentifierTag(new IdentifierTag(uuidValue1)); - - AddressTag addressTag2 = new AddressTag(); - addressTag2.setKind(kind); - addressTag2.setPublicKey(new PublicKey(author)); - addressTag2.setIdentifierTag(new IdentifierTag(uuidValue2)); - - FiltersEncoder encoder = - new FiltersEncoder( - new Filters(new AddressTagFilter<>(addressTag1), new AddressTagFilter<>(addressTag2))); - - String encoded = encoder.encode(); - String addressableTags = String.join("\",\"", addressableTag1, addressableTag2); - assertEquals("{\"#a\":[\"" + addressableTags + "\"]}", encoded); - } - - @Test - public void testSinceFiltersEncoder() { - - Long since = Date.from(Instant.now()).getTime(); - - FiltersEncoder encoder = new FiltersEncoder(new Filters(new SinceFilter(since))); - String encodedFilters = encoder.encode(); - assertEquals("{\"since\":" + since + "}", encodedFilters); - } - - @Test - public void testUntilFiltersEncoder() { - - Long until = Date.from(Instant.now()).getTime(); - - FiltersEncoder encoder = new FiltersEncoder(new Filters(new UntilFilter(until))); - String encodedFilters = encoder.encode(); - assertEquals("{\"until\":" + until + "}", encodedFilters); - } - - @Test - public void testReqMessageEmptyFilters() { - String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; - - assertThrows( - IllegalArgumentException.class, - () -> new ReqMessage(subscriptionId, new Filters(List.of()))); - } - - @Test - public void testReqMessageCustomGenericTagFilter() { - String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; - - assertDoesNotThrow( - () -> - new ReqMessage( - subscriptionId, - new Filters( - new GenericTagQueryFilter<>(new GenericTagQuery("some-tag", "some-value"))))); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/FiltersTest.java b/nostr-java-event/src/test/java/nostr/event/unit/FiltersTest.java index a38f05e24..d943a0d58 100644 --- a/nostr-java-event/src/test/java/nostr/event/unit/FiltersTest.java +++ b/nostr-java-event/src/test/java/nostr/event/unit/FiltersTest.java @@ -1,46 +1,43 @@ package nostr.event.unit; -import nostr.base.Kind; -import nostr.event.filter.Filterable; +import nostr.event.filter.EventFilter; import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; import org.junit.jupiter.api.Test; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; public class FiltersTest { @Test - void missingTypeReturnsEmptyList() { - Filters filters = new Filters(new KindFilter<>(Kind.valueOf(1))); - assertTrue(filters.getFilterByType("unknown").isEmpty()); + void emptyFiltersListThrows() { + assertThrows(IllegalArgumentException.class, () -> new Filters(new java.util.ArrayList<>())); } @Test - void setLimitRequiresPositive() { - Filters filters = new Filters(new KindFilter<>(Kind.valueOf(1))); - assertThrows(IllegalArgumentException.class, () -> filters.setLimit(0)); - assertThrows(IllegalArgumentException.class, () -> filters.setLimit(-5)); - filters.setLimit(1); - assertEquals(1, filters.getLimit()); + void singleFilterWrapped() { + EventFilter filter = EventFilter.builder().kind(1).build(); + Filters filters = new Filters(filter); + assertEquals(1, filters.getFilters().size()); } @Test - void nullFilterKeyThrows() throws Exception { - Map> map = new HashMap<>(); - map.put(null, List.of(new KindFilter<>(Kind.valueOf(1)))); - Constructor constructor = Filters.class.getDeclaredConstructor(Map.class); - constructor.setAccessible(true); - InvocationTargetException ex = - assertThrows(InvocationTargetException.class, () -> constructor.newInstance(map)); - assertEquals("Filter key for filterable [kinds] is not defined", ex.getCause().getMessage()); + void eventFilterBuilderBasic() { + EventFilter filter = EventFilter.builder() + .kind(1) + .kind(7) + .author("abc123") + .since(1000L) + .until(2000L) + .limit(10) + .addTagFilter("e", "eventid1") + .build(); + + assertEquals(java.util.List.of(1, 7), filter.getKinds()); + assertEquals(java.util.List.of("abc123"), filter.getAuthors()); + assertEquals(1000L, filter.getSince()); + assertEquals(2000L, filter.getUntil()); + assertEquals(10, filter.getLimit()); + assertEquals(java.util.List.of("eventid1"), filter.getTagFilters().get("e")); } } diff --git a/nostr-java-event/src/test/java/nostr/event/unit/GenericEventBuilderTest.java b/nostr-java-event/src/test/java/nostr/event/unit/GenericEventBuilderTest.java index eedf4e818..8cade97ba 100644 --- a/nostr-java-event/src/test/java/nostr/event/unit/GenericEventBuilderTest.java +++ b/nostr-java-event/src/test/java/nostr/event/unit/GenericEventBuilderTest.java @@ -1,6 +1,6 @@ package nostr.event.unit; -import nostr.base.Kind; +import nostr.base.Kinds; import nostr.base.PublicKey; import nostr.event.BaseTag; import nostr.event.impl.GenericEvent; @@ -17,16 +17,15 @@ class GenericEventBuilderTest { private static final PublicKey PUBLIC_KEY = new PublicKey("f6f8a2d4c6e8b0a1f2d3c4b5a6e7d8c9b0a1c2d3e4f5a6b7c8d9e0f1a2b3c4d5"); - // Ensures the builder populates core fields when provided with a standard Kind enum. @Test - void shouldBuildGenericEventWithStandardKind() { + void shouldBuildGenericEventWithKind() { BaseTag titleTag = BaseTag.create("title", "Builder test"); GenericEvent event = GenericEvent.builder() .id(HEX_ID) .pubKey(PUBLIC_KEY) - .kind(Kind.TEXT_NOTE) + .kind(Kinds.TEXT_NOTE) .tags(List.of(titleTag)) .content("hello world") .createdAt(1_700_000_000L) @@ -34,20 +33,19 @@ void shouldBuildGenericEventWithStandardKind() { assertEquals(HEX_ID, event.getId()); assertEquals(PUBLIC_KEY, event.getPubKey()); - assertEquals(Kind.TEXT_NOTE.getValue(), event.getKind()); + assertEquals(Kinds.TEXT_NOTE, event.getKind()); assertEquals("hello world", event.getContent()); assertEquals(1_700_000_000L, event.getCreatedAt()); assertEquals(1, event.getTags().size()); assertEquals("title", event.getTags().get(0).getCode()); } - // Ensures custom kinds outside the enum can be provided through the builder's customKind field. @Test void shouldBuildGenericEventWithCustomKind() { GenericEvent event = GenericEvent.builder() .pubKey(PUBLIC_KEY) - .customKind(65_535) + .kind(65_535) .tags(List.of()) .content("") .createdAt(1L) @@ -56,7 +54,6 @@ void shouldBuildGenericEventWithCustomKind() { assertEquals(65_535, event.getKind()); } - // Ensures the builder fails fast when neither an enum nor custom kind is supplied. @Test void shouldRequireKindWhenBuilding() { assertThrows( diff --git a/nostr-java-event/src/test/java/nostr/event/unit/GenericTagTest.java b/nostr-java-event/src/test/java/nostr/event/unit/GenericTagTest.java index c3f8d062e..069e02a3f 100644 --- a/nostr-java-event/src/test/java/nostr/event/unit/GenericTagTest.java +++ b/nostr-java-event/src/test/java/nostr/event/unit/GenericTagTest.java @@ -19,6 +19,6 @@ public void testCreateGenericFallback() { assertInstanceOf(GenericTag.class, tag); assertEquals(code, tag.getCode()); - assertEquals("test-value", ((GenericTag) tag).getAttributes().get(0).value()); + assertEquals("test-value", ((GenericTag) tag).getParams().get(0)); } } diff --git a/nostr-java-event/src/test/java/nostr/event/unit/JsonContentValidationTest.java b/nostr-java-event/src/test/java/nostr/event/unit/JsonContentValidationTest.java deleted file mode 100644 index 2359f2dd4..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/JsonContentValidationTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package nostr.event.unit; - -import nostr.base.PublicKey; -import nostr.event.impl.ChannelCreateEvent; -import nostr.event.impl.CreateOrUpdateProductEvent; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class JsonContentValidationTest { - - private static final PublicKey PUBKEY = - new PublicKey("56adf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6f"); - - private static class TestChannelCreateEvent extends ChannelCreateEvent { - public TestChannelCreateEvent(PublicKey pk, String content) { - super(pk, content); - } - - public void callValidateContent() { - super.validateContent(); - } - } - - private static class TestProductEvent extends CreateOrUpdateProductEvent { - public TestProductEvent(PublicKey pk, List tags, String content) { - super(pk, tags, content); - } - - public void callValidateContent() { - super.validateContent(); - } - } - - @Test - void channelCreateInvalidJson() { - TestChannelCreateEvent event = new TestChannelCreateEvent(PUBKEY, "{invalid"); - assertThrows(AssertionError.class, event::callValidateContent); - } - - @Test - void channelCreateMissingFields() { - String json = "{\"name\":\"test\"}"; // missing about and picture - TestChannelCreateEvent event = new TestChannelCreateEvent(PUBKEY, json); - assertThrows(AssertionError.class, event::callValidateContent); - } - - @Test - void productEventInvalidJson() { - TestProductEvent event = new TestProductEvent(PUBKEY, List.of(), "{invalid"); - assertThrows(AssertionError.class, event::callValidateContent); - } - - @Test - void productEventMissingFields() { - String json = "{\"id\":\"123\",\"currency\":\"USD\",\"price\":10}"; // missing name - TestProductEvent event = new TestProductEvent(PUBKEY, List.of(), json); - assertThrows(AssertionError.class, event::callValidateContent); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/KindMappingTest.java b/nostr-java-event/src/test/java/nostr/event/unit/KindMappingTest.java deleted file mode 100644 index 316bf5c1b..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/KindMappingTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package nostr.event.unit; - -import nostr.base.Kind; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class KindMappingTest { - @Test - void testKindValueOf() { - assertEquals("1", Kind.valueOf(1).toString()); - } - - @Test - void testKindName() { - assertEquals("text_note", Kind.valueOf(1).getName()); - } - - @Test - void testKindUndefinedName() { - assertThrows(IllegalArgumentException.class, () -> Kind.valueOf(9999999)); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/Nip60FilterJsonTest.java b/nostr-java-event/src/test/java/nostr/event/unit/Nip60FilterJsonTest.java index 368e82b1f..cadac1e1b 100644 --- a/nostr-java-event/src/test/java/nostr/event/unit/Nip60FilterJsonTest.java +++ b/nostr-java-event/src/test/java/nostr/event/unit/Nip60FilterJsonTest.java @@ -1,12 +1,7 @@ package nostr.event.unit; -import nostr.base.GenericTagQuery; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.filter.AuthorFilter; -import nostr.event.filter.Filters; -import nostr.event.filter.GenericTagQueryFilter; -import nostr.event.filter.KindFilter; +import nostr.base.Kinds; +import nostr.event.filter.EventFilter; import nostr.event.json.codec.FiltersEncoder; import nostr.event.message.ReqMessage; import org.junit.jupiter.api.Test; @@ -25,36 +20,31 @@ class Nip60FilterJsonTest { @Test void testNip60WalletFilterJsonFormat() { - // First filter: wallet-related kinds + author - Filters filter = new Filters(List.of( - new KindFilter<>(Kind.WALLET), - new KindFilter<>(Kind.WALLET_UNSPENT_PROOF), - new KindFilter<>(Kind.WALLET_TX_HISTORY), - new AuthorFilter<>(new PublicKey(TEST_PUBKEY)) - )); + EventFilter filter = EventFilter.builder() + .kind(Kinds.WALLET) + .kind(Kinds.WALLET_UNSPENT_PROOF) + .kind(Kinds.WALLET_TX_HISTORY) + .author(TEST_PUBKEY) + .build(); String filterJson = new FiltersEncoder(filter).encode(); assertNotNull(filterJson); - // Verify kinds are present with correct NIP-60 values assertTrue(filterJson.contains("17375"), "Should contain WALLET kind (17375)"); assertTrue(filterJson.contains("7375"), "Should contain WALLET_UNSPENT_PROOF kind (7375)"); assertTrue(filterJson.contains("7376"), "Should contain WALLET_TX_HISTORY kind (7376)"); - // Verify author pubkey is present assertTrue(filterJson.contains(TEST_PUBKEY), "Should contain author pubkey"); - // Verify JSON structure has expected fields assertTrue(filterJson.contains("\"kinds\""), "Should have 'kinds' field"); assertTrue(filterJson.contains("\"authors\""), "Should have 'authors' field"); } @Test void testNip60ProofFilterWithTagQuery() { - // Filter with kind + #a tag query (wallet proof lookup by wallet reference) - String walletRef = Kind.WALLET.getValue() + ":" + TEST_PUBKEY; - Filters filter = new Filters(List.of( - new KindFilter<>(Kind.WALLET_UNSPENT_PROOF), - new GenericTagQueryFilter<>(new GenericTagQuery("#a", walletRef)) - )); + String walletRef = Kinds.WALLET + ":" + TEST_PUBKEY; + EventFilter filter = EventFilter.builder() + .kind(Kinds.WALLET_UNSPENT_PROOF) + .addTagFilter("a", walletRef) + .build(); String filterJson = new FiltersEncoder(filter).encode(); @@ -66,21 +56,20 @@ void testNip60ProofFilterWithTagQuery() { @Test void testNip60ReqMessageFormat() { - Filters walletFilter = new Filters(List.of( - new KindFilter<>(Kind.WALLET), - new AuthorFilter<>(new PublicKey(TEST_PUBKEY)) - )); + EventFilter walletFilter = EventFilter.builder() + .kind(Kinds.WALLET) + .author(TEST_PUBKEY) + .build(); - Filters proofFilter = new Filters(List.of( - new KindFilter<>(Kind.WALLET_UNSPENT_PROOF), - new AuthorFilter<>(new PublicKey(TEST_PUBKEY)) - )); + EventFilter proofFilter = EventFilter.builder() + .kind(Kinds.WALLET_UNSPENT_PROOF) + .author(TEST_PUBKEY) + .build(); ReqMessage req = new ReqMessage("nip60-sync", List.of(walletFilter, proofFilter)); String reqJson = req.encode(); assertNotNull(reqJson); - // REQ message format: ["REQ", , , , ...] assertTrue(reqJson.startsWith("[\"REQ\""), "Should start with REQ command"); assertTrue(reqJson.contains("\"nip60-sync\""), "Should contain subscription ID"); assertTrue(reqJson.contains("17375"), "Should contain WALLET kind"); diff --git a/nostr-java-event/src/test/java/nostr/event/unit/PriceTagTest.java b/nostr-java-event/src/test/java/nostr/event/unit/PriceTagTest.java deleted file mode 100644 index c572abda8..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/PriceTagTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package nostr.event.unit; - -import nostr.event.tag.PriceTag; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Field; -import java.math.BigDecimal; -import java.util.List; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class PriceTagTest { - private static final BigDecimal aVal = new BigDecimal(10.000); - private static final BigDecimal bVal = new BigDecimal(10.00); - private static final BigDecimal cVal = new BigDecimal(10.0); - private static final BigDecimal dVal = new BigDecimal(10.); - private static final BigDecimal eVal = new BigDecimal(10); - - private static final BigDecimal aString = new BigDecimal("10.000"); - private static final BigDecimal bString = new BigDecimal("10.00"); - private static final BigDecimal cString = new BigDecimal("10.0"); - private static final BigDecimal dString = new BigDecimal("10."); - private static final BigDecimal eString = new BigDecimal("10"); - - private static final String BTC = "BTC"; - private static final String freq = "femptosecond"; - - @Test - void valueParameterCompare() { - List list = - Stream.of(aVal, bVal, cVal, dVal, eVal) - .map(bigDecimal -> new PriceTag(bigDecimal, BTC, freq)) - .toList(); - assertTrue(list.stream().allMatch(list.getFirst()::equals)); - } - - @Test - void stringParameterCompare() { - List list = - Stream.of(aString, bString, cString, dString, eString) - .map(bigDecimal -> new PriceTag(bigDecimal, BTC, freq)) - .toList(); - assertTrue(list.stream().allMatch(list.getFirst()::equals)); - } - - @Test - void failure() { - List priceTags = - List.of( - new PriceTag(new BigDecimal("1"), BTC, freq), - new PriceTag(new BigDecimal("01"), BTC, freq), - new PriceTag(new BigDecimal("001"), BTC, freq), - new PriceTag(new BigDecimal(1), BTC, freq), - new PriceTag(new BigDecimal(01), BTC, freq), - new PriceTag(new BigDecimal(001), BTC, freq)); - List list = - Stream.of(aString, bString, cString, dString, eString) - .map(bigDecimal -> new PriceTag(bigDecimal, BTC, freq)) - .toList(); - assertTrue(list.stream().noneMatch(priceTags::equals)); - } - - @Test - void getSupportedFields() { - PriceTag priceTag = new PriceTag(new BigDecimal(11111), "BTC", "NANOSECONDS"); - assertDoesNotThrow( - () -> { - List list = priceTag.getSupportedFields().stream().toList(); - assertTrue( - List.of("number", "currency", "frequency") - .containsAll(list.stream().map(Field::getName).toList())); - assertTrue( - List.of("java.math.BigDecimal", "java.lang.String") - .containsAll( - list.stream().map(field -> field.getAnnotatedType().toString()).toList())); - }); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/ProductSerializationTest.java b/nostr-java-event/src/test/java/nostr/event/unit/ProductSerializationTest.java deleted file mode 100644 index 14ce86c30..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/ProductSerializationTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package nostr.event.unit; - -import com.fasterxml.jackson.databind.JsonNode; -import nostr.event.entities.Product; -import nostr.event.entities.Stall; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class ProductSerializationTest { - - @Test - void specSerialization() throws Exception { - Product.Spec spec = new Product.Spec("color", "blue"); - String json = mapper().writeValueAsString(spec); - JsonNode node = mapper().readTree(json); - assertEquals("color", node.get("key").asText()); - assertEquals("blue", node.get("value").asText()); - } - - @Test - void productSerialization() throws Exception { - Product product = new Product(); - Stall stall = new Stall(); - product.setStall(stall); - product.setName("item"); - product.setCurrency("USD"); - product.setPrice(1f); - product.setQuantity(1); - product.setSpecs(List.of(new Product.Spec("size", "M"))); - - JsonNode node = mapper().readTree(product.value()); - - assertTrue(node.has("id")); - assertEquals("item", node.get("name").asText()); - assertEquals("USD", node.get("currency").asText()); - assertEquals(1f, node.get("price").floatValue()); - assertEquals(1, node.get("quantity").asInt()); - assertTrue(node.has("stall")); - assertEquals(stall.getId(), node.get("stall").get("id").asText()); - assertTrue(node.has("specs")); - assertTrue(node.get("specs").isArray()); - JsonNode specNode = node.get("specs").get(0); - assertEquals("size", specNode.get("key").asText()); - assertEquals("M", specNode.get("value").asText()); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/PubkeyTagTest.java b/nostr-java-event/src/test/java/nostr/event/unit/PubkeyTagTest.java deleted file mode 100644 index ee4573d65..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/PubkeyTagTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package nostr.event.unit; - -import nostr.base.PublicKey; -import nostr.event.tag.PubKeyTag; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Field; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; - -class PubkeyTagTest { - - @Test - void getSupportedFields() { - String sha256 = "56adf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6f"; - PubKeyTag pubKeyTag = new PubKeyTag(new PublicKey(sha256)); - assertDoesNotThrow( - () -> { - Field field = pubKeyTag.getSupportedFields().stream().findFirst().orElseThrow(); - assertEquals("nostr.base.PublicKey", field.getAnnotatedType().toString()); - assertEquals("publicKey", field.getName()); - assertEquals(sha256, pubKeyTag.getFieldValue(field).orElseThrow()); - }); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/RelaysTagTest.java b/nostr-java-event/src/test/java/nostr/event/unit/RelaysTagTest.java deleted file mode 100644 index ae7e8c598..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/RelaysTagTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package nostr.event.unit; - -import com.fasterxml.jackson.databind.JsonNode; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.json.codec.BaseTagEncoder; -import nostr.event.tag.RelaysTag; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; - -class RelaysTagTest { - - public static final String RELAYS_KEY = "relays"; - public static final String HOST_VALUE = "ws://localhost:5555"; - public static final String HOST_VALUE2 = "ws://anotherlocalhost:5432"; - - @Test - void testSerialize() { - final String expected = "[\"relays\",\"ws://localhost:5555\",\"ws://anotherlocalhost:5432\"]"; - RelaysTag relaysTag = new RelaysTag(List.of(new Relay(HOST_VALUE), new Relay(HOST_VALUE2))); - BaseTagEncoder baseTagEncoder = new BaseTagEncoder(relaysTag); - assertDoesNotThrow( - () -> { - assertEquals(expected, baseTagEncoder.encode()); - }); - } - - @Test - void testDeserialize() { - final String EXPECTED = "[\"relays\",\"ws://localhost:5555\"]"; - assertDoesNotThrow( - () -> { - JsonNode node = mapper().readTree(EXPECTED); - BaseTag deserialize = RelaysTag.deserialize(node); - assertEquals(RELAYS_KEY, deserialize.getCode()); - assertEquals(HOST_VALUE, ((RelaysTag) deserialize).getRelays().getFirst().getUri()); - }); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/TagDeserializerTest.java b/nostr-java-event/src/test/java/nostr/event/unit/TagDeserializerTest.java deleted file mode 100644 index 45a9b8d6c..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/TagDeserializerTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package nostr.event.unit; - -import nostr.event.BaseTag; -import nostr.event.tag.AddressTag; -import nostr.event.tag.EventTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PriceTag; -import nostr.event.tag.UrlTag; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; - -import static nostr.base.json.EventJsonMapper.mapper; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNull; - -class TagDeserializerTest { - - @Test - // Parses an AddressTag from JSON and verifies its fields. - void testAddressTagDeserialization() throws Exception { - String pubKey = "bbbd79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984"; - String json = "[\"a\",\"1:" + pubKey + ":test\",\"ws://localhost:8080\"]"; - BaseTag tag = mapper().readValue(json, BaseTag.class); - assertInstanceOf(AddressTag.class, tag); - AddressTag aTag = (AddressTag) tag; - assertEquals(1, aTag.getKind()); - assertEquals(pubKey, aTag.getPublicKey().toString()); - assertEquals("test", aTag.getIdentifierTag().getUuid()); - assertEquals("ws://localhost:8080", aTag.getRelay().getUri()); - } - - @Test - // Parses an EventTag with relay and marker and checks values. - void testEventTagDeserialization() throws Exception { - String id = "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"; - String json = "[\"e\",\"" + id + "\",\"wss://relay.example.com\",\"root\"]"; - BaseTag tag = mapper().readValue(json, BaseTag.class); - assertInstanceOf(EventTag.class, tag); - EventTag eTag = (EventTag) tag; - assertEquals(id, eTag.getIdEvent()); - assertEquals("wss://relay.example.com", eTag.getRecommendedRelayUrl()); - assertEquals("root", eTag.getMarker().getValue()); - } - - @Test - // Parses an EventTag without relay or marker and ensures marker is null. - void testEventTagDeserializationWithoutMarker() throws Exception { - String id = "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"; - String json = "[\"e\",\"" + id + "\"]"; - BaseTag tag = mapper().readValue(json, BaseTag.class); - assertInstanceOf(EventTag.class, tag); - EventTag eTag = (EventTag) tag; - assertEquals(id, eTag.getIdEvent()); - assertNull(eTag.getMarker()); - assertNull(eTag.getRecommendedRelayUrl()); - } - - @Test - // Parses a PriceTag from JSON and validates number and currency. - void testPriceTagDeserialization() throws Exception { - String json = "[\"price\",\"10.99\",\"USD\"]"; - BaseTag tag = mapper().readValue(json, BaseTag.class); - assertInstanceOf(PriceTag.class, tag); - PriceTag pTag = (PriceTag) tag; - assertEquals(new BigDecimal("10.99"), pTag.getNumber()); - assertEquals("USD", pTag.getCurrency()); - } - - @Test - // Parses a UrlTag from JSON and checks the URL value. - void testUrlTagDeserialization() throws Exception { - String json = "[\"u\",\"http://example.com\"]"; - BaseTag tag = mapper().readValue(json, BaseTag.class); - assertInstanceOf(UrlTag.class, tag); - UrlTag uTag = (UrlTag) tag; - assertEquals("http://example.com", uTag.getUrl()); - } - - @Test - // Falls back to GenericTag for unknown tag codes. - void testGenericFallback() throws Exception { - String json = "[\"unknown\",\"value\"]"; - BaseTag tag = mapper().readValue(json, BaseTag.class); - assertInstanceOf(GenericTag.class, tag); - GenericTag gTag = (GenericTag) tag; - assertEquals("unknown", gTag.getCode()); - assertEquals("value", gTag.getAttributes().get(0).value()); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/TagRegistryTest.java b/nostr-java-event/src/test/java/nostr/event/unit/TagRegistryTest.java deleted file mode 100644 index 2e781a3d6..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/TagRegistryTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package nostr.event.unit; - -import nostr.base.annotation.Key; -import nostr.base.annotation.Tag; -import nostr.event.BaseTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.TagRegistry; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - -/** Tests for dynamic tag registration. */ -class TagRegistryTest { - - @Tag(code = "x") - static class CustomTag extends BaseTag { - @Key private String value; - - static CustomTag updateFields(GenericTag genericTag) { - CustomTag tag = new CustomTag(); - tag.value = genericTag.getAttributes().get(0).value().toString(); - return tag; - } - } - - @Test - void registerCustomTag() { - TagRegistry.register("x", CustomTag::updateFields); - BaseTag created = BaseTag.create("x", "hello"); - assertInstanceOf(CustomTag.class, created); - assertEquals("hello", ((CustomTag) created).value); - } -} diff --git a/nostr-java-event/src/test/java/nostr/event/unit/ValidateKindTest.java b/nostr-java-event/src/test/java/nostr/event/unit/ValidateKindTest.java deleted file mode 100644 index adcdff4b1..000000000 --- a/nostr-java-event/src/test/java/nostr/event/unit/ValidateKindTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package nostr.event.unit; - -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.event.impl.TextNoteEvent; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class ValidateKindTest { - @Test - public void testTextNoteInvalidKind() { - TextNoteEvent event = new TextNoteEvent(); - event.setPubKey( - new PublicKey("bbbd79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984")); - event.setKind(Kind.DELETION.getValue()); - event.setContent(""); - event.setTags(new ArrayList<>()); - event.setSignature( - Signature.fromString( - "86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546")); - event.setCreatedAt(0L); - event.setId("494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"); - - assertThrows(AssertionError.class, event::validate); - } -} diff --git a/nostr-java-examples/pom.xml b/nostr-java-examples/pom.xml deleted file mode 100644 index 04649fb39..000000000 --- a/nostr-java-examples/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - 4.0.0 - - - xyz.tcheeric - nostr-java - 1.3.0 - ../pom.xml - - - nostr-java-examples - jar - nostr-java-examples - - - - reposilite-releases - https://maven.398ja.xyz/releases - - - reposilite-snapshots - https://maven.398ja.xyz/snapshots - - - - - - - ${project.groupId} - nostr-java-api - - - - diff --git a/nostr-java-examples/src/main/java/nostr/examples/ExpirationEventExample.java b/nostr-java-examples/src/main/java/nostr/examples/ExpirationEventExample.java deleted file mode 100644 index fc4d0edb3..000000000 --- a/nostr-java-examples/src/main/java/nostr/examples/ExpirationEventExample.java +++ /dev/null @@ -1,60 +0,0 @@ -package nostr.examples; - -import nostr.base.ElementAttribute; -import nostr.base.Kind; -import nostr.client.springwebsocket.SpringWebSocketClient; -import nostr.client.springwebsocket.StandardWebSocketClient; -import nostr.event.BaseTag; -import nostr.event.impl.GenericEvent; -import nostr.event.message.EventMessage; -import nostr.event.tag.GenericTag; -import nostr.id.Identity; - -import java.time.Instant; -import java.util.List; - -/** - * Example demonstrating creation of an expiration event (NIP-40) and showing how to send it with - * either available WebSocket client. - */ -public class ExpirationEventExample { - - private static final String RELAY_URI = "ws://localhost:5555"; - private static final long EXPIRATION_SECONDS = 3600; // 1 hour - - private static GenericEvent createExpirationEvent() { - Identity identity = Identity.generateRandomIdentity(); - long expiration = Instant.now().plusSeconds(EXPIRATION_SECONDS).getEpochSecond(); - BaseTag expirationTag = - new GenericTag("expiration", new ElementAttribute("param0", String.valueOf(expiration))); - GenericEvent event = - new GenericEvent( - identity.getPublicKey(), - Kind.TEXT_NOTE, - List.of(expirationTag), - "This message will expire at the specified timestamp and be deleted by relays.\n"); - identity.sign(event); - return event; - } - - private static void sendWithStandardClient(GenericEvent event) throws Exception { - try (StandardWebSocketClient client = new StandardWebSocketClient(RELAY_URI)) { - client.send(new EventMessage(event)); - } - } - - private static void sendWithSpringClient(GenericEvent event) throws Exception { - try (SpringWebSocketClient client = - new SpringWebSocketClient(new StandardWebSocketClient(RELAY_URI), RELAY_URI)) { - client.send(new EventMessage(event)); - } - } - - public static void main(String[] args) throws Exception { - GenericEvent event = createExpirationEvent(); - // Alternative: use the standard client instead of the Spring client. It waits for - // a relay response and does not automatically retry failed sends. - // sendWithStandardClient(event); - sendWithSpringClient(event); - } -} diff --git a/nostr-java-examples/src/main/java/nostr/examples/FilterExample.java b/nostr-java-examples/src/main/java/nostr/examples/FilterExample.java deleted file mode 100644 index a69b53a6c..000000000 --- a/nostr-java-examples/src/main/java/nostr/examples/FilterExample.java +++ /dev/null @@ -1,44 +0,0 @@ -package nostr.examples; - -import nostr.api.NIP01; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.BaseMessage; -import nostr.event.filter.AuthorFilter; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.message.EventMessage; -import nostr.id.Identity; - -import java.util.List; -import java.util.Map; - -/** Demonstrates requesting events from a relay using filters for author and kind. */ -public class FilterExample { - - private static final String RELAY_URL = "wss://relay.damus.io"; - - public static void main(String[] args) throws Exception { - var author = new PublicKey("21ef0d8541375ae4bca85285097fba370f7e540b5a30e5e75670c16679f9d144"); - - var filters = new Filters(new AuthorFilter<>(author), new KindFilter<>(Kind.TEXT_NOTE)); - - var subId = "filter-example-" + System.currentTimeMillis(); - - Identity sender = Identity.generateRandomIdentity(); - NIP01 client = new NIP01(sender); - client.setRelays(Map.of("damus", RELAY_URL)); - - List responses = client.sendRequest(filters, subId); - - var decoder = new BaseMessageDecoder(); - for (String json : responses) { - BaseMessage message = decoder.decode(json); - if (message instanceof EventMessage eventMessage) { - System.out.println(eventMessage.getEvent()); - } - } - client.close(); - } -} diff --git a/nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java b/nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java deleted file mode 100644 index 146f7c47e..000000000 --- a/nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java +++ /dev/null @@ -1,326 +0,0 @@ -package nostr.examples; - -import nostr.api.NIP01; -import nostr.api.NIP04; -import nostr.api.NIP05; -import nostr.api.NIP09; -import nostr.api.NIP25; -import nostr.api.NIP28; -import nostr.api.NIP30; -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.entities.ChannelProfile; -import nostr.event.entities.Reaction; -import nostr.event.entities.UserProfile; -import nostr.event.filter.AuthorFilter; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; -import nostr.event.filter.SinceFilter; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.PubKeyTag; -import nostr.id.Identity; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.List; -import java.util.Map; - -/** Example demonstrating several nostr-java API calls. */ -public class NostrApiExamples { - - private static final Identity RECIPIENT = Identity.generateRandomIdentity(); - private static final Identity SENDER = Identity.generateRandomIdentity(); - - private static final UserProfile PROFILE = - new UserProfile(SENDER.getPublicKey(), "Nostr Guy", "guy@nostr-java.io", "It's me!", null); - private static final Map RELAYS = Map.of("local", "localhost:5555"); - - static { - try { - PROFILE.setPicture( - new URI("https://images.unsplash.com/photo-1462888210965-cdf193fb74de").toURL()); - } catch (MalformedURLException | URISyntaxException e) { - throw new RuntimeException(e); - } - } - - public static void main(String[] args) throws Exception { - new NostrApiExamples().run(); - } - - public void run() throws Exception { - logAccountsData(); - - metaDataEvent(); - sendTextNoteEvent(); - sendEncryptedDirectMessage(); - deletionEvent(); - ephemerealEvent(); - reactionEvent(); - replaceableEvent(); - internetIdMetadata(); - filters(); - createChannel(); - updateChannelMetadata(); - sendChannelMessage(); - hideMessage(); - muteUser(); - } - - private static GenericEvent sendTextNoteEvent() { - logHeader("sendTextNoteEvent"); - - List tags = new ArrayList<>(List.of(new PubKeyTag(RECIPIENT.getPublicKey()))); - - var nip01 = new NIP01(SENDER); - nip01.createTextNoteEvent(tags, "Hello world, I'm here on nostr-java API!").sign().send(RELAYS); - - return nip01.getEvent(); - } - - private static void sendEncryptedDirectMessage() { - logHeader("sendEncryptedDirectMessage"); - - var nip04 = new NIP04(SENDER, RECIPIENT.getPublicKey()); - nip04.createDirectMessageEvent("Hello Nakamoto!").sign().send(RELAYS); - } - - private static void deletionEvent() { - logHeader("deletionEvent"); - - var event = sendTextNoteEvent(); - - var nip09 = new NIP09(SENDER); - nip09.createDeletionEvent(event).sign().send(); - } - - private static GenericEvent metaDataEvent() { - logHeader("metaDataEvent"); - - var nip01 = new NIP01(SENDER); - nip01.createMetadataEvent(PROFILE).sign().send(RELAYS); - - return nip01.getEvent(); - } - - private static void ephemerealEvent() { - logHeader("ephemeralEvent"); - - var nip01 = new NIP01(SENDER); - nip01 - .createEphemeralEvent(Kind.EPHEMEREAL_EVENT.getValue(), "An ephemeral event") - .sign() - .send(RELAYS); - } - - private static void reactionEvent() { - logHeader("reactionEvent"); - - List tags = - new ArrayList<>( - List.of( - NIP30.createEmojiTag( - "soapbox", "https://gleasonator.com/emoji/Gleasonator/soapbox.png"))); - var nip01 = new NIP01(SENDER); - var event = nip01.createTextNoteEvent(tags, "Hello Astral, Please like me! :soapbox:"); - event.signAndSend(RELAYS); - - var nip25 = new NIP25(RECIPIENT); - var reactionEvent = - nip25.createReactionEvent(event.getEvent(), Reaction.LIKE, new Relay("localhost:5555")); - reactionEvent.signAndSend(RELAYS); - nip25 - .createReactionEvent(event.getEvent(), "\uD83D\uDCA9", new Relay("localhost:5555")) - .signAndSend(); - - BaseTag eventTag = NIP01.createEventTag(event.getEvent().getId()); - nip25 - .createReactionEvent( - eventTag, - NIP30.createEmojiTag( - "ablobcatrainbow", "https://gleasonator.com/emoji/blobcat/ablobcatrainbow.png")) - .signAndSend(); - } - - private static void replaceableEvent() { - logHeader("replaceableEvent"); - - var nip01 = new NIP01(SENDER); - var event = nip01.createTextNoteEvent("Hello Astral, Please replace me!"); - event.signAndSend(RELAYS); - - nip01 - .createReplaceableEvent( - List.of(NIP01.createEventTag(event.getEvent().getId())), - Kind.REPLACEABLE_EVENT.getValue(), - "New content") - .signAndSend(); - } - - private static void internetIdMetadata() { - logHeader("internetIdMetadata"); - var profile = - UserProfile.builder() - .name("Guilherme Gps") - .publicKey( - new PublicKey("21ef0d8541375ae4bca85285097fba370f7e540b5a30e5e75670c16679f9d144")) - .nip05("me@guilhermegps.com.br") - .build(); - - var nip05 = new NIP05(SENDER); - nip05.createInternetIdentifierMetadataEvent(profile).sign().send(RELAYS); - } - - private static void filters() throws InterruptedException { - logHeader("filters"); - - var date = Calendar.getInstance(); - var subId = "subId" + date.getTimeInMillis(); - date.add(Calendar.DAY_OF_MONTH, -5); - - var nip01 = NIP01.getInstance(); - nip01 - .setRelays(RELAYS) - .sendRequest( - new Filters( - new KindFilter<>(Kind.EPHEMEREAL_EVENT), - new KindFilter<>(Kind.TEXT_NOTE), - new AuthorFilter<>( - new PublicKey( - "21ef0d8541375ae4bca85285097fba370f7e540b5a30e5e75670c16679f9d144")), - new SinceFilter(date.getTimeInMillis() / 1000)), - subId); - - Thread.sleep(5000); - } - - private static GenericEvent createChannel() { - try { - logHeader("createChannel"); - - var channel = - new ChannelProfile( - "JNostr Channel", - "This is a channel to test NIP28 in nostr-java", - "https://cdn.pixabay.com/photo/2020/05/19/13/48/cartoon-5190942_960_720.jpg"); - var nip28 = new NIP28(SENDER); - nip28.setSender(SENDER); - nip28.createChannelCreateEvent(channel).sign().send(); - return nip28.getEvent(); - } catch (MalformedURLException | URISyntaxException ex) { - throw new RuntimeException(ex); - } - } - - private static void updateChannelMetadata() { - try { - logHeader("updateChannelMetadata"); - - var channelCreateEvent = createChannel(); - var channel = - new ChannelProfile( - "test change name", - "This is a channel to test NIP28 in nostr-java | changed", - "https://cdn.pixabay.com/photo/2020/05/19/13/48/cartoon-5190942_960_720.jpg"); - - var nip28 = new NIP28(SENDER); - nip28.updateChannelMetadataEvent(channelCreateEvent, channel, null).sign().send(); - - } catch (MalformedURLException | URISyntaxException ex) { - throw new RuntimeException(ex); - } - } - - private static GenericEvent sendChannelMessage() { - logHeader("sendChannelMessage"); - - var channelCreateEvent = createChannel(); - - var nip28 = new NIP28(SENDER); - nip28 - .createChannelMessageEvent( - channelCreateEvent, new Relay("localhost:5555"), "Hello everybody!") - .sign() - .send(); - - return nip28.getEvent(); - } - - private static GenericEvent hideMessage() { - logHeader("hideMessage"); - - var channelMessageEvent = sendChannelMessage(); - - var nip28 = new NIP28(SENDER); - nip28.createHideMessageEvent(channelMessageEvent, "Dick pic").sign().send(); - - return nip28.getEvent(); - } - - private static GenericEvent muteUser() { - logHeader("muteUser"); - - var nip28 = new NIP28(SENDER); - nip28.createMuteUserEvent(RECIPIENT.getPublicKey(), "Posting dick pics").sign().send(); - - return nip28.getEvent(); - } - - private static void logAccountsData() { - String msg = - "################################ ACCOUNTS BEGINNING ################################" - + '\n' - + "*** RECEIVER ***" - + '\n' - + '\n' - + "* PrivateKey: " - + RECIPIENT.getPrivateKey().toBech32String() - + '\n' - + "* PrivateKey HEX: " - + RECIPIENT.getPrivateKey().toString() - + '\n' - + "* PublicKey: " - + RECIPIENT.getPublicKey().toBech32String() - + '\n' - + "* PublicKey HEX: " - + RECIPIENT.getPublicKey().toString() - + '\n' - + '\n' - + "*** SENDER ***" - + '\n' - + '\n' - + "* PrivateKey: " - + SENDER.getPrivateKey().toBech32String() - + '\n' - + "* PrivateKey HEX: " - + SENDER.getPrivateKey().toString() - + '\n' - + "* PublicKey: " - + SENDER.getPublicKey().toBech32String() - + '\n' - + "* PublicKey HEX: " - + SENDER.getPublicKey().toString() - + '\n' - + '\n' - + "################################ ACCOUNTS END ################################"; - - System.out.println(msg); - } - - private static void logHeader(String header) { - for (int i = 0; i < 30; i++) { - System.out.print("#"); - } - System.out.println(); - System.out.println("\t" + header); - for (int i = 0; i < 30; i++) { - System.out.print("#"); - } - System.out.println(); - } -} diff --git a/nostr-java-examples/src/main/java/nostr/examples/SpringClientTextEventExample.java b/nostr-java-examples/src/main/java/nostr/examples/SpringClientTextEventExample.java deleted file mode 100644 index 59da82e17..000000000 --- a/nostr-java-examples/src/main/java/nostr/examples/SpringClientTextEventExample.java +++ /dev/null @@ -1,22 +0,0 @@ -package nostr.examples; - -import nostr.api.NIP01; -import nostr.id.Identity; - -import java.util.Map; - -/** - * Example showing how to create, sign and send a text note using the NIP01 helper built on top of - * NostrSpringWebSocketClient. - */ -public class SpringClientTextEventExample { - - private static final Map RELAYS = Map.of("local", "ws://localhost:5555"); - - public static void main(String[] args) { - Identity sender = Identity.generateRandomIdentity(); - NIP01 client = new NIP01(sender); - client.setRelays(RELAYS); - client.createTextNoteEvent("Hello from NostrSpringWebSocketClient!\n").signAndSend(); - } -} diff --git a/nostr-java-examples/src/main/java/nostr/examples/SpringSubscriptionExample.java b/nostr-java-examples/src/main/java/nostr/examples/SpringSubscriptionExample.java deleted file mode 100644 index 743dfcd42..000000000 --- a/nostr-java-examples/src/main/java/nostr/examples/SpringSubscriptionExample.java +++ /dev/null @@ -1,48 +0,0 @@ -package nostr.examples; - -import nostr.api.NostrSpringWebSocketClient; -import nostr.base.Kind; -import nostr.event.filter.Filters; -import nostr.event.filter.KindFilter; - -import java.time.Duration; -import java.util.Map; - -/** - * Example showing how to open a non-blocking subscription using - * {@link nostr.api.NostrSpringWebSocketClient} and close it after a fixed duration. - */ -public class SpringSubscriptionExample { - - private static final Map RELAYS = Map.of("local", "ws://localhost:5555"); - private static final Duration LISTEN_DURATION = Duration.ofSeconds(30); - - public static void main(String[] args) throws Exception { - NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(); - client.setRelays(RELAYS); - - Filters filters = new Filters(new KindFilter<>(Kind.TEXT_NOTE)); - - AutoCloseable subscription = - client.subscribe( - filters, - "example-subscription", - message -> System.out.printf("Received from relay: %s%n", message), - error -> - System.err.printf( - "Subscription error for %s: %s%n", RELAYS.keySet(), error.getMessage())); - - try { - System.out.printf( - "Listening for %d seconds. Publish events to %s to see them here.%n", - LISTEN_DURATION.toSeconds(), RELAYS.values()); - Thread.sleep(LISTEN_DURATION.toMillis()); - } finally { - try { - subscription.close(); - } finally { - client.close(); - } - } - } -} diff --git a/nostr-java-examples/src/main/java/nostr/examples/TextNoteEventExample.java b/nostr-java-examples/src/main/java/nostr/examples/TextNoteEventExample.java deleted file mode 100644 index ebd4bad64..000000000 --- a/nostr-java-examples/src/main/java/nostr/examples/TextNoteEventExample.java +++ /dev/null @@ -1,30 +0,0 @@ -package nostr.examples; - -import nostr.client.springwebsocket.StandardWebSocketClient; -import nostr.event.BaseTag; -import nostr.event.impl.TextNoteEvent; -import nostr.event.message.EventMessage; -import nostr.id.Identity; - -import java.util.List; - -/** - * Demonstrates creating, signing, and sending a text note using the - * {@link nostr.event.impl.TextNoteEvent} class. - */ -public class TextNoteEventExample { - - private static final String RELAY_URI = "ws://localhost:5555"; - - public static void main(String[] args) throws Exception { - Identity identity = Identity.generateRandomIdentity(); - TextNoteEvent event = - new TextNoteEvent( - identity.getPublicKey(), List.of(), "Hello from TextNoteEvent!\n"); - identity.sign(event); - try (StandardWebSocketClient client = new StandardWebSocketClient(RELAY_URI)) { - client.send(new EventMessage(event)); - } - System.out.println(event); - } -} diff --git a/nostr-java-examples/src/main/resources/logging.properties b/nostr-java-examples/src/main/resources/logging.properties deleted file mode 100644 index 8c9a31025..000000000 --- a/nostr-java-examples/src/main/resources/logging.properties +++ /dev/null @@ -1,86 +0,0 @@ -# The MIT License -# -# Copyright 2022 eric. -# -# 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. - -############################################################ -# Default Logging Configuration File -# -# You can use a different file by specifying a filename -# with the java.util.logging.config.file system property. -# For example java -Djava.util.logging.config.file=myfile -############################################################ - -############################################################ -# Global properties -############################################################ - -# "handlers" specifies a comma separated list of log Handler -# classes. These handlers will be installed during VM startup. -# Note that these classes must be on the system classpath. -# By default we only configure a ConsoleHandler, which will only -# show messages at the INFO and above levels. -handlers= java.util.logging.ConsoleHandler - -# To also add the FileHandler, use the following line instead. -#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler - -# Default global logging level. -# This specifies which kinds of events are logged across -# all loggers. For any given facility this global level -# can be overriden by a facility specific level -# Note that the ConsoleHandler also has a separate level -# setting to limit messages printed to the console. -.level= FINER - -############################################################ -# Handler specific properties. -# Describes specific configuration info for Handlers. -############################################################ - -# default file output is in user's home directory. -java.util.logging.FileHandler.pattern = %h/nostr-java-client%u.log -java.util.logging.FileHandler.limit = 50000 -java.util.logging.FileHandler.count = 1 -# Default number of locks FileHandler can obtain synchronously. -# This specifies maximum number of attempts to obtain lock file by FileHandler -# implemented by incrementing the unique field %u as per FileHandler API documentation. -java.util.logging.FileHandler.maxLocks = 100 -java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter - -# Limit the message that are printed on the console to INFO and above. -java.util.logging.ConsoleHandler.level = FINEST -java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter - -# Example to customize the SimpleFormatter output format -# to print one-line log message like this: -# : [] -# -# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n - -############################################################ -# Facility specific properties. -# Provides extra control for each logger. -############################################################ - -# For example, set the com.xyz.foo logger to only log SEVERE -# messages: -# com.xyz.foo.level = SEVERE -nostr.json.unmarshaller.level = FINEST \ No newline at end of file diff --git a/nostr-java-id/src/test/java/nostr/id/ClassifiedListingEventTest.java b/nostr-java-id/src/test/java/nostr/id/ClassifiedListingEventTest.java deleted file mode 100644 index 5d3c689b9..000000000 --- a/nostr-java-id/src/test/java/nostr/id/ClassifiedListingEventTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package nostr.id; - -import nostr.base.Kind; -import nostr.base.PublicKey; -import nostr.event.BaseTag; -import nostr.event.impl.ClassifiedListingEvent; -import nostr.event.tag.EventTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.PriceTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.SubjectTag; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ClassifiedListingEventTest { - public static final PublicKey senderPubkey = - new PublicKey(Identity.generateRandomIdentity().getPublicKey().toString()); - public static final String CLASSIFIED_LISTING_CONTENT = "classified listing content"; - - public static final String PTAG_HEX = - "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76985"; - public static final String ETAG_HEX = - "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4347"; - - public static final PubKeyTag P_TAG = new PubKeyTag(new PublicKey(PTAG_HEX)); - public static final EventTag E_TAG = new EventTag(ETAG_HEX); - - public static final String SUBJECT = "Classified Listing Test Subject Tag"; - public static final SubjectTag SUBJECT_TAG = new SubjectTag(SUBJECT); - public static final GeohashTag G_TAG = new GeohashTag("Classified Listing Test Geohash Tag"); - public static final HashtagTag T_TAG = new HashtagTag("Classified Listing Test Hashtag Tag"); - - public static final BigDecimal NUMBER = new BigDecimal("2.71"); - public static final String FREQUENCY = "NANOSECOND"; - public static final String CURRENCY = "BTC"; - public static final PriceTag PRICE_TAG = new PriceTag(NUMBER, CURRENCY, FREQUENCY); - - public static final String CLASSIFIED_LISTING_TITLE = "classified listing title"; - public static final String CLASSIFIED_LISTING_SUMMARY = "classified listing summary"; - public static final String CLASSIFIED_LISTING_PUBLISHED_AT = "1687765220"; - public static final String CLASSIFIED_LISTING_LOCATION = "classified listing location"; - public static final String TITLE_CODE = "title"; - public static final String SUMMARY_CODE = "summary"; - public static final String PUBLISHED_AT_CODE = "published_at"; - public static final String LOCATION_CODE = "location"; - - private ClassifiedListingEvent instance; - - @BeforeAll - void setup() { - List tags = new ArrayList<>(); - tags.add(E_TAG); - tags.add(P_TAG); - tags.add(BaseTag.create(TITLE_CODE, CLASSIFIED_LISTING_TITLE)); - tags.add(BaseTag.create(SUMMARY_CODE, CLASSIFIED_LISTING_SUMMARY)); - tags.add(BaseTag.create(PUBLISHED_AT_CODE, CLASSIFIED_LISTING_PUBLISHED_AT)); - tags.add(BaseTag.create(LOCATION_CODE, CLASSIFIED_LISTING_LOCATION)); - tags.add(SUBJECT_TAG); - tags.add(G_TAG); - tags.add(T_TAG); - tags.add(PRICE_TAG); - instance = - new ClassifiedListingEvent( - senderPubkey, Kind.CLASSIFIED_LISTING, tags, CLASSIFIED_LISTING_CONTENT); - instance.setSignature(Identity.generateRandomIdentity().sign(instance)); - } - - @Test - void testConstructClassifiedListingEvent() { - System.out.println("testConstructClassifiedListingEvent"); - - assertEquals(10, instance.getTags().size()); - assertEquals(CLASSIFIED_LISTING_CONTENT, instance.getContent()); - assertEquals(Kind.CLASSIFIED_LISTING.getValue(), instance.getKind().intValue()); - assertEquals(senderPubkey.toString(), instance.getPubKey().toString()); - assertEquals(senderPubkey.toBech32String(), instance.getPubKey().toBech32String()); - assertEquals(senderPubkey.toHexString(), instance.getPubKey().toHexString()); - assertEquals(CLASSIFIED_LISTING_CONTENT, instance.getContent()); - } -} diff --git a/nostr-java-id/src/test/java/nostr/id/EntityFactory.java b/nostr-java-id/src/test/java/nostr/id/EntityFactory.java deleted file mode 100644 index 036c96303..000000000 --- a/nostr-java-id/src/test/java/nostr/id/EntityFactory.java +++ /dev/null @@ -1,174 +0,0 @@ -package nostr.id; - -import lombok.extern.slf4j.Slf4j; -import nostr.base.ElementAttribute; -import nostr.base.GenericTagQuery; -import nostr.base.IEvent; -import nostr.base.PublicKey; -import nostr.event.BaseTag; -import nostr.event.entities.Reaction; -import nostr.event.entities.UserProfile; -import nostr.event.impl.DirectMessageEvent; -import nostr.event.impl.GenericEvent; -import nostr.event.impl.InternetIdentifierMetadataEvent; -import nostr.event.impl.MentionsEvent; -import nostr.event.impl.OtsEvent; -import nostr.event.impl.ReactionEvent; -import nostr.event.impl.ReplaceableEvent; -import nostr.event.impl.TextNoteEvent; -import nostr.event.tag.EventTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.PubKeyTag; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -/** - * @author squirrel - */ -@Slf4j -// TODO - Add the sender PK to all createEvents. -public class EntityFactory { - - @Slf4j - public static class Events { - - /* - public static EphemeralEvent createEphemeralEvent(PublicKey publicKey) { - List tagList = new ArrayList<>(); - tagList.add(PubKeyTag.builder().publicKey(publicKey).petName("eric").build()); - EphemeralEvent event = new EphemeralEvent(publicKey, Kind.EPHEMEREAL_EVENT.getValue(), tagList); - event.update(); - return event; - } - */ - - public static DirectMessageEvent createDirectMessageEvent( - PublicKey senderPublicKey, PublicKey rcptPublicKey, String content) { - List tagList = new ArrayList<>(); - tagList.add(PubKeyTag.builder().publicKey(rcptPublicKey).petName("uq7yfx3l").build()); - DirectMessageEvent event = new DirectMessageEvent(senderPublicKey, tagList, content); - event.update(); - return event; - } - - public static InternetIdentifierMetadataEvent createInternetIdentifierMetadataEvent( - UserProfile profile) { - final PublicKey publicKey = profile.getPublicKey(); - InternetIdentifierMetadataEvent event = - new InternetIdentifierMetadataEvent(publicKey, profile.toString()); - event.update(); - return event; - } - - public static MentionsEvent createMentionsEvent(PublicKey publicKey, Integer kind) { - List tagList = new ArrayList<>(); - tagList.add(PubKeyTag.builder().publicKey(publicKey).petName("charlie").build()); - String content = generateRamdomAlpha(32); - StringBuilder sbContent = new StringBuilder(content); - - int len = tagList.size(); - for (BaseTag baseTag : tagList) { - sbContent.append(", ").append(((PubKeyTag) baseTag).getPublicKey().toString()); - } - - MentionsEvent event = new MentionsEvent(publicKey, kind, tagList, sbContent.toString()); - event.update(); - return event; - } - - public static ReactionEvent createReactionEvent(PublicKey publicKey, GenericEvent original) { - List tagList = new ArrayList<>(); - tagList.add(EventTag.builder().idEvent(original.getId()).build()); - return new ReactionEvent(publicKey, tagList, Reaction.LIKE.getEmoji()); - } - - public static ReplaceableEvent createReplaceableEvent(PublicKey publicKey) { - String content = generateRamdomAlpha(32); - return new ReplaceableEvent(publicKey, 15000, new ArrayList<>(), content); - } - - public static TextNoteEvent createTextNoteEvent(PublicKey publicKey) { - String content = generateRamdomAlpha(32); - return createTextNoteEvent(publicKey, content); - } - - public static TextNoteEvent createTextNoteEvent(PublicKey publicKey, String content) { - List tagList = new ArrayList<>(); - tagList.add(PubKeyTag.builder().publicKey(publicKey).petName("alice").build()); - return new TextNoteEvent(publicKey, tagList, content); - } - - public static OtsEvent createOtsEvent(PublicKey publicKey, IEvent event) { - List tagList = new ArrayList<>(); - final EventTag eventTag = EventTag.builder().idEvent(event.getId()).build(); - tagList.add(eventTag); - return new OtsEvent(publicKey, tagList, generateRamdomAlpha(32)); - } - - public static GenericTag createGenericTag(PublicKey publicKey) { - IEvent event = createTextNoteEvent(publicKey); - return createGenericTag(publicKey, event); - } - - public static GenericTag createGenericTag(PublicKey publicKey, IEvent event) { - GenericTag tag = new GenericTag("devil"); - tag.addAttribute(new ElementAttribute("param0", "Lucifer")); - ((GenericEvent) event).addTag(tag); - return tag; - } - - // Removed deprecated compatibility overload createGenericTag(publicKey, event, Integer) in 1.0.0 - - public static List createGenericTagQuery() { - Character c = generateRamdomAlpha(1).charAt(0); - String v1 = generateRamdomAlpha(5); - String v2 = generateRamdomAlpha(6); - String v3 = generateRamdomAlpha(7); - - List list = new ArrayList<>(); - list.add(v3); - list.add(v2); - list.add(v1); - - return list.stream().map(item -> new GenericTagQuery(c.toString(), item)).toList(); - } - } - - public static UserProfile createProfile(PublicKey pubKey) { - try { - String number = EntityFactory.generateRandomNumber(4); - String about = "about_" + number; - String name = "name_" + number; - String nip05 = name + "@tcheeric.com"; - String url = "https://assets.tcheeric.com/" + number + ".PNG"; - - return new UserProfile(pubKey, name, nip05, about, new URI(url).toURL()); - - } catch (MalformedURLException | URISyntaxException ex) { - throw new RuntimeException(ex); - } - } - - public static String generateRamdomAlpha(int len) { - return generateRandom(58, 122, len); - } - - public static String generateRandomNumber(int len) { - return generateRandom(48, 57, len); - } - - private static String generateRandom(int leftLimit, int rightLimit, int len) { - - return new Random() - .ints(leftLimit, rightLimit + 1) - .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) - .limit(len) - .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) - .toString(); - } -} diff --git a/nostr-java-id/src/test/java/nostr/id/ReactionEventTest.java b/nostr-java-id/src/test/java/nostr/id/ReactionEventTest.java deleted file mode 100644 index b9a47c048..000000000 --- a/nostr-java-id/src/test/java/nostr/id/ReactionEventTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package nostr.id; - -import nostr.base.PublicKey; -import nostr.event.entities.Reaction; -import nostr.event.impl.GenericEvent; -import nostr.event.impl.ReactionEvent; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class ReactionEventTest { - - @Test - void testGetReactedEventId() { - PublicKey pk = Identity.generateRandomIdentity().getPublicKey(); - GenericEvent original = EntityFactory.Events.createTextNoteEvent(pk); - original.update(); - ReactionEvent reaction = EntityFactory.Events.createReactionEvent(pk, original); - reaction.update(); - assertEquals(original.getId(), reaction.getReactedEventId()); - } - - @Test - void testMissingEventTag() { - PublicKey pk = Identity.generateRandomIdentity().getPublicKey(); - ReactionEvent reaction = new ReactionEvent(pk, new ArrayList<>(), Reaction.LIKE.getEmoji()); - assertThrows(AssertionError.class, reaction::getReactedEventId); - } -} diff --git a/nostr-java-id/src/test/java/nostr/id/ZapReceiptEventTest.java b/nostr-java-id/src/test/java/nostr/id/ZapReceiptEventTest.java deleted file mode 100644 index bc22f8141..000000000 --- a/nostr-java-id/src/test/java/nostr/id/ZapReceiptEventTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package nostr.id; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -class ZapReceiptEventTest { - - /* - @Test - void testConstructZapReceiptEvent() { - - - PublicKey sender = Identity.generateRandomIdentity().getPublicKey(); - String zapRequestPubKeyTag = Identity.generateRandomIdentity().getPublicKey().toString(); - String zapRequestEventTag = Identity.generateRandomIdentity().getPublicKey().toString(); - String zapRequestAddressTag = Identity.generateRandomIdentity().getPublicKey().toString(); - final String ZAP_RECEIPT_IDENTIFIER = "ipsum"; - final String ZAP_RECEIPT_RELAY_URI = "ws://localhost:5555"; - final String BOLT_11 = "bolt11"; - final String DESCRIPTION_SHA256 = "descriptionSha256"; - final String PRE_IMAGE = "preimage"; - - ZapReceiptEvent instance = new ZapReceiptEvent(sender, zapRequestPubKeyTag, zapRequestEventTag, zapRequestAddressTag, ZAP_RECEIPT_IDENTIFIER, ZAP_RECEIPT_RELAY_URI, BOLT_11, DESCRIPTION_SHA256, PRE_IMAGE); - - assertNotNull(instance.getZapReceipt()); - assertNotNull(instance.getZapReceipt().getBolt11()); - assertNotNull(instance.getZapReceipt().getDescriptionSha256()); - assertNotNull(instance.getZapReceipt().getPreimage()); - - assertTrue(instance.getTags().stream().filter(AddressTag.class::isInstance).map(AddressTag.class::cast).map(addressTag -> addressTag.getPublicKey().toString()).anyMatch(zapRequestAddressTag::equals)); - - assertTrue(instance.getTags().stream().filter(AddressTag.class::isInstance).map(AddressTag.class::cast).map(addressTag -> addressTag.getRelay().getUri()).anyMatch(ZAP_RECEIPT_RELAY_URI::equals)); - - assertTrue(instance.getTags().stream().filter(AddressTag.class::isInstance).map(AddressTag.class::cast).map(addressTag -> addressTag.getIdentifierTag().getId()).anyMatch(ZAP_RECEIPT_IDENTIFIER::equals)); - - assertEquals(BOLT_11, instance.getZapReceipt().getBolt11()); - assertEquals(DESCRIPTION_SHA256, instance.getZapReceipt().getDescriptionSha256()); - assertEquals(PRE_IMAGE, instance.getZapReceipt().getPreimage()); - } - */ - -} diff --git a/nostr-java-id/src/test/java/nostr/id/ZapRequestEventTest.java b/nostr-java-id/src/test/java/nostr/id/ZapRequestEventTest.java deleted file mode 100644 index 05458ef40..000000000 --- a/nostr-java-id/src/test/java/nostr/id/ZapRequestEventTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package nostr.id; - -import nostr.base.ElementAttribute; -import nostr.base.PublicKey; -import nostr.base.Relay; -import nostr.event.BaseTag; -import nostr.event.impl.ZapRequestEvent; -import nostr.event.tag.EventTag; -import nostr.event.tag.GenericTag; -import nostr.event.tag.GeohashTag; -import nostr.event.tag.HashtagTag; -import nostr.event.tag.PubKeyTag; -import nostr.event.tag.RelaysTag; -import nostr.event.tag.SubjectTag; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ZapRequestEventTest { - public final PublicKey sender = Identity.generateRandomIdentity().getPublicKey(); - public final PubKeyTag recipient = - new PubKeyTag(Identity.generateRandomIdentity().getPublicKey()); - - public static final String PTAG_HEX = - "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76985"; - public static final String ETAG_HEX = - "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4347"; - public static final PubKeyTag P_TAG = new PubKeyTag(new PublicKey(PTAG_HEX)); - public static final EventTag E_TAG = new EventTag(ETAG_HEX); - - public static final String ZAP_REQUEST_CONTENT = "zap request content"; - public static final String SUBJECT = "Zap Request Subject"; - public static final Long AMOUNT = 1232456L; - public static final String LNURL = - "lnurl1dp68gurn8ghj7ar0wfsj6er9wchxuemjda4ju6t09ashq6f0w4ek2u30d3h82unv8a6xzeead3hkw6twye4nz0fcxgmnsef3vy6rsefkx93nyd338ycnvdeex9jxzcnzxeskvdekxq6rswr9x3nrqvfexvex2vf3vejnwvp4x3nr2wfhx56x2vmyv5mx2udztdn"; - public static final RelaysTag relaysTag = new RelaysTag(new Relay("ws://localhost:5555")); - - public static final SubjectTag SUBJECT_TAG = new SubjectTag(SUBJECT); - public static final GeohashTag G_TAG = new GeohashTag("Zap Request Test Geohash Tag"); - public static final HashtagTag T_TAG = new HashtagTag("Zap Request Test Hashtag Tag"); - - private ZapRequestEvent instance; - - @BeforeAll - void setup() { - List tags = new ArrayList<>(); - tags.add(E_TAG); - tags.add(P_TAG); - tags.add(SUBJECT_TAG); - tags.add(G_TAG); - tags.add(T_TAG); - tags.add(relaysTag); - - GenericTag amountTag = new GenericTag("amount"); - amountTag.addAttribute(new ElementAttribute("amount", AMOUNT.toString())); - tags.add(amountTag); - - GenericTag lnUrlTag = new GenericTag("lnurl"); - lnUrlTag.addAttribute(new ElementAttribute("lnurl", LNURL)); - tags.add(lnUrlTag); - - instance = new ZapRequestEvent(sender, tags, ZAP_REQUEST_CONTENT); - instance.setSignature(Identity.generateRandomIdentity().sign(instance)); - } - - @Test - void testConstructZapRequestEvent() { - System.out.println("testConstructZapRequestEvent"); - - Assertions.assertNotNull(instance.getTags()); - Assertions.assertNotNull(instance.getContent()); - Assertions.assertNotNull(instance.getZapRequest()); - - Assertions.assertTrue( - instance.getRelays().stream() - .anyMatch( - relay -> - relay - .getUri() - .equals( - relaysTag.getRelays().stream() - .map(Relay::getUri) - .collect(Collectors.joining())))); - Assertions.assertEquals(ZAP_REQUEST_CONTENT, instance.getContent()); - Assertions.assertEquals(LNURL, instance.getLnUrl()); - Assertions.assertEquals(AMOUNT, instance.getAmount()); - } -} diff --git a/nostr-java-id/src/test/resources/junit-platform.properties b/nostr-java-id/src/test/resources/junit-platform.properties deleted file mode 100644 index a413a5904..000000000 --- a/nostr-java-id/src/test/resources/junit-platform.properties +++ /dev/null @@ -1,7 +0,0 @@ -# junit-platform.properties - -junit.jupiter.execution.parallel.enabled=true -junit.jupiter.execution.parallel.config.strategy=dynamic -junit.jupiter.execution.parallel.mode.default=same_thread -#junit.jupiter.execution.parallel.mode.default=concurrent -#junit.jupiter.execution.parallel.mode.classes.default=concurrent diff --git a/nostr-java-id/pom.xml b/nostr-java-identity/pom.xml similarity index 86% rename from nostr-java-id/pom.xml rename to nostr-java-identity/pom.xml index fe6e47962..097c56418 100644 --- a/nostr-java-id/pom.xml +++ b/nostr-java-identity/pom.xml @@ -4,13 +4,14 @@ xyz.tcheeric nostr-java - 1.3.0 + 2.0.0 ../pom.xml - - nostr-java-id + + nostr-java-identity jar - nostr-java-id + nostr-java-identity + Identity management and encryption for the Nostr Java SDK @@ -28,22 +29,23 @@ ${project.groupId} nostr-java-event - + + org.projectlombok lombok - provided org.slf4j slf4j-api + + org.junit.jupiter junit-jupiter - test diff --git a/nostr-java-encryption/src/main/java/nostr/encryption/MessageCipher.java b/nostr-java-identity/src/main/java/nostr/encryption/MessageCipher.java similarity index 100% rename from nostr-java-encryption/src/main/java/nostr/encryption/MessageCipher.java rename to nostr-java-identity/src/main/java/nostr/encryption/MessageCipher.java diff --git a/nostr-java-encryption/src/main/java/nostr/encryption/MessageCipher04.java b/nostr-java-identity/src/main/java/nostr/encryption/MessageCipher04.java similarity index 100% rename from nostr-java-encryption/src/main/java/nostr/encryption/MessageCipher04.java rename to nostr-java-identity/src/main/java/nostr/encryption/MessageCipher04.java diff --git a/nostr-java-encryption/src/main/java/nostr/encryption/MessageCipher44.java b/nostr-java-identity/src/main/java/nostr/encryption/MessageCipher44.java similarity index 100% rename from nostr-java-encryption/src/main/java/nostr/encryption/MessageCipher44.java rename to nostr-java-identity/src/main/java/nostr/encryption/MessageCipher44.java diff --git a/nostr-java-id/src/main/java/nostr/id/Identity.java b/nostr-java-identity/src/main/java/nostr/id/Identity.java similarity index 100% rename from nostr-java-id/src/main/java/nostr/id/Identity.java rename to nostr-java-identity/src/main/java/nostr/id/Identity.java diff --git a/nostr-java-id/src/main/java/nostr/id/SigningException.java b/nostr-java-identity/src/main/java/nostr/id/SigningException.java similarity index 100% rename from nostr-java-id/src/main/java/nostr/id/SigningException.java rename to nostr-java-identity/src/main/java/nostr/id/SigningException.java diff --git a/nostr-java-encryption/src/test/java/nostr/encryption/MessageCipherTest.java b/nostr-java-identity/src/test/java/nostr/encryption/MessageCipherTest.java similarity index 100% rename from nostr-java-encryption/src/test/java/nostr/encryption/MessageCipherTest.java rename to nostr-java-identity/src/test/java/nostr/encryption/MessageCipherTest.java diff --git a/nostr-java-identity/src/test/java/nostr/id/EntityFactory.java b/nostr-java-identity/src/test/java/nostr/id/EntityFactory.java new file mode 100644 index 000000000..ec39261af --- /dev/null +++ b/nostr-java-identity/src/test/java/nostr/id/EntityFactory.java @@ -0,0 +1,102 @@ +package nostr.id; + +import lombok.extern.slf4j.Slf4j; +import nostr.base.Kinds; +import nostr.base.PublicKey; +import nostr.event.BaseTag; +import nostr.event.impl.GenericEvent; +import nostr.event.tag.GenericTag; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * @author squirrel + */ +@Slf4j +public class EntityFactory { + + @Slf4j + public static class Events { + + public static GenericEvent createDirectMessageEvent( + PublicKey senderPublicKey, PublicKey rcptPublicKey, String content) { + List tagList = new ArrayList<>(); + tagList.add(BaseTag.create("p", rcptPublicKey.toString())); + GenericEvent event = new GenericEvent(senderPublicKey, Kinds.ENCRYPTED_DIRECT_MESSAGE, tagList, content); + event.update(); + return event; + } + + public static GenericEvent createMentionsEvent(PublicKey publicKey, Integer kind) { + List tagList = new ArrayList<>(); + tagList.add(BaseTag.create("p", publicKey.toString())); + String content = generateRamdomAlpha(32); + StringBuilder sbContent = new StringBuilder(content); + sbContent.append(", ").append(publicKey); + + GenericEvent event = new GenericEvent(publicKey, kind, tagList, sbContent.toString()); + event.update(); + return event; + } + + public static GenericEvent createReactionEvent(PublicKey publicKey, GenericEvent original) { + List tagList = new ArrayList<>(); + tagList.add(BaseTag.create("e", original.getId())); + return new GenericEvent(publicKey, Kinds.REACTION, tagList, "+"); + } + + public static GenericEvent createReplaceableEvent(PublicKey publicKey) { + String content = generateRamdomAlpha(32); + return new GenericEvent(publicKey, 15000, new ArrayList<>(), content); + } + + public static GenericEvent createTextNoteEvent(PublicKey publicKey) { + String content = generateRamdomAlpha(32); + return createTextNoteEvent(publicKey, content); + } + + public static GenericEvent createTextNoteEvent(PublicKey publicKey, String content) { + List tagList = new ArrayList<>(); + tagList.add(BaseTag.create("p", publicKey.toString())); + return new GenericEvent(publicKey, Kinds.TEXT_NOTE, tagList, content); + } + + public static GenericEvent createOtsEvent(PublicKey publicKey, GenericEvent event) { + List tagList = new ArrayList<>(); + tagList.add(BaseTag.create("e", event.getId())); + return new GenericEvent(publicKey, Kinds.OTS_EVENT, tagList, generateRamdomAlpha(32)); + } + + public static GenericTag createGenericTag(PublicKey publicKey) { + GenericEvent event = createTextNoteEvent(publicKey); + return createGenericTag(publicKey, event); + } + + public static GenericTag createGenericTag(PublicKey publicKey, GenericEvent event) { + GenericTag tag = GenericTag.of("devil", "Lucifer"); + event.addTag(tag); + return tag; + } + + } + + public static String generateRamdomAlpha(int len) { + return generateRandom(58, 122, len); + } + + public static String generateRandomNumber(int len) { + return generateRandom(48, 57, len); + } + + private static String generateRandom(int leftLimit, int rightLimit, int len) { + + return new Random() + .ints(leftLimit, rightLimit + 1) + .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) + .limit(len) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } +} diff --git a/nostr-java-id/src/test/java/nostr/id/EventTest.java b/nostr-java-identity/src/test/java/nostr/id/EventTest.java similarity index 90% rename from nostr-java-id/src/test/java/nostr/id/EventTest.java rename to nostr-java-identity/src/test/java/nostr/id/EventTest.java index 0ca220875..fe8a50d0d 100644 --- a/nostr-java-id/src/test/java/nostr/id/EventTest.java +++ b/nostr-java-identity/src/test/java/nostr/id/EventTest.java @@ -1,14 +1,12 @@ package nostr.id; import lombok.extern.slf4j.Slf4j; -import nostr.base.ElementAttribute; import nostr.base.PublicKey; import nostr.crypto.bech32.Bech32; import nostr.crypto.bech32.Bech32Prefix; import nostr.event.BaseTag; import nostr.event.impl.GenericEvent; import nostr.event.json.codec.BaseTagEncoder; -import nostr.event.message.GenericMessage; import nostr.event.tag.GenericTag; import nostr.util.NostrException; import nostr.util.NostrUtil; @@ -88,17 +86,6 @@ public void testNip05Validator() throws Exception { assertThrows(NostrException.class, nip05Validator::validate); } - @Test - public void testAuthMessage() { - System.out.println("testAuthMessage"); - - GenericMessage msg = new GenericMessage("AUTH"); - String attr = "challenge-string"; - msg.addAttribute(new ElementAttribute("challenge", attr)); - - var muattr = (msg.getAttributes().getFirst().value()).toString(); - assertEquals(attr, muattr); - } @Test public void testEventIdConstraints() { diff --git a/nostr-java-id/src/test/java/nostr/id/IdentityTest.java b/nostr-java-identity/src/test/java/nostr/id/IdentityTest.java similarity index 88% rename from nostr-java-id/src/test/java/nostr/id/IdentityTest.java rename to nostr-java-identity/src/test/java/nostr/id/IdentityTest.java index b0f7cabe3..0ae71ac17 100644 --- a/nostr-java-id/src/test/java/nostr/id/IdentityTest.java +++ b/nostr-java-identity/src/test/java/nostr/id/IdentityTest.java @@ -1,159 +1,148 @@ -package nostr.id; - -import nostr.base.ISignable; -import nostr.base.PublicKey; -import nostr.base.Signature; -import nostr.crypto.schnorr.Schnorr; -import nostr.crypto.schnorr.SchnorrException; -import nostr.event.impl.GenericEvent; -import nostr.event.tag.DelegationTag; -import nostr.util.NostrUtil; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * @author squirrel - */ -public class IdentityTest { - - public IdentityTest() {} - - @Test - // Ensures signing a text note event attaches a signature - public void testSignEvent() { - System.out.println("testSignEvent"); - Identity identity = Identity.generateRandomIdentity(); - PublicKey publicKey = identity.getPublicKey(); - GenericEvent instance = EntityFactory.Events.createTextNoteEvent(publicKey); - identity.sign(instance); - Assertions.assertNotNull(instance.getSignature()); - } - - @Test - // Ensures signing a delegation tag populates its signature - public void testSignDelegationTag() { - System.out.println("testSignDelegationTag"); - Identity identity = Identity.generateRandomIdentity(); - PublicKey publicKey = identity.getPublicKey(); - DelegationTag delegationTag = new DelegationTag(publicKey, null); - identity.sign(delegationTag); - Assertions.assertNotNull(delegationTag.getSignature()); - } - - @Test - // Verifies that generating random identities yields unique private keys - public void testGenerateRandomIdentityProducesUniqueKeys() { - Identity id1 = Identity.generateRandomIdentity(); - Identity id2 = Identity.generateRandomIdentity(); - Assertions.assertNotEquals(id1.getPrivateKey(), id2.getPrivateKey()); - } - - @Test - // Confirms that deriving the public key from a known private key matches expectations - public void testGetPublicKeyDerivation() { - String privHex = "0000000000000000000000000000000000000000000000000000000000000001"; - Identity identity = Identity.create(privHex); - PublicKey expected = - new PublicKey("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); - Assertions.assertEquals(expected, identity.getPublicKey()); - } - - @Test - // Verifies that signing produces a Schnorr signature that validates successfully - public void testSignProducesValidSignature() - throws NoSuchAlgorithmException, SchnorrException { - String privHex = "0000000000000000000000000000000000000000000000000000000000000001"; - Identity identity = Identity.create(privHex); - final byte[] message = "hello".getBytes(StandardCharsets.UTF_8); - - ISignable signable = - new ISignable() { - private Signature signature; - - @Override - public Signature getSignature() { - return signature; - } - - @Override - public void setSignature(Signature signature) { - this.signature = signature; - } - - @Override - public Consumer getSignatureConsumer() { - return this::setSignature; - } - - @Override - public Supplier getByteArraySupplier() { - return () -> ByteBuffer.wrap(message); - } - }; - - identity.sign(signable); - - byte[] msgHash = NostrUtil.sha256(message); - boolean verified = - Schnorr.verify( - msgHash, identity.getPublicKey().getRawData(), signable.getSignature().getRawData()); - Assertions.assertTrue(verified); - } - - @Test - // Confirms public key derivation is cached for subsequent calls - public void testPublicKeyCaching() { - Identity identity = Identity.generateRandomIdentity(); - PublicKey first = identity.getPublicKey(); - PublicKey second = identity.getPublicKey(); - Assertions.assertSame(first, second); - } - - @Test - // Ensures that invalid private keys trigger a derivation failure - public void testGetPublicKeyFailure() { - String invalidPriv = "0000000000000000000000000000000000000000000000000000000000000000"; - Identity identity = Identity.create(invalidPriv); - Assertions.assertThrows(IllegalStateException.class, identity::getPublicKey); - } - - @Test - // Ensures that signing with an invalid private key throws SigningException - public void testSignWithInvalidKeyFails() { - String invalidPriv = "0000000000000000000000000000000000000000000000000000000000000000"; - Identity identity = Identity.create(invalidPriv); - - ISignable signable = - new ISignable() { - private Signature signature; - - @Override - public Signature getSignature() { - return signature; - } - - @Override - public void setSignature(Signature signature) { - this.signature = signature; - } - - @Override - public Consumer getSignatureConsumer() { - return this::setSignature; - } - - @Override - public Supplier getByteArraySupplier() { - return () -> ByteBuffer.wrap("msg".getBytes(StandardCharsets.UTF_8)); - } - }; - - Assertions.assertThrows(SigningException.class, () -> identity.sign(signable)); - } -} +package nostr.id; + +import nostr.base.ISignable; +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.crypto.schnorr.Schnorr; +import nostr.crypto.schnorr.SchnorrException; +import nostr.event.impl.GenericEvent; + +import nostr.util.NostrUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * @author squirrel + */ +public class IdentityTest { + + public IdentityTest() {} + + @Test + // Ensures signing a text note event attaches a signature + public void testSignEvent() { + System.out.println("testSignEvent"); + Identity identity = Identity.generateRandomIdentity(); + PublicKey publicKey = identity.getPublicKey(); + GenericEvent instance = EntityFactory.Events.createTextNoteEvent(publicKey); + identity.sign(instance); + Assertions.assertNotNull(instance.getSignature()); + } + + @Test + // Verifies that generating random identities yields unique private keys + public void testGenerateRandomIdentityProducesUniqueKeys() { + Identity id1 = Identity.generateRandomIdentity(); + Identity id2 = Identity.generateRandomIdentity(); + Assertions.assertNotEquals(id1.getPrivateKey(), id2.getPrivateKey()); + } + + @Test + // Confirms that deriving the public key from a known private key matches expectations + public void testGetPublicKeyDerivation() { + String privHex = "0000000000000000000000000000000000000000000000000000000000000001"; + Identity identity = Identity.create(privHex); + PublicKey expected = + new PublicKey("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); + Assertions.assertEquals(expected, identity.getPublicKey()); + } + + @Test + // Verifies that signing produces a Schnorr signature that validates successfully + public void testSignProducesValidSignature() + throws NoSuchAlgorithmException, SchnorrException { + String privHex = "0000000000000000000000000000000000000000000000000000000000000001"; + Identity identity = Identity.create(privHex); + final byte[] message = "hello".getBytes(StandardCharsets.UTF_8); + + ISignable signable = + new ISignable() { + private Signature signature; + + @Override + public Signature getSignature() { + return signature; + } + + @Override + public void setSignature(Signature signature) { + this.signature = signature; + } + + @Override + public Consumer getSignatureConsumer() { + return this::setSignature; + } + + @Override + public Supplier getByteArraySupplier() { + return () -> ByteBuffer.wrap(message); + } + }; + + identity.sign(signable); + + byte[] msgHash = NostrUtil.sha256(message); + boolean verified = + Schnorr.verify( + msgHash, identity.getPublicKey().getRawData(), signable.getSignature().getRawData()); + Assertions.assertTrue(verified); + } + + @Test + // Confirms public key derivation is cached for subsequent calls + public void testPublicKeyCaching() { + Identity identity = Identity.generateRandomIdentity(); + PublicKey first = identity.getPublicKey(); + PublicKey second = identity.getPublicKey(); + Assertions.assertSame(first, second); + } + + @Test + // Ensures that invalid private keys trigger a derivation failure + public void testGetPublicKeyFailure() { + String invalidPriv = "0000000000000000000000000000000000000000000000000000000000000000"; + Identity identity = Identity.create(invalidPriv); + Assertions.assertThrows(IllegalStateException.class, identity::getPublicKey); + } + + @Test + // Ensures that signing with an invalid private key throws SigningException + public void testSignWithInvalidKeyFails() { + String invalidPriv = "0000000000000000000000000000000000000000000000000000000000000000"; + Identity identity = Identity.create(invalidPriv); + + ISignable signable = + new ISignable() { + private Signature signature; + + @Override + public Signature getSignature() { + return signature; + } + + @Override + public void setSignature(Signature signature) { + this.signature = signature; + } + + @Override + public Consumer getSignatureConsumer() { + return this::setSignature; + } + + @Override + public Supplier getByteArraySupplier() { + return () -> ByteBuffer.wrap("msg".getBytes(StandardCharsets.UTF_8)); + } + }; + + Assertions.assertThrows(SigningException.class, () -> identity.sign(signable)); + } +} diff --git a/nostr-java-id/src/test/resources/application-test.properties b/nostr-java-identity/src/test/resources/application-test.properties similarity index 100% rename from nostr-java-id/src/test/resources/application-test.properties rename to nostr-java-identity/src/test/resources/application-test.properties diff --git a/nostr-java-crypto/src/test/resources/junit-platform.properties b/nostr-java-identity/src/test/resources/junit-platform.properties similarity index 100% rename from nostr-java-crypto/src/test/resources/junit-platform.properties rename to nostr-java-identity/src/test/resources/junit-platform.properties diff --git a/nostr-java-util/pom.xml b/nostr-java-util/pom.xml deleted file mode 100644 index 89daa33d4..000000000 --- a/nostr-java-util/pom.xml +++ /dev/null @@ -1,62 +0,0 @@ - - 4.0.0 - - - xyz.tcheeric - nostr-java - 1.3.0 - ../pom.xml - - - nostr-java-util - jar - nostr-java-util - - - - reposilite-releases - https://maven.398ja.xyz/releases - - - reposilite-snapshots - https://maven.398ja.xyz/snapshots - - - - - - org.apache.commons - commons-lang3 - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.module - jackson-module-blackbird - - - - org.projectlombok - lombok - - provided - - - org.slf4j - slf4j-api - - - org.junit.jupiter - junit-jupiter - - test - - - org.junit.platform - junit-platform-launcher - test - - - diff --git a/nostr-java-util/src/main/java/nostr/util/http/DefaultHttpClientProvider.java b/nostr-java-util/src/main/java/nostr/util/http/DefaultHttpClientProvider.java deleted file mode 100644 index 7524f8b1e..000000000 --- a/nostr-java-util/src/main/java/nostr/util/http/DefaultHttpClientProvider.java +++ /dev/null @@ -1,13 +0,0 @@ -package nostr.util.http; - -import java.net.http.HttpClient; -import java.time.Duration; - -/** Default implementation of {@link HttpClientProvider} using Java's HTTP client. */ -public class DefaultHttpClientProvider implements HttpClientProvider { - - @Override - public HttpClient create(Duration connectTimeout) { - return HttpClient.newBuilder().connectTimeout(connectTimeout).build(); - } -} diff --git a/nostr-java-util/src/main/java/nostr/util/http/HttpClientProvider.java b/nostr-java-util/src/main/java/nostr/util/http/HttpClientProvider.java deleted file mode 100644 index 07e7dba8b..000000000 --- a/nostr-java-util/src/main/java/nostr/util/http/HttpClientProvider.java +++ /dev/null @@ -1,16 +0,0 @@ -package nostr.util.http; - -import java.net.http.HttpClient; -import java.time.Duration; - -/** Provides {@link HttpClient} instances with configurable timeouts. */ -public interface HttpClientProvider { - - /** - * Create a new {@link HttpClient} with the given connect timeout. - * - * @param connectTimeout the connection timeout - * @return configured HttpClient instance - */ - HttpClient create(Duration connectTimeout); -} diff --git a/nostr-java-util/src/test/resources/junit-platform.properties b/nostr-java-util/src/test/resources/junit-platform.properties deleted file mode 100644 index a413a5904..000000000 --- a/nostr-java-util/src/test/resources/junit-platform.properties +++ /dev/null @@ -1,7 +0,0 @@ -# junit-platform.properties - -junit.jupiter.execution.parallel.enabled=true -junit.jupiter.execution.parallel.config.strategy=dynamic -junit.jupiter.execution.parallel.mode.default=same_thread -#junit.jupiter.execution.parallel.mode.default=concurrent -#junit.jupiter.execution.parallel.mode.classes.default=concurrent diff --git a/pom.xml b/pom.xml index 7d740d2a0..92a0b6e09 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ xyz.tcheeric nostr-java - 1.3.0 + 2.0.0 pom nostr-java @@ -63,15 +63,10 @@ - nostr-java-util - nostr-java-crypto - nostr-java-base + nostr-java-core nostr-java-event - nostr-java-id - nostr-java-encryption + nostr-java-identity nostr-java-client - nostr-java-api - nostr-java-examples @@ -110,20 +105,10 @@ import - + ${project.groupId} - nostr-java-util - ${project.version} - - - ${project.groupId} - nostr-java-crypto - ${project.version} - - - ${project.groupId} - nostr-java-base + nostr-java-core ${project.version} @@ -133,12 +118,7 @@ ${project.groupId} - nostr-java-id - ${project.version} - - - ${project.groupId} - nostr-java-encryption + nostr-java-identity ${project.version} @@ -146,16 +126,6 @@ nostr-java-client ${project.version} - - ${project.groupId} - nostr-java-api - ${project.version} - - - ${project.groupId} - nostr-java-examples - ${project.version} - From 7d85439a6512e4be9bad888317b711bc9a2c73d0 Mon Sep 17 00:00:00 2001 From: tcheeric Date: Tue, 24 Feb 2026 03:19:46 +0000 Subject: [PATCH 2/2] fix: address PR review comments - Fix SECURE_CODING.md: replace "imani-bridge" with "nostr-java" and fix broken markdown escaping for \n, \r characters - Fix GenericTagDecoder: preserve duplicate tag parameters (Nostr tags are positional arrays where repeated values are valid data) - Fix EventFilter: deep-copy tagFilters map in constructor to prevent mutable state leaking from reused builders - Fix NostrRelayClient: log warning when max-events-per-request limit is reached instead of silently dropping events Co-Authored-By: Claude Opus 4.6 --- docs/developer/SECURE_CODING.md | 7 +++---- .../nostr/client/springwebsocket/NostrRelayClient.java | 3 +++ .../src/main/java/nostr/event/filter/EventFilter.java | 2 +- .../java/nostr/event/json/codec/GenericTagDecoder.java | 4 +--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/developer/SECURE_CODING.md b/docs/developer/SECURE_CODING.md index a905d497b..c253afa14 100644 --- a/docs/developer/SECURE_CODING.md +++ b/docs/developer/SECURE_CODING.md @@ -1,6 +1,6 @@ -# Secure Coding Guidelines for imani-bridge +# Secure Coding Guidelines for nostr-java -This document outlines the mandatory secure coding practices for the `imani-bridge` project. These guidelines are derived from industry best practices (OWASP, Oracle, etc.) and must be followed for all contributions. +This document outlines the mandatory secure coding practices for the `nostr-java` project. These guidelines are derived from industry best practices (OWASP, Oracle, etc.) and must be followed for all contributions. ## 1. Input Validation and Output Encoding @@ -28,8 +28,7 @@ This document outlines the mandatory secure coding practices for the `imani-brid ### Log Injection * **Sanitize Logs:** Ensure user input written to logs does not contain newline characters (` -`, ` `) to prevent log forging. -* **Structured Logging:** Prefer structured logging (JSON) to mitigate format string attacks. +* **Sanitize Logs:** Ensure user input written to logs does not contain newline characters (`\n`, `\r`) to prevent log forging. ## 3. Cryptography & Secrets Management diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java index 45a1c7150..c05bd55b8 100644 --- a/nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java +++ b/nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java @@ -90,6 +90,9 @@ private static final class PendingRequest { void addEvent(String event) { if (events.size() < maxEvents) { events.add(event); + } else if (events.size() == maxEvents) { + log.warn("Max events per request limit ({}) reached; subsequent events will be dropped. " + + "Increase nostr.websocket.max-events-per-request or use narrower filters.", maxEvents); } } diff --git a/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java index 501d566dd..c2d171615 100644 --- a/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java +++ b/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java @@ -46,7 +46,7 @@ private EventFilter(Builder builder) { this.since = builder.since; this.until = builder.until; this.limit = builder.limit; - this.tagFilters = Collections.unmodifiableMap(builder.tagFilters); + this.tagFilters = Collections.unmodifiableMap(new HashMap<>(builder.tagFilters)); } public static Builder builder() { diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java index b5c754575..309946d9b 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java @@ -32,9 +32,7 @@ public T decode(@NonNull String json) throws EventEncodingException { String[] jsonElements = I_DECODER_MAPPER_BLACKBIRD.readValue(json, String[].class); List params = new ArrayList<>(Math.max(0, jsonElements.length - 1)); for (int i = 1; i < jsonElements.length; i++) { - if (!params.contains(jsonElements[i])) { - params.add(jsonElements[i]); - } + params.add(jsonElements[i]); } GenericTag genericTag = new GenericTag(jsonElements[0], params);