Compare commits

...

26 Commits

Author SHA1 Message Date
Wilson Silva
defeba1278
Bump the gem version to v0.7.0 2024-04-13 14:21:34 +01:00
Wilson Silva
955b74054b
Update the changelog for v0.7.0 2024-04-13 14:20:18 +01:00
Wilson Silva
90ab1a6149
Add plain text and coloured logging to the Client
Logs websocket events (connect, message, error and close events)
2024-04-13 14:12:13 +01:00
Wilson Silva
a0cf41bfb4
Expose the relay in the client connection event 2024-04-13 14:12:13 +01:00
Wilson Silva
61eb0459d4
Fix a typo in the client spec 2024-04-13 14:12:13 +01:00
Wilson Silva
0497878d54
Update steep and remove workaround
The maintainer of steep, @soutaro, fixed the bug that I reported :)
2024-04-13 14:12:12 +01:00
Wilson Silva
7946b82aaf
Update json and rubocop-rspec 2024-04-13 14:12:12 +01:00
Wilson Silva
e114656166
Update bun, mermaid, vitepress and vitepress-plugin-mermaid 2024-04-13 14:12:12 +01:00
Wilson Silva
04574cc836
Be more informative when the signature has an invalid type 2024-04-13 14:12:12 +01:00
Wilson Silva
1ff9611051
Remove an unnecessary header in the changelog 2024-03-18 20:15:05 +00:00
Wilson Silva
1d12363af1
Document a common issue 2024-03-15 19:42:37 +00:00
Wilson Silva
9ae68542f1
Bump the gem version to v0.6.0 2024-03-15 19:13:34 +00:00
Wilson Silva
6a5068a552
Update the changelog for v0.6.0 2024-03-15 19:13:34 +00:00
Wilson Silva
470a72d4de
Document how to verify the signature of messages and events 2024-03-15 19:13:34 +00:00
Wilson Silva
86cd6c6baa
Remove blank lines 2024-03-15 19:13:34 +00:00
Wilson Silva
9d6d91e436
Update Bun to v1.0.30 2024-03-15 19:13:34 +00:00
Wilson Silva
838a2db834
Allow key pairs to be destructured 2024-03-15 19:13:34 +00:00
Wilson Silva
7c571d3b12
Simplify the nullable RBS types 2024-03-15 19:13:34 +00:00
Wilson Silva
01010c763f
Allow the verification of signatures and events
Added the methods:
- Event#verify_signature
- Crypto#check_sig!
- Crypto#valid_sig?
- Crypto#sign_message

Fixed a primitive obsession by introducing a Signature class to ensure that signatures are valid Nostr signatures.
2024-03-15 19:13:33 +00:00
Wilson Silva
f8893f9b0e
Update bip-schnorr, dotenv, overcommit, rbs, rspec, rubocop, and rubocop-rspec 2024-03-15 19:13:33 +00:00
Wilson Silva
3788ba4ce5
Add a build badge 2024-03-15 19:13:33 +00:00
Wilson Silva
0f83b8071a
Update Ruby to v3.3 2024-03-15 19:13:33 +00:00
Wilson Silva
c8d633dbea
Fix the @raise documentation of Nostr::Key#initialize 2024-03-15 19:13:33 +00:00
Wilson Silva
df51354d3e
Fix the signature tests in user_spec 2024-03-13 12:53:27 +00:00
Wilson Silva
3765b60a68
Fix the documentation of generate_key_pair 2024-03-13 12:47:16 +00:00
Wilson Silva
17cd2bf0f4
Adopt Architecture Decision Records 2024-03-13 12:46:48 +00:00
67 changed files with 1661 additions and 120 deletions

1
.adr-dir Normal file
View File

@ -0,0 +1 @@
adr

View File

@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
ruby:
- '3.2.0'
- '3.3.0'
steps:
- uses: actions/checkout@v3

View File

@ -5,9 +5,11 @@ require:
- rubocop-rspec
AllCops:
TargetRubyVersion: 3.2
TargetRubyVersion: 3.3
DisplayCopNames: true
NewCops: enable
Exclude:
- docs/**/*
# ----------------------- Gemspec -----------------------

View File

@ -9,7 +9,7 @@
# Offense count: 2
# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 23
Max: 24
# Offense count: 2
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods.

View File

@ -1,2 +1,2 @@
ruby 3.2.2
bun 1.0.11
ruby 3.3.0
bun 1.1.3

View File

@ -4,10 +4,63 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.1/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.7.0] 2024-04-13
### Added
- Added the `Nostr::Client::Logger` class to log connection events, messages sent and received, errors, and connection
closures.
- Added the `Nostr::Client::ColorLogger` class to log events in color using ANSI escape codes.
- Added the `Nostr::Client::PlainLogger` class to log events without color coding.
- Added Architecture Decision Records (ADRs) to document the decision process for logging functionality.
- Added a new common use case document for logging and debugging.
### Changed
- Updated the `Nostr::Client` class to use the `ColorLogger` by default for logging client interactions with relays.
- Updated the `Nostr::Client#connect` method to emit a `:send` event when sending a message.
- Updated the `Nostr::Client#on` method to pass the `relay` parameter to the `:connect` event handler.
- Updated bun to version `1.1.3` (was `1.0.30`).
- Updated the gem `json` to version `2.7` (was `2.6`).
- Updated the gem `mermaid` to version `10.9` (was `10.6`).
- Updated the gem `rubocop-rspec` to version `2.29` (was `2.27`).
- Updated the gem `steep` to version `1.7.dev3` (was `1.6`).
- Updated the gem `vitepress` to version `1.1` (was `1.0.0-rc.25`).
- Updated the gem `vitepress-plugin-mermaid` to version `2.0.16` (was `2.0.15`).
- Updated the error message in `Nostr::InvalidSignatureTypeError` to provide more detail.
### Fixed
Fixed a type-checking issue in `Nostr::Event#verify_signature` by removing a workaround after the steep gem author,
[@soutaro](https://github.com/soutaro) resolved [the problem I reported](https://github.com/soutaro/steep/issues/1079).
## [0.6.0] 2024-03-15
### Added
- Added Architecture Decision Records (ADRs) to document architectural decisions
- Added the `Signature` class to fix the primitive obsession with signatures and to make it easier to work with them
- Added `valid_sig?` and `check_sig!` to the `Crypto` class to verify whether an event's signature is valid
- Added `sign_message` to the `Crypto` class to sign a message
- Added `verify_signature?` to the `Event` class to verify whether an event's signature is valid
- Added `#to_ary` to the `KeyPair` class to enable keypair destructuring
- Added RBS types for `schnorr`
### Changed
- Updated the required Ruby version to `3.3.0` (was `3.2.0`)
- Updated the gem `dotenv` to version `3.1` (was `2.8`)
- Updated the gem `bip-schnorr` to version `0.7` (was `0.6`)
- Updated the gem `overcommit` to version `0.63` (was `0.59`)
- Updated the gem `rbs` to version `3.4` (was `3.3`)
- Updated the gem `rspec` to version `3.13` (was `3.12`)
- Updated the gem `rspec-rubocop` to version `2.27` (was `2.25`)
- Updated the gem `rubocop` to version `1.62` (was `1.57`)
### Fixed
- Fixed a typo in the README (`generate_keypair` -> `generate_key_pair`)
- Fixed a typo in the YARD documentation of `Nostr::Key#initialize` (`ValidationError` -> `KeyValidationError`)
- Fixed YARD example rendering issues in `InvalidKeyFormatError#initialize`, `InvalidKeyLengthError#initialize`,
`InvalidKeyTypeError#initialize`, `Event#initialize`, `EncryptedDirectMessage#initialize` and `Filter#to_h`
@ -17,7 +70,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Added relay message type enums `Nostr::RelayMessageType`
- Compliance with [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) - bech32-formatted strings
- `Nostr::PrivateKey` and `Nostr::PublicKey` to represent private and public keys, respectively
- Added `Nostr::PrivateKey` and `Nostr::PublicKey` to represent private and public keys, respectively
- Added a validation of private and public keys
- Added an ability to convert keys to and from Bech32 format
- Added RBS types for `faye-websocket` and `bech32`
@ -89,7 +142,8 @@ principles of immutability and was a major source of internal complexity as I ne
- Initial release
[unreleased]: https://github.com/wilsonsilva/nostr/compare/v0.5.0...HEAD
[0.7.0]: https://github.com/wilsonsilva/nostr/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/wilsonsilva/nostr/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/wilsonsilva/nostr/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/wilsonsilva/nostr/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/wilsonsilva/nostr/compare/v0.2.0...v0.3.0

View File

@ -1,6 +1,7 @@
# Nostr
[![Gem Version](https://badge.fury.io/rb/nostr.svg)](https://badge.fury.io/rb/nostr)
![Build](https://github.com/wilsonsilva/nostr/actions/workflows/main.yml/badge.svg)
[![Maintainability](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/maintainability)](https://codeclimate.com/github/wilsonsilva/nostr/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/test_coverage)](https://codeclimate.com/github/wilsonsilva/nostr/test_coverage)
@ -63,7 +64,7 @@ keypair = keygen.get_key_pair_from_private_key(
# c) Or create a new keypair
keygen = Nostr::Keygen.new
keypair = keygen.generate_keypair
keypair = keygen.generate_key_pair
# Create a user with the keypair
user = Nostr::User.new(keypair: keypair)
@ -79,7 +80,7 @@ relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
client.connect(relay)
# Listen asynchronously for the connect event
client.on :connect do
client.on :connect do |relay|
# Send the event to the Relay
client.publish(text_note_event)
@ -117,6 +118,9 @@ end
client.on :close do |code, reason|
# You may attempt to reconnect to the relay here
end
# This line keeps the background client from exiting immediately.
gets
```
## 📚 Documentation

View File

@ -0,0 +1,19 @@
# 1. Record architecture decisions
Date: 2024-03-13
## Status
Accepted
## Context
We need to record the architectural decisions made on this project.
## Decision
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
## Consequences
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).

View File

@ -0,0 +1,27 @@
# 2. introduction-of-signature-class
Date: 2024-03-14
## Status
Accepted
## Context
I noticed significant overuse of primitive strings for signatures, which led to widespread and repetitive validation logic, increasing the potential for errors and making the system harder to manage and maintain.
## Decision
I introduced the Nostr::Signature class, choosing to subclass String to leverage string-like behavior while embedding specific validation rules for signatures. This move was aimed at streamlining validation, ensuring consistency, and maintaining the usability of strings.
## Consequences
### Positive
- This design choice has made the codebase cleaner and more robust, reducing the chances of errors related to signature handling. It ensures that all signature instances are valid at creation, leveraging the familiarity and flexibility of string operations without sacrificing the integrity of the data. Moreover, it sets a strong foundation for extending signature-related functionality in the future.
### Negative
- __Performance Concerns:__ Subclassing String might introduce slight performance overheads due to the additional validation logic executed upon instantiation of a Signature object.
- __Integration Challenges:__ Integrating this class into existing systems where strings were used indiscriminately for signatures requires careful refactoring to ensure compatibility. There's also the potential for issues when passing Nostr::Signature objects to libraries or APIs expecting plain strings without the additional constraints.
- __Learning Curve:__ For new team members or contributors, understanding the necessity and functionality of the Nostr::Signature class adds to the learning curve, potentially slowing down initial development efforts as they familiarize themselves with the custom implementation.

View File

@ -0,0 +1,122 @@
# 3. Logging methods vs logger class
Date: 2024-03-19
## Status
Accepted
## Context
I'm deciding between integrating logging directly into the main class or creating a dedicated logger class.
### Option 1: Logging methods
The first approach weaves logging actions into the operational code, resulting in a tight coupling of functionality and
logging. Classes should be open for extension but closed for modification, and this strategy violates that principle.
```ruby
class Client
def connect(relay)
execute_within_an_em_thread do
client = build_websocket_client(relay.url)
parent_to_child_channel.subscribe do |msg|
client.send(msg)
emit(:send, msg)
log_send(msg) # <------ new code
end
client.on :open do
child_to_parent_channel.push(type: :open, relay:)
log_connection_opened(relay) # <------ new code
end
client.on :message do |event|
child_to_parent_channel.push(type: :message, data: event.data)
log_message_received(event.data) # <------ new code
end
client.on :error do |event|
child_to_parent_channel.push(type: :error, message: event.message)
log_error(event.message) # <------ new code
end
client.on :close do |event|
child_to_parent_channel.push(type: :close, code: event.code, reason: event.reason)
log_connection_closed(event.code, event.reason) # <------ new code
end
end
# ------ new code below ------
def log_send(msg)
logger.info("Message sent: #{msg}")
end
def log_connection_opened(relay)
logger.info("Connection opened to #{relay.url}")
end
def log_message_received(data)
logger.info("Message received: #{data}")
end
def log_error(message)
logger.error("Error: #{message}")
end
def log_connection_closed(code, reason)
logger.info("Connection closed with code: #{code}, reason: #{reason}")
end
end
end
```
### Option 2: Logger class
The second strategy separates logging into its own class, promoting cleaner code and adherence to the Single
Responsibility Principle. Client already exposes events that can be tapped into, so the logger class can listen to these
events and log accordingly.
```ruby
class ClientLogger
def attach_to(client)
logger_instance = self
client.on(:connect) { |relay| logger_instance.on_connect(relay) }
client.on(:message) { |message| logger_instance.on_message(message) }
client.on(:send) { |message| logger_instance.on_send(message) }
client.on(:error) { |message| logger_instance.on_error(message) }
client.on(:close) { |code, reason| logger_instance.on_close(code, reason) }
end
def on_connect(relay); end
def on_message(message); end
def on_send(message); end
def on_error(message); end
def on_close(code, reason); end
end
client = Nostr::Client.new
logger = Nostr::ClientLogger.new
logger.attach_to(client)
```
This approach decouples logging from the main class, making it easier to maintain and extend the logging system without
affecting the core logic.
## Decision
I've chosen the dedicated logger class route. This choice is driven by a desire for extensibility in the logging system.
With a separate logger, I can easily modify logging behavior—like changing formats, adjusting verbosity levels,
switching colors, or altering output destinations (files, networks, etc.) — without needing to rewrite any of the main
operational code.
## Consequences
Adopting a dedicated logger class offers greater flexibility and simplifies maintenance, making it straightforward to
adjust how and what I log independently of the core logic. This separation of concerns means that any future changes
to logging preferences or requirements can be implemented quickly and without risk to the main class's functionality.
However, it's important to manage the integration carefully to avoid introducing complexity, such as handling
dependencies and ensuring seamless communication between the main operations and the logging system.

View File

@ -0,0 +1,66 @@
# 3. Default Logging Activation
Date: 2024-03-19
## Status
Accepted
## Context
Logging provides visibility into the gem's behavior and helps to diagnose issues. The decision centered on whether
to enable logging by default or require manual activation.
### Option 1: On-demand Logging
```ruby
client = Nostr::Client.new
logger = Nostr::ClientLogger.new
logger.attach_to(client)
```
#### Advantages:
- Offers users flexibility and control over logging.
- Conserves resources by logging only when needed.
#### Disadvantages:
- Potential to miss critical logs if not enabled.
- Requires users to read and understand documentation to enable logging.
- Requires users to manually activate and configure logging.
### Option 2: Automatic Logging
```ruby
class Client
def initialize(logger: ClientLogger.new)
@logger = logger
logger&.attach_to(self)
end
end
client = Nostr::Client.new
```
#### Advantages:
- Ensures comprehensive logging without user intervention.
- Simplifies debugging and monitoring.
- Balances logging detail with performance impact.
#### Disadvantages:
- Needs additional steps to disable logging.
## Decision
Logging will be enabled by default. Users can disable logging by passing a `nil` logger to the client:
```ruby
client = Nostr::Client.new(logger: nil)
```
## Consequences
Enabling logging by default favors ease of use and simplifies the developer's experience.

32
adr/0005-logger-types.md Normal file
View File

@ -0,0 +1,32 @@
# 3. Logger Types
Date: 2024-04-01
## Status
Accepted
## Context
In developing the `Nostr::Client` logging mechanism, I identified a need to cater to multiple development environments
and developer preferences. The consideration was whether to implement a singular logger type or to introduce
multiple, specialized loggers. The alternatives were:
- A single `ClientLogger` providing a one-size-fits-all solution.
- Multiple logger types:
- `Nostr::Client::Logger`: The base class for logging, providing core functionalities.
- `Nostr::Client::ColorLogger`: An extension of the base logger, introducing color-coded outputs for enhanced readability in environments that support ANSI colors.
- `Nostr::Client::PlainLogger`: A variation that produces logs without color coding, suitable for environments lacking color support or for users preferring plain text.
## Decision
I decided to implement the latter option: three specific kinds of loggers (`Nostr::Client::Logger`,
`Nostr::Client::ColorLogger`, and `Nostr::Client::PlainLogger`). This approach is intended to offer flexibility and
cater to the varied preferences and requirements of our users, recognizing the diverse environments in which our
library might be used.
## Consequences
- `Developer Choice`: Developers gain the ability to select the one that best matches their needs and environmental constraints, thereby enhancing the library's usability.
- `Code Complexity`: While introducing multiple logger types increases the library's code complexity, this is offset by the significant gain in flexibility and user satisfaction.
- `Broad Compatibility`: This decision ensures that the logging mechanism is adaptable to a wide range of operational environments, enhancing the library's overall robustness and accessibility.

View File

@ -78,7 +78,10 @@ export default defineConfig(withMermaid({
text: 'Common use cases',
collapsed: false,
items: [
{ text: 'Logging and debugging', link: '/common-use-cases/logging-and-debugging' },
{ text: 'Bech32 enc/decoding (NIP-19)', link: '/common-use-cases/bech32-encoding-and-decoding-(NIP-19)' },
{ text: 'Signing/verifying messages', link: '/common-use-cases/signing-and-verifying-messages' },
{ text: 'Signing/verifying events', link: '/common-use-cases/signing-and-verifying-events' },
]
},
{

Binary file not shown.

View File

@ -0,0 +1,60 @@
# Logging and debugging
The `Nostr::Client` class provides built-in logging functionality to help you debug and monitor client interactions with
relays. By default, the client uses the `ColorLogger`, which logs events in color. However, you can customize the
logging behavior or disable it entirely.
## Disabling logging
To instantiate a client without any logging, simply pass `logger: nil` when creating the client instance:
```ruby
client = Nostr::Client.new(logger: nil)
```
This will disable all logging for the client.
## Formatting the logging
The `Nostr::Client::Logger` class is the base class for logging functionality. It defines the following methods for
logging different events:
- `on_connect(relay)`: Logs when the client connects to a relay.
- `on_message(message)`: Logs a message received from the relay.
- `on_send(message)`: Logs a message sent to the relay.
- `on_error(message)`: Logs an error message.
- `on_close(code, reason)`: Logs when the connection with a relay is closed.
You can create your own logger by subclassing `Nostr::Client::Logger` and overriding these methods to customize the
logging format.
The `Nostr::Client::ColorLogger` is a built-in logger that logs events in color. It uses ANSI escape codes to add color
to the log output. Here's an example of how the ColorLogger formats the log messages:
- Connection: `"\u001b[32m\u001b[1mConnected to the relay\u001b[22m #{relay.name} (#{relay.url})\u001b[0m"`
- Message received: `"\u001b[32m\u001b[1m◄-\u001b[0m #{message}"`
- Message sent: `"\u001b[32m\u001b[1m-►\u001b[0m #{message}"`
- Error: `"\u001b[31m\u001b[1mError: \u001b[22m#{message}\u001b[0m"`
- Connection closed: `"\u001b[31m\u001b[1mConnection closed: \u001b[22m#{reason} (##{code})\u001b[0m"`
## Plain text logging
If you prefer plain text logging without colors, you can use the `Nostr::Client::PlainLogger`. This logger formats the
log messages in a simple, readable format without any ANSI escape codes.
To use the `PlainLogger`, pass it as the `logger` option when creating the client instance:
```ruby
client = Nostr::Client.new(logger: Nostr::Client::PlainLogger.new)
```
The `PlainLogger` formats the log messages as follows:
- Connection: `"Connected to the relay #{relay.name} (#{relay.url})"`
- Message received: `"◄- #{message}"`
- Message sent: `"-► #{message}"`
- Error: `"Error: #{message}"`
- Connection closed: `"Connection closed: #{reason} (##{code})"`
By using the appropriate logger or creating your own custom logger, you can effectively debug and monitor your Nostr
client's interactions with relays.

View File

@ -0,0 +1,50 @@
# Signing and verifying events
Signing an event in Nostr proves it was sent by the owner of a specific private key.
## Signing an event
To sign an event, use the private key associated with the event's creator. Here's how to sign a message using a
predefined keypair:
```ruby{14}
require 'nostr'
private_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
public_key = Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
event = Nostr::Event.new(
pubkey: public_key.to_s,
kind: Nostr::EventKind::TEXT_NOTE,
content: 'We did it with security, now were going to do it with the economy.',
created_at: Time.now.to_i,
)
# Sign the event with the private key
event.sign(private_key)
puts "Event ID: #{event.id}"
puts "Event Signature: #{event.sig}"
```
## Verifying an event's signature
To verify an event, you must ensure the event's signature is valid. This indicates the event was created by the owner
of the corresponding public key.
When the event was signed with the private key corresponding to the public key, the `verify_signature` method will
return `true`.
```ruby
event.verify_signature # => true
```
And when the event was not signed with the private key corresponding to the public key, the `verify_signature` method
will return `false`.
An event without an `id`, `pubkey`, `sig` is considered invalid and will return `false` when calling `verify_signature`.
```ruby
other_public_key = Nostr::PublicKey.new('10be96d345ed58d923a734560680f1adfd2b1006c28ac93b8e1b032a9a32c6e9')
event.verify_signature # => false
```

View File

@ -0,0 +1,43 @@
# Signing and verifying messages
Signing a message in Nostr proves it was sent by the owner of a specific private key.
## Signing a message
To sign a message, you'll need a private key. Here's how to sign a message using a predefined keypair:
```ruby{9}
require 'nostr'
private_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
public_key = Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
message = 'We did it with security, now were going to do it with the economy.' # The message you want to sign
crypto = Nostr::Crypto.new
signature = crypto.sign_message(message, private_key)
signature # => "d7a0aac1fadcddf1aa2949bedfcdf25ce0c1604e648e55d31431fdacbff8e8256f7c2166d98292f80bc5f79105a0b6e8a89236a47d97cf5d0e7cc1ebf34dea5c"
```
## Verifying a signature
To verify a signature, you need the original message, the public key of the signer, and the signature.
```ruby
crypto.valid_sig?(message, public_key, signature) # => true
crypto.check_sig!(message, public_key, signature) # => true
```
When the message was not signed with the private key corresponding to the public key, the `valid_sig?` method will return `false`.
```ruby
other_public_key = Nostr::PublicKey.new('10be96d345ed58d923a734560680f1adfd2b1006c28ac93b8e1b032a9a32c6e9')
crypto.valid_sig?(message, public_key, signature) # => false
```
And when the message was not signed with the private key corresponding to the public key, the `check_sig!` method will raise an error.
```ruby
other_public_key = Nostr::PublicKey.new('10be96d345ed58d923a734560680f1adfd2b1006c28ac93b8e1b032a9a32c6e9')
crypto.check_sig!(message, other_public_key, signature) # => Schnorr::InvalidSignatureError: signature verification failed
```

View File

@ -28,7 +28,7 @@ The `:connect` event is fired when a connection with a WebSocket is opened. You
client = Nostr::Client.new
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
client.on :connect do
client.on :connect do |relay|
# When this block executes, you're connected to the relay
end

View File

@ -35,6 +35,7 @@ classDiagram
serialize()
to_h()
sign(private_key)
verify_signature()
}
class Subscription {
id
@ -110,7 +111,7 @@ keypair = keygen.get_key_pair_from_private_key(
# c) Or create a new keypair
keygen = Nostr::Keygen.new
keypair = keygen.generate_keypair
keypair = keygen.generate_key_pair
# Create a user with the keypair
user = Nostr::User.new(keypair: keypair)
@ -164,6 +165,9 @@ end
client.on :close do |code, reason|
# You may attempt to reconnect to the relay here
end
# This line keeps the background client from exiting immediately.
gets
```
Beyond what's covered here, the Nostr protocol and this gem boast a wealth of additional functionalities. For an

View File

@ -39,6 +39,4 @@ features:
- title: Fully typed
details: All code is typed with <a href="https://rubygems.org/gems/rbs" target="_blank">RBS</a> with the help of <a href="https://rubygems.org/gems/typeprof" target="_blank">TypeProf</a>. Type correctness is enforced by <a href="https://rubygems.org/gems/steep" target="_blank">Steep</a>.
icon: ✅
---

View File

@ -5,8 +5,8 @@
"docs:preview": "vitepress preview"
},
"devDependencies": {
"mermaid": "^10.6.1",
"vitepress": "^1.0.0-rc.25",
"vitepress-plugin-mermaid": "^2.0.15"
"mermaid": "^10.9.0",
"vitepress": "^1.1.0",
"vitepress-plugin-mermaid": "^2.0.16"
}
}

View File

@ -12,9 +12,12 @@ require_relative 'nostr/relay'
require_relative 'nostr/relay_message_type'
require_relative 'nostr/key_pair'
require_relative 'nostr/event_kind'
require_relative 'nostr/signature'
require_relative 'nostr/event'
require_relative 'nostr/events/encrypted_direct_message'
require_relative 'nostr/client'
require_relative 'nostr/client/logger'
require_relative 'nostr/client/color_logger'
require_relative 'nostr/user'
require_relative 'nostr/key'
require_relative 'nostr/private_key'

View File

@ -18,11 +18,19 @@ module Nostr
# @api public
#
# @example Instantiating a client that logs all the events it sends and receives
# client = Nostr::Client.new(debug: true)
# client = Nostr::Client.new
#
def initialize
# @example Instantiating a client with no logging
# client = Nostr::Client.new(logger: nil)
#
# @example Instantiating a client with your own logger
# client = Nostr::Client.new(logger: YourLogger.new)
#
def initialize(logger: ColorLogger.new)
@subscriptions = {}
@logger = logger
logger&.attach_to(self)
initialize_channels
end
@ -40,11 +48,11 @@ module Nostr
#
def connect(relay)
execute_within_an_em_thread do
client = Faye::WebSocket::Client.new(relay.url, [], { tls: { verify_peer: false } })
parent_to_child_channel.subscribe { |msg| client.send(msg) }
client = build_websocket_client(relay.url)
parent_to_child_channel.subscribe { |msg| client.send(msg) && emit(:send, msg) }
client.on :open do
child_to_parent_channel.push(type: :open)
child_to_parent_channel.push(type: :open, relay:)
end
client.on :message do |event|
@ -122,6 +130,14 @@ module Nostr
private
# The logger that prints all the events that the client sends and receives
#
# @api private
#
# @return [ClientLogger]
#
attr_reader :logger
# The subscriptions that the client has created
#
# @api private
@ -167,11 +183,21 @@ module Nostr
@child_to_parent_channel = EventMachine::Channel.new
child_to_parent_channel.subscribe do |msg|
emit :connect if msg[:type] == :open
emit :connect, msg[:relay] if msg[:type] == :open
emit :message, msg[:data] if msg[:type] == :message
emit :error, msg[:message] if msg[:type] == :error
emit :close, msg[:code], msg[:reason] if msg[:type] == :close
end
end
# Builds a websocket client
#
# @api private
#
# @return [Faye::WebSocket::Client]
#
def build_websocket_client(relay_url)
Faye::WebSocket::Client.new(relay_url, [], { tls: { verify_peer: false } })
end
end
end

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
module Nostr
class Client
# Logs connection events, messages sent and received, errors, and connection closures in color.
class ColorLogger < Logger
# Logs connection to a relay
#
# @api private
#
# @param [Nostr::Relay] relay The relay the client connected to.
#
# @return [void]
#
def on_connect(relay)
puts "\u001b[32m\u001b[1mConnected to the relay\u001b[22m #{relay.name} (#{relay.url})\u001b[0m"
end
# Logs a message received from the relay
#
# @api private
#
# @param [String] message The message received.
#
# @return [void]
#
def on_message(message)
puts "\u001b[32m\u001b[1m◄-\u001b[0m #{message}"
end
# Logs a message sent to the relay
#
# @api private
#
# @param [String] message The message sent.
#
# @return [void]
#
def on_send(message)
puts "\u001b[32m\u001b[1m-►\u001b[0m #{message}"
end
# Logs an error message
#
# @api private
#
# @param [String] message The error message.
#
# @return [void]
#
def on_error(message)
puts "\u001b[31m\u001b[1mError: \u001b[22m#{message}\u001b[0m"
end
# Logs a closure of connection with a relay
#
# @api private
#
# @param [String] code The closure code.
# @param [String] reason The reason for the closure.
#
# @return [void]
#
def on_close(code, reason)
puts "\u001b[31m\u001b[1mConnection closed: \u001b[22m#{reason} (##{code})\u001b[0m"
end
end
end
end

View File

@ -0,0 +1,85 @@
# frozen_string_literal: true
module Nostr
class Client
# Logs connection events, messages sent and received, errors, and connection closures.
class Logger
# Attaches event handlers to the specified Nostr client for logging purposes
#
# @api public
#
# @example Attaching the logger to a client
# client = Nostr::Client.new
# logger = Nostr::Client::Logger.new
# logger.attach_to(client)
#
# # Now, actions like connecting, sending messages, receiving messages,
# # errors, and closing the connection will be logged to the console.
#
# @param [Nostr::Client] client The client to attach logging functionality to.
#
# @return [void]
#
def attach_to(client)
logger_instance = self
client.on(:connect) { |relay| logger_instance.on_connect(relay) }
client.on(:message) { |message| logger_instance.on_message(message) }
client.on(:send) { |message| logger_instance.on_send(message) }
client.on(:error) { |message| logger_instance.on_error(message) }
client.on(:close) { |code, reason| logger_instance.on_close(code, reason) }
end
# Logs connection to a relay
#
# @api private
#
# @param [Nostr::Relay] relay The relay the client connected to.
#
# @return [void]
#
def on_connect(relay); end
# Logs a message received from the relay
#
# @api private
#
# @param [String] message The message received.
#
# @return [void]
#
def on_message(message); end
# Logs a message sent to the relay
#
# @api private
#
# @param [String] message The message sent.
#
# @return [void]
#
def on_send(message); end
# Logs an error message
#
# @api private
#
# @param [String] message The error message.
#
# @return [void]
#
def on_error(message); end
# Logs a closure of connection with a relay
#
# @api private
#
# @param [String] code The closure code.
# @param [String] reason The reason for the closure.
#
# @return [void]
#
def on_close(code, reason); end
end
end
end

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
module Nostr
class Client
# Logs connection events, messages sent and received, errors, and connection closures.
class PlainLogger < Logger
# Logs connection to a relay
#
# @api private
#
# @param [Nostr::Relay] relay The relay the client connected to.
#
# @return [void]
#
def on_connect(relay)
puts "Connected to the relay #{relay.name} (#{relay.url})"
end
# Logs a message received from the relay
#
# @api private
#
# @param [String] message The message received.
#
# @return [void]
#
def on_message(message)
puts "◄- #{message}"
end
# Logs a message sent to the relay
#
# @api private
#
# @param [String] message The message sent.
#
# @return [void]
#
def on_send(message)
puts "-► #{message}"
end
# Logs an error message
#
# @api private
#
# @param [String] message The error message.
#
# @return [void]
#
def on_error(message)
puts "Error: #{message}"
end
# Logs a closure of connection with a relay
#
# @api private
#
# @param [String] code The closure code.
# @param [String] reason The reason for the closure.
#
# @return [void]
#
def on_close(code, reason)
puts "Connection closed: #{reason} (##{code})"
end
end
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Nostr
# Performs cryptographic operations on a +Nostr::Event+.
# Performs cryptographic operations.
class Crypto
# Numeric base of the OpenSSL big number used in an event content's encryption.
#
@ -90,17 +90,93 @@ module Nostr
#
def sign_event(event, private_key)
event_digest = hash_event(event)
hex_private_key = Array(private_key).pack('H*')
hex_message = Array(event_digest).pack('H*')
event_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
signature = sign_message(event_digest, private_key)
event.id = event_digest
event.sig = event_signature
event.sig = signature
event
end
# Signs a message using the Schnorr signature algorithm
#
# @api public
#
# @example Signing a message
# crypto = Nostr::Crypto.new
# message = 'Viva la libertad carajo'
# private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
# signature = crypto.sign_message(message, private_key)
# signature # => 'b2115694a576f5bdcebf8c0951a3c7adcfbdb17b11cb9e6d6b7017691138bc6' \
# '38fee642a7bd26f71b313a7057181294198900a9770d1435e43f182acf3d34c26'
#
# @param [String] message The message to be signed
# @param [PrivateKey] private_key The private key used for signing
#
# @return [Signature] A signature object containing the signature as a 64-byte hexadecimal string.
#
def sign_message(message, private_key)
hex_private_key = Array(private_key).pack('H*')
hex_message = Array(message).pack('H*')
hex_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
Signature.new(hex_signature.to_s)
end
# Verifies the given {Signature} and returns true if it is valid
#
# @api public
#
# @example Checking a signature
# public_key = Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6')
# private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
# message = 'Viva la libertad carajo'
# crypto = Nostr::Crypto.new
# signature = crypto.sign_message(message, private_key)
# valid = crypto.valid_sig?(message, public_key, signature)
# valid # => true
#
# @see #check_sig!
#
# @param [String] message A message to be signed with binary format.
# @param [PublicKey] public_key The public key with binary format.
# @param [Signature] signature The signature with binary format.
#
# @return [Boolean] whether signature is valid.
#
def valid_sig?(message, public_key, signature)
signature = Schnorr::Signature.decode([signature].pack('H*'))
Schnorr.valid_sig?([message].pack('H*'), [public_key].pack('H*'), signature.encode)
end
# Verifies the given {Signature} and raises an +Schnorr::InvalidSignatureError+ if it is invalid
#
# @api public
#
# @example Checking a signature
# public_key = Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6')
# private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
# message = 'Viva la libertad carajo'
# crypto = Nostr::Crypto.new
# signature = crypto.sign_message(message, private_key)
# valid = crypto.valid_sig?(message, public_key, signature)
# valid # => true
#
# @see #valid_sig?
#
# @param [String] message A message to be signed with binary format.
# @param [PublicKey] public_key The public key with binary format.
# @param [Signature] signature The signature with binary format.
#
# @raise [Schnorr::InvalidSignatureError] if the signature is invalid.
#
# @return [Boolean] whether signature is valid.
#
def check_sig!(message, public_key, signature)
signature = Schnorr::Signature.decode([signature].pack('H*'))
Schnorr.check_sig!([message].pack('H*'), [public_key].pack('H*'), signature.encode)
end
private
# Finds a shared key between two keys

View File

@ -6,3 +6,7 @@ require_relative 'errors/invalid_hrp_error'
require_relative 'errors/invalid_key_type_error'
require_relative 'errors/invalid_key_length_error'
require_relative 'errors/invalid_key_format_error'
require_relative 'errors/signature_validation_error'
require_relative 'errors/invalid_signature_type_error'
require_relative 'errors/invalid_signature_length_error'
require_relative 'errors/invalid_signature_format_error'

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Nostr
# Raised when the signature is in an invalid format
#
# @api public
#
class InvalidSignatureFormatError < SignatureValidationError
# Initializes the error
#
# @example
# InvalidSignatureFormatError.new
#
def initialize
super('Only lowercase hexadecimal characters are allowed in signatures.')
end
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Nostr
# Raised when the signature's length is not 128 characters
#
# @api public
#
class InvalidSignatureLengthError < SignatureValidationError
# Initializes the error
#
# @example
# InvalidSignatureLengthError.new
#
def initialize
super('Invalid signature length. It should have 128 characters.')
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Nostr
# Raised when the signature is not a string
#
# @api public
#
class InvalidSignatureTypeError < SignatureValidationError
# Initializes the error
#
# @example
# InvalidSignatureTypeError.new
#
def initialize = super('Invalid signature type. It must be a string with lowercase hexadecimal characters.')
end
end

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
module Nostr
# Base class for all signature validation errors
class SignatureValidationError < Error; end
end

View File

@ -181,6 +181,33 @@ module Nostr
crypto.sign_event(self, private_key)
end
# Verifies if the signature of the event is valid. A valid signature means that the event was signed by the owner
#
# @api public
#
# @example Verifying the signature of an event
# event = Nostr::Event.new(
# id: '90b75b78daf883ae57fbcc414d43faa028560b3211ee58e4ea82bf395bb82042',
# pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
# created_at: 1667422587,
# kind: Nostr::EventKind::TEXT_NOTE,
# content: 'Your feedback is appreciated, now pay $8',
# sig: '32f18adebe942e19b171c1c7d2fb27ce794dfea4155e289dca7952b43ed1ec39' \
# '1d3dc198ba2761bc6d40c737a6eaf4edcc8963acabd3bfcebd04f16637025bdc'
# )
#
# event.verify_signature # => true
#
# @return [Boolean] Whether the signature is valid or not.
#
def verify_signature
crypto = Crypto.new
return false if id.nil? || pubkey.nil? || sig.nil?
crypto.valid_sig?(id, pubkey, sig)
end
# Serializes the event, to obtain a SHA256 digest of it
#
# @api public

View File

@ -18,14 +18,14 @@ module Nostr
#
LENGTH = 64
# Instantiates a new key. Can't be used directly because this is an abstract class. Raises a +ValidationError+
# Instantiates a new key. Can't be used directly because this is an abstract class. Raises a +KeyValidationError+
#
# @see Nostr::PrivateKey
# @see Nostr::PublicKey
#
# @param [String] hex_value Hex-encoded value of the key
#
# @raise [ValidationError]
# @raise [KeyValidationError]
#
def initialize(hex_value)
validate_hex_value(hex_value)

View File

@ -38,8 +38,8 @@ module Nostr
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
#
# @raise ArgumentError when the private key is not a +PrivateKey+
# @raise ArgumentError when the public key is not a +PublicKey+
# @raise ArgumentError when the private key is not a {PrivateKey}
# @raise ArgumentError when the public key is not a {PublicKey}
#
def initialize(private_key:, public_key:)
validate_keys(private_key, public_key)
@ -48,6 +48,30 @@ module Nostr
@public_key = public_key
end
# Allows array destructuring of the KeyPair, enabling the extraction of +PrivateKey+ and +PublicKey+ separately
#
# @api public
#
# @example Implicit usage of `to_ary` for destructuring
# keypair = Nostr::KeyPair.new(
# private_key: Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d'),
# public_key: Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6'),
# )
# # The `to_ary` method can be implicitly used for array destructuring:
# private_key, public_key = keypair
# # Now `private_key` and `public_key` hold the respective values.
#
# @example Explicit usage of `to_ary`
# array_representation = keypair.to_ary
# # array_representation is now an array: [PrivateKey, PublicKey]
# # where PrivateKey and PublicKey are the respective objects.
#
# @return [Array<PrivateKey, PublicKey>] An array containing the {PrivateKey} and {PublicKey} in that order
#
def to_ary
[private_key, public_key]
end
private
# Validates the keys

View File

@ -22,7 +22,7 @@ module Nostr
# @api public
#
# @example
# keypair = keygen.generate_keypair
# keypair = keygen.generate_key_pair
# keypair # #<Nostr::KeyPair:0x0000000107bd3550
# @private_key="893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900",
# @public_key="2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558">

67
lib/nostr/signature.rb Normal file
View File

@ -0,0 +1,67 @@
# frozen_string_literal: true
module Nostr
# 64-bytes lowercase hex of the signature of the sha256 hash of the serialized event data,
# which is the same as the "id" field
class Signature < String
# The regular expression for hexadecimal lowercase characters
#
# @return [Regexp] The regular expression for hexadecimal lowercase characters
#
FORMAT = /^[a-f0-9]+$/
# The length of the signature in hex format
#
# @return [Integer] The length of the signature in hex format
#
LENGTH = 128
# Instantiates a new Signature
#
# @api public
#
# @example Instantiating a new signature
# Nostr::Signature.new(
# 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
# '06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
# )
#
# @param [String] hex_value Hex-encoded value of the signature
#
# @raise [SignatureValidationError]
#
def initialize(hex_value)
validate(hex_value)
super(hex_value)
end
private
# Hex-encoded value of the signature
#
# @api private
#
# @return [String] hex_value Hex-encoded value of the signature
#
attr_reader :hex_value
# Validates the hex value of the signature
#
# @api private
#
# @param [String] hex_value The signature in hex format
#
# @raise InvalidSignatureTypeError when the signature is not a string
# @raise InvalidSignatureLengthError when the signature's length is not 128 characters
# @raise InvalidSignatureFormatError when the signature is in an invalid format
#
# @return [void]
#
def validate(hex_value)
raise InvalidSignatureTypeError unless hex_value.is_a?(String)
raise InvalidSignatureLengthError unless hex_value.size == LENGTH
raise InvalidSignatureFormatError unless hex_value.match(FORMAT)
end
end
end

View File

@ -2,5 +2,5 @@
module Nostr
# The version of the gem
VERSION = '0.5.0'
VERSION = '0.7.0'
end

View File

@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
spec.description = 'Client and relay implementation of the Nostr protocol.'
spec.homepage = 'https://nostr-ruby.com/'
spec.license = 'MIT'
spec.required_ruby_version = '>= 3.2.0'
spec.required_ruby_version = '>= 3.3.0'
spec.metadata['rubygems_mfa_required'] = 'true'
spec.metadata['homepage_uri'] = spec.homepage
@ -32,32 +32,32 @@ Gem::Specification.new do |spec|
spec.require_paths = ['lib']
spec.add_dependency 'bech32', '~> 1.4'
spec.add_dependency 'bip-schnorr', '~> 0.6'
spec.add_dependency 'bip-schnorr', '~> 0.7'
spec.add_dependency 'ecdsa', '~> 1.2'
spec.add_dependency 'event_emitter', '~> 0.2'
spec.add_dependency 'faye-websocket', '~> 0.11'
spec.add_dependency 'json', '~> 2.6'
spec.add_dependency 'json', '~> 2.7'
spec.add_development_dependency 'bundler-audit', '~> 0.9'
spec.add_development_dependency 'dotenv', '~> 2.8'
spec.add_development_dependency 'dotenv', '~> 3.1'
spec.add_development_dependency 'guard', '~> 2.18'
spec.add_development_dependency 'guard-bundler', '~> 3.0'
spec.add_development_dependency 'guard-bundler-audit', '~> 0.1'
spec.add_development_dependency 'guard-rspec', '~> 4.7'
spec.add_development_dependency 'guard-rubocop', '~> 1.5'
spec.add_development_dependency 'overcommit', '~> 0.59'
spec.add_development_dependency 'overcommit', '~> 0.63'
spec.add_development_dependency 'pry', '~> 0.14'
spec.add_development_dependency 'puma', '~> 6.4'
spec.add_development_dependency 'rack', '~> 3.0'
spec.add_development_dependency 'rake', '~> 13.1'
spec.add_development_dependency 'rbs', '~> 3.3'
spec.add_development_dependency 'rspec', '~> 3.12'
spec.add_development_dependency 'rubocop', '~> 1.57'
spec.add_development_dependency 'rbs', '~> 3.4'
spec.add_development_dependency 'rspec', '~> 3.13'
spec.add_development_dependency 'rubocop', '~> 1.62'
spec.add_development_dependency 'rubocop-rake', '~> 0.6'
spec.add_development_dependency 'rubocop-rspec', '2.25'
spec.add_development_dependency 'rubocop-rspec', '2.29'
spec.add_development_dependency 'simplecov', '= 0.17'
spec.add_development_dependency 'simplecov-console', '~> 0.9'
spec.add_development_dependency 'steep', '~> 1.6'
spec.add_development_dependency 'steep', '~> 1.7.dev3'
spec.add_development_dependency 'typeprof', '~> 0.21'
spec.add_development_dependency 'yard', '~> 0.9'
spec.add_development_dependency 'yard-junk', '~> 0.0.9'

View File

@ -10,11 +10,13 @@ module Nostr
private
attr_reader logger: Logger
attr_reader subscriptions: Hash[String, Subscription]
attr_reader parent_to_child_channel: EventMachine::Channel
attr_reader child_to_parent_channel: EventMachine::Channel
def execute_within_an_em_thread: { -> void } -> Thread
def initialize_channels: -> void
def build_websocket_client: (String relay_name) -> Faye::WebSocket::Client
end
end

View File

@ -0,0 +1,6 @@
module Nostr
class Client
class ColorLogger < Logger
end
end
end

View File

@ -0,0 +1,12 @@
module Nostr
class Client
class Logger
def attach_to: (Client client) -> void
def on_connect: (Relay relay) -> void
def on_message: (String message) -> void
def on_send: (String message) -> void
def on_error: (String message) -> void
def on_close: (String code, String reason) -> void
end
end
end

View File

@ -0,0 +1,6 @@
module Nostr
class Client
class PlainLogger < Logger
end
end
end

View File

@ -7,6 +7,9 @@ module Nostr
def encrypt_text: (PrivateKey, PublicKey, String) -> String
def decrypt_text: (PrivateKey, PublicKey, String) -> String
def sign_event: (Event, PrivateKey) -> Event
def sign_message: (String, PrivateKey) -> Signature
def valid_sig?: (String, PublicKey, Signature) -> bool
def check_sig!: (String, PublicKey, Signature) -> bool
private

View File

@ -0,0 +1,5 @@
module Nostr
class InvalidSignatureFormatError < SignatureValidationError
def initialize: -> void
end
end

View File

@ -0,0 +1,5 @@
module Nostr
class InvalidSignatureLengthError < SignatureValidationError
def initialize: -> void
end
end

View File

@ -0,0 +1,5 @@
module Nostr
class InvalidSignatureTypeError < SignatureValidationError
def initialize: -> void
end
end

View File

@ -0,0 +1,4 @@
module Nostr
class SignatureValidationError < Error
end
end

View File

@ -5,8 +5,8 @@ module Nostr
attr_reader kind: Integer
attr_reader tags: Array[Array[String]]
attr_reader content: String
attr_accessor id: String?|nil
attr_accessor sig: String?|nil
attr_accessor id: String?
attr_accessor sig: Signature?
def initialize: (
pubkey: PublicKey,
@ -14,24 +14,25 @@ module Nostr
content: String,
?created_at: Integer,
?tags: Array[Array[String]],
?id: String|nil,
?sig: String|nil
?id: String?,
?sig: Signature?
) -> void
def serialize: -> [Integer, String, Integer, Integer, Array[Array[String]], String]
def to_h: -> {
id: String?|nil,
id: String?,
pubkey: String,
created_at: Integer,
kind: Integer,
tags: Array[Array[String]],
content: String,
sig: String?|nil
sig: String?
}
def ==: (Event other) -> bool
def sign:(PrivateKey) -> Event
def verify_signature: -> bool
def add_event_reference: (String) -> Array[Array[String]]
def add_pubkey_reference: (PublicKey) -> Array[Array[String]]

View File

@ -5,6 +5,7 @@ module Nostr
attr_reader public_key: PublicKey
def initialize: (private_key: PrivateKey, public_key: PublicKey) -> void
def to_ary: -> [PrivateKey, PublicKey]
private

14
sig/nostr/signature.rbs Normal file
View File

@ -0,0 +1,14 @@
module Nostr
class Signature < String
FORMAT: Regexp
LENGTH: int
def initialize: (String) -> void
private
attr_reader hex_value: String
def validate: (String) -> nil
end
end

View File

@ -1,16 +1,14 @@
# Added only to satisfy the Steep requirements. Not 100% reliable.
module EventEmitter
interface _Event
def data: -> String
def message: -> String
def code: -> Integer
def reason: -> String
end
def self.included: (Module) -> void
def self.apply: (untyped) -> void
def add_listener: (Symbol event_name) { (_Event event) -> void } -> void
def __events: () -> Array[untyped]
def add_listener: (Symbol | String type, ?Hash[untyped, untyped] params) { (*untyped) -> void } -> Integer
alias on add_listener
alias once add_listener
def remove_listener: (untyped id_or_type) -> Array[untyped]?
def emit: (Symbol `type`, *untyped data) -> Array[untyped]
def once: (Symbol `type`) -> Integer
def remove_listener: (Integer | Symbol | String id_or_type) -> void
def emit: (Symbol | String type, *untyped data) -> void
end

View File

@ -1,4 +1,6 @@
# Added only to satisfy the Steep requirements. Not 100% reliable.
module Schnorr
def self.sign: (String message, String private_key, ?String aux_rand) -> untyped
def self.sign: (String message, String private_key, ?String aux_rand) -> Signature
def self.valid_sig?: (String message, String public_key, String signature) -> bool
def self.check_sig!: (String message, String public_key, String signature) -> bool
end

16
sig/vendor/schnorr/signature.rbs vendored Normal file
View File

@ -0,0 +1,16 @@
# Added only to satisfy the Steep requirements. Not 100% reliable.
module Schnorr
class InvalidSignatureError < StandardError
end
class Signature
attr_reader r: Integer
attr_reader s: Integer
def self.decode: (String string) -> Signature
def initialize: (Integer r, Integer s) -> void
def encode: -> String
def ==: (untyped other) -> bool
end
end

View File

@ -0,0 +1,62 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::Client::ColorLogger do
let(:client) { instance_spy(Nostr::Client) }
let(:relay) { Nostr::Relay.new(url: 'ws://0.0.0.0:4180/', name: 'localhost') }
let(:logger) { described_class.new }
describe '#attach_to' do
it 'attaches event handlers to the client' do
logger.attach_to(client)
aggregate_failures do
expect(client).to have_received(:on).with(:connect)
expect(client).to have_received(:on).with(:message)
expect(client).to have_received(:on).with(:send)
expect(client).to have_received(:on).with(:error)
expect(client).to have_received(:on).with(:close)
end
end
end
describe '#on_connect' do
it 'logs connection to a relay' do
expect do
logger.on_connect(relay)
end.to output("\e[32m\e[1mConnected to the relay\e[22m localhost (ws://0.0.0.0:4180/)\e[0m\n").to_stdout
end
end
describe '#on_message' do
it 'logs a message received from the relay' do
message = 'Received message'
expect { logger.on_message(message) }.to output("\e[32m\e[1m◄-\e[0m #{message}\n").to_stdout
end
end
describe '#on_send' do
it 'logs a message sent to the relay' do
message = 'Sent message'
expect { logger.on_send(message) }.to output("\e[32m\e[1m-►\e[0m #{message}\n").to_stdout
end
end
describe '#on_error' do
it 'logs an error message' do
message = 'Error message'
expect { logger.on_error(message) }.to output("\e[31m\e[1mError: \e[22m#{message}\e[0m\n").to_stdout
end
end
describe '#on_close' do
it 'logs a closure of connection with a relay' do
code = '1000'
reason = 'Connection closed'
expect do
logger.on_close(code, reason)
end.to output("\e[31m\e[1mConnection closed: \e[22m#{reason} (##{code})\e[0m\n").to_stdout
end
end
end

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::Client::Logger do
let(:client) { instance_spy(Nostr::Client) }
let(:relay) { Nostr::Relay.new(url: 'ws://0.0.0.0:4180/', name: 'localhost') }
let(:logger) { described_class.new }
describe '#attach_to' do
it 'attaches event handlers to the client' do
logger.attach_to(client)
aggregate_failures do
expect(client).to have_received(:on).with(:connect)
expect(client).to have_received(:on).with(:message)
expect(client).to have_received(:on).with(:send)
expect(client).to have_received(:on).with(:error)
expect(client).to have_received(:on).with(:close)
end
end
end
describe '#on_connect' do
it 'returns nil' do
expect(logger.on_connect(relay)).to be_nil
end
end
describe '#on_message' do
it 'returns nil' do
message = 'Received message'
expect(logger.on_message(message)).to be_nil
end
end
describe '#on_send' do
it 'returns nil' do
message = 'Sent message'
expect(logger.on_send(message)).to be_nil
end
end
describe '#on_error' do
it 'returns nil' do
message = 'Error message'
expect(logger.on_error(message)).to be_nil
end
end
describe '#on_close' do
it 'returns nil' do
code = 1000
reason = 'Normal closure'
expect(logger.on_close(code, reason)).to be_nil
end
end
end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'spec_helper'
require 'nostr/client/plain_logger'
RSpec.describe Nostr::Client::PlainLogger do
let(:client) { instance_spy(Nostr::Client) }
let(:relay) { Nostr::Relay.new(url: 'ws://0.0.0.0:4180/', name: 'localhost') }
let(:logger) { described_class.new }
describe '#attach_to' do
it 'attaches event handlers to the client' do
logger.attach_to(client)
aggregate_failures do
expect(client).to have_received(:on).with(:connect)
expect(client).to have_received(:on).with(:message)
expect(client).to have_received(:on).with(:send)
expect(client).to have_received(:on).with(:error)
expect(client).to have_received(:on).with(:close)
end
end
end
describe '#on_connect' do
it 'logs connection to a relay' do
expect do
logger.on_connect(relay)
end.to output("Connected to the relay localhost (ws://0.0.0.0:4180/)\n").to_stdout
end
end
describe '#on_message' do
it 'logs a message received from the relay' do
message = 'Received message'
expect { logger.on_message(message) }.to output("◄- #{message}\n").to_stdout
end
end
describe '#on_send' do
it 'logs a message sent to the relay' do
message = 'Sent message'
expect { logger.on_send(message) }.to output("-► #{message}\n").to_stdout
end
end
describe '#on_error' do
it 'logs an error message' do
message = 'Error message'
expect { logger.on_error(message) }.to output("Error: #{message}\n").to_stdout
end
end
describe '#on_close' do
it 'logs a closure of connection with a relay' do
code = '1000'
reason = 'Connection closed'
expect do
logger.on_close(code, reason)
end.to output("Connection closed: #{reason} (##{code})\n").to_stdout
end
end
end

View File

@ -17,7 +17,7 @@ RSpec.describe Nostr::Client do
@echo_server.stop
end
let(:client) { described_class.new }
let(:client) { described_class.new(logger: nil) }
let(:relay) { Nostr::Relay.new(url: plain_text_url, name: 'localhost') }
let(:port) { 4180 }
@ -27,8 +27,8 @@ RSpec.describe Nostr::Client do
after { stop }
describe '.new' do
it 'creates an instance of a relay' do
client = described_class.new
it 'creates an instance of a client' do
client = described_class.new(logger: nil)
expect(client).to be_an_instance_of(described_class)
end
@ -52,16 +52,16 @@ RSpec.describe Nostr::Client do
describe '#on' do
context 'when the connection is opened' do
it 'fires the :connect event' do
connect_event_fired = false
connected_relay = nil
client.on :connect do
connect_event_fired = true
client.on :connect do |relay|
connected_relay = relay
end
client.connect(relay)
sleep 0.02
expect(connect_event_fired).to be(true)
expect(connected_relay).to eq(relay)
end
end
@ -134,7 +134,7 @@ RSpec.describe Nostr::Client do
it 'sends a REQ message to the relay, asking for all events and returns a subscription with the same id' do
id = '16605b59b539f6e86762f28fb57db2fd'
client = described_class.new
client = described_class.new(logger: nil)
sent_message = nil
subscription = nil
@ -164,7 +164,7 @@ RSpec.describe Nostr::Client do
allow(SecureRandom).to receive(:hex).and_return('16605b59b539f6e86762f28fb57db2fd')
filter = Nostr::Filter.new(since: 1_230_981_305)
client = described_class.new
client = described_class.new(logger: nil)
sent_message = nil
subscription = nil
@ -194,7 +194,7 @@ RSpec.describe Nostr::Client do
id = '16605b59b539f6e86762f28fb57db2fd'
filter = Nostr::Filter.new(since: 1_230_981_305)
client = described_class.new
client = described_class.new(logger: nil)
sent_message = nil
subscription = nil
@ -223,7 +223,7 @@ RSpec.describe Nostr::Client do
it 'sends a REQ message to the relay, asking for all events and returns a subscription with a random id' do
allow(SecureRandom).to receive(:hex).and_return('16605b59b539f6e86762f28fb57db2fd')
client = described_class.new
client = described_class.new(logger: nil)
sent_message = nil
subscription = nil
@ -253,7 +253,7 @@ RSpec.describe Nostr::Client do
it 'sends a CLOSE message to the relay, asking it to stop a subscription' do
subscription_id = '16605b59b539f6e86762f28fb57db2fd'
client = described_class.new
client = described_class.new(logger: nil)
sent_message = nil
@ -277,7 +277,7 @@ RSpec.describe Nostr::Client do
describe '#publish' do
it 'sends a message to the relay' do
relay = Nostr::Relay.new(url: plain_text_url, name: 'localhost')
client = described_class.new
client = described_class.new(logger: nil)
event = Nostr::Event.new(
id: '2a3184512d34077601e992ba3c3215354b21a8c76f85c2c7f66093481854e811',
pubkey: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',

View File

@ -5,6 +5,62 @@ require 'spec_helper'
RSpec.describe Nostr::Crypto do
let(:crypto) { described_class.new }
describe '#check_sig!' do
let(:keypair) do
Nostr::KeyPair.new(
public_key: Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6'),
private_key: Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
)
end
let(:message) { 'Your feedback is appreciated, now pay $8' }
context 'when the signature is valid' do
it 'returns true' do
signature = crypto.sign_message(message, keypair.private_key)
expect(crypto.check_sig!(message, keypair.public_key, signature)).to be(true)
end
end
context 'when the signature is invalid' do
it 'raises an error' do
signature = Nostr::Signature.new('badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb' \
'badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb')
expect do
crypto.check_sig!(message, keypair.public_key, signature)
end.to raise_error(Schnorr::InvalidSignatureError, 'signature verification failed.')
end
end
end
describe '#valid_sig?' do
let(:keypair) do
Nostr::KeyPair.new(
public_key: Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6'),
private_key: Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
)
end
let(:message) { 'Your feedback is appreciated, now pay $8' }
context 'when the signature is valid' do
it 'returns true' do
signature = crypto.sign_message(message, keypair.private_key)
expect(crypto.valid_sig?(message, keypair.public_key, signature)).to be(true)
end
end
context 'when the signature is invalid' do
it 'returns false' do
signature = Nostr::Signature.new('badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb' \
'badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb')
expect(crypto.valid_sig?(message, keypair.public_key, signature)).to be(false)
end
end
end
describe '#sign_event' do
let(:keypair) do
Nostr::KeyPair.new(
@ -31,6 +87,19 @@ RSpec.describe Nostr::Crypto do
end
end
describe '#sign_message' do
let(:private_key) { Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d') }
let(:message) { 'Your feedback is appreciated, now pay $8' }
it 'signs a message' do
signature = crypto.sign_message(message, private_key)
hex_signature = '0fa6d8e26f44ddad9eca5be2b8a25d09338c1767f8bfce384046c8eb771d1120e4bda5ca49' \
'27e74837f912d4810945af6abf8d38139c1347f2d71ba8c52b175b'
expect(signature).to eq(Nostr::Signature.new(hex_signature))
end
end
describe '#encrypt_text' do
let(:sender_keypair) do
Nostr::KeyPair.new(

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::InvalidSignatureFormatError do
describe '#initialize' do
let(:error) { described_class.new }
it 'builds a useful error message' do
expect(error.message).to eq('Only lowercase hexadecimal characters are allowed in signatures.')
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::InvalidSignatureLengthError do
describe '#initialize' do
let(:error) { described_class.new }
it 'builds a useful error message' do
expect(error.message).to eq('Invalid signature length. It should have 128 characters.')
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::InvalidSignatureTypeError do
describe '#initialize' do
let(:error) { described_class.new }
it 'builds a useful error message' do
expect(error.message).to eq('Invalid signature type. It must be a string with lowercase hexadecimal characters.')
end
end
end

View File

@ -5,17 +5,17 @@ require 'spec_helper'
RSpec.describe Nostr::Event do
let(:event) do
described_class.new(
id: '20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9',
pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
created_at: 1_230_981_305,
id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba',
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [
%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408],
%w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e]
],
content: 'Your feedback is appreciated, now pay $8',
sig: '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \
'63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe'
sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
'06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
)
end
@ -24,8 +24,8 @@ RSpec.describe Nostr::Event do
it 'returns true' do
event1 = described_class.new(
id: '2a3184512d34077601e992ba3c3215354b21a8c76f85c2c7f66093481854e811',
pubkey: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
created_at: 1_230_981_305,
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]],
content: 'Your feedback is appreciated, now pay $8',
@ -35,8 +35,8 @@ RSpec.describe Nostr::Event do
event2 = described_class.new(
id: '2a3184512d34077601e992ba3c3215354b21a8c76f85c2c7f66093481854e811',
pubkey: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
created_at: 1_230_981_305,
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]],
content: 'Your feedback is appreciated, now pay $8',
@ -52,8 +52,8 @@ RSpec.describe Nostr::Event do
it 'returns false' do
event1 = described_class.new(
id: '2a3184512d34077601e992ba3c3215354b21a8c76f85c2c7f66093481854e811',
pubkey: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
created_at: 1_230_981_305,
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]],
content: 'Your feedback is appreciated, now pay $8',
@ -63,13 +63,13 @@ RSpec.describe Nostr::Event do
event2 = described_class.new(
id: '2a3184512d34077601e992ba3c3215354b21a8c76f85c2c7f66093481854e811',
pubkey: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
created_at: 1_230_981_305,
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]],
content: 'Your feedback is appreciated, now pay $8',
sig: '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \
'63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe'
sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
'06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
)
expect(event1).not_to eq(event2)
@ -80,17 +80,17 @@ RSpec.describe Nostr::Event do
describe '.new' do
it 'creates an instance of an event' do
event = described_class.new(
id: '20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9',
pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
created_at: 1_230_981_305,
id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba',
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [
%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408],
%w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e]
],
content: 'Your feedback is appreciated, now pay $8',
sig: '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \
'63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe'
sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
'06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
)
expect(event).to be_an_instance_of(described_class)
@ -100,7 +100,7 @@ RSpec.describe Nostr::Event do
describe '#add_event_reference' do
let(:event) do
described_class.new(
pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
kind: Nostr::EventKind::TEXT_NOTE,
tags: [
%w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e]
@ -124,7 +124,7 @@ RSpec.describe Nostr::Event do
describe '#add_pubkey_reference' do
let(:event) do
described_class.new(
pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
kind: Nostr::EventKind::TEXT_NOTE,
tags: [
%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]
@ -153,7 +153,7 @@ RSpec.describe Nostr::Event do
describe '#created_at' do
it 'exposes the event creation date' do
expect(event.created_at).to eq(1_230_981_305)
expect(event.created_at).to eq(1_667_422_587)
end
end
@ -165,13 +165,13 @@ RSpec.describe Nostr::Event do
describe '#id' do
it 'exposes the event id' do
expect(event.id).to eq('20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9')
expect(event.id).to eq('499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba')
end
end
describe '#id=' do
it 'sets the event id' do
new_id = '20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9'
new_id = '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba'
event.id = new_id
expect(event.id).to eq(new_id)
@ -180,7 +180,7 @@ RSpec.describe Nostr::Event do
describe '#pubkey' do
it 'exposes the event pubkey' do
expect(event.pubkey).to eq('ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460')
expect(event.pubkey).to eq('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558')
end
end
@ -191,8 +191,8 @@ RSpec.describe Nostr::Event do
expect(serialized_event).to eq(
[
0,
'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
1_230_981_305,
Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
1_667_422_587,
1,
[
%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408],
@ -207,16 +207,16 @@ RSpec.describe Nostr::Event do
describe '#sig' do
it 'exposes the event signature' do
expect(event.sig).to eq(
'058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \
'63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe'
'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
'06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
)
end
end
describe '#sig=' do
it 'sets the event signature' do
new_signature = '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \
'63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe'
new_signature = 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
'06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
event.sig = new_signature
expect(event.sig).to eq(new_signature)
@ -226,7 +226,7 @@ RSpec.describe Nostr::Event do
describe '#sign' do
let(:event) do
described_class.new(
pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
@ -262,16 +262,126 @@ RSpec.describe Nostr::Event do
describe '#to_h' do
it 'converts the event to a hash' do
expect(event.to_h).to eq(
id: '20f31a9b2a0ced48a167add9732ccade1dca5e34b44316e37da4af33bc8946a9',
pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
created_at: 1_230_981_305,
id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba',
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408],
%w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e]],
content: 'Your feedback is appreciated, now pay $8',
sig: '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39' \
'63d0086171f842ffebf1f7790ce147b4811a15ef3f59c76ec1324b970cc57ffe'
sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
'06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
)
end
end
describe '#verify_signature' do
context 'when the id is nil' do
let(:event) do
described_class.new(
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [
%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408],
%w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e]
],
content: 'Your feedback is appreciated, now pay $8',
sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
'06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
)
end
it 'returns false' do
expect(event.verify_signature).to be(false)
end
end
context 'when the sig is nil' do
let(:event) do
described_class.new(
id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba',
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [
%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408],
%w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e]
],
content: 'Your feedback is appreciated, now pay $8'
)
end
it 'returns false' do
event.sig = nil
expect(event.verify_signature).to be(false)
end
end
context 'when the pubkey is missing' do
let(:event) do
described_class.new(
id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba',
pubkey: nil,
created_at: 1_667_422_587,
kind: 1,
tags: [
%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408],
%w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e]
],
content: 'Your feedback is appreciated, now pay $8',
sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
'06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
)
end
it 'returns false' do
expect(event.verify_signature).to be(false)
end
end
context 'when the sig is valid' do
let(:event) do
described_class.new(
id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba',
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [
%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408],
%w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e]
],
content: 'Your feedback is appreciated, now pay $8',
sig: 'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
'06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
)
end
it 'returns true' do
expect(event.verify_signature).to be(true)
end
end
context 'when the sig is invalid' do
let(:event) do
described_class.new(
id: '499ff6e60cc6b38bd6b94446fb1d61cc18cf19bf324e93dfe84338daeaab3fba',
pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
created_at: 1_667_422_587,
kind: 1,
tags: [
%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408],
%w[p 472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e]
],
content: 'Your feedback is appreciated, now pay $8',
sig: 'badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb' \
'badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb'
)
end
it 'returns false' do
expect(event.verify_signature).to be(false)
end
end
end
end

View File

@ -56,4 +56,15 @@ RSpec.describe Nostr::KeyPair do
expect(keypair.public_key).to eq('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558')
end
end
describe '#to_ary' do
it 'converts the key pair to an array' do
expect(keypair.to_ary).to eq(
[
Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'),
Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558')
]
)
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::Signature do
let(:valid_hex_signature) do
'f418c97b50cc68227e82f4f3a79d79eb2b7a0fa517859c86e1a8fa91e3741b7f' \
'06e070c44129227b83fcbe93cecb02a346804a4080ce47685ecad60ab4f5f128'
end
let(:signature) { described_class.new(valid_hex_signature) }
describe '.new' do
context 'when the signature is not a string' do
it 'raises an InvalidSignatureTypeError' do
expect { described_class.new(1234) }.to raise_error(
Nostr::InvalidSignatureTypeError,
'Invalid signature type. It must be a string with lowercase hexadecimal characters.'
)
end
end
context "when the signature's length is not 128 characters" do
it 'raises an InvalidSignatureLengthError' do
expect { described_class.new('a' * 129) }.to raise_error(
Nostr::InvalidSignatureLengthError,
'Invalid signature length. It should have 128 characters.'
)
end
end
context 'when the signature contains non-hexadecimal characters' do
it 'raises an InvalidKeyFormatError' do
expect { described_class.new('g' * 128) }.to raise_error(
Nostr::InvalidSignatureFormatError,
'Only lowercase hexadecimal characters are allowed in signatures.'
)
end
end
context 'when the signature contains uppercase characters' do
it 'raises an InvalidKeyFormatError' do
expect { described_class.new('A' * 128) }.to raise_error(
Nostr::InvalidSignatureFormatError,
'Only lowercase hexadecimal characters are allowed in signatures.'
)
end
end
context 'when the signature is valid' do
it 'does not raise any error' do
expect { described_class.new('a' * 128) }.not_to raise_error
end
end
end
end

View File

@ -68,8 +68,8 @@ RSpec.describe Nostr::User do
kind: 1,
tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]],
content: 'Your feedback is appreciated, now pay $8',
sig: '970fea8d213da86c583804522c45d04e61c18c433704b62f793f187bca82091c' \
'3884d6207c6511c0966ecf6230082179a49257b03e5a4d2d08da9124a190f1bb'
sig: '6e852020d5c70527674a21ab7d47db3c355cdbac443a80f5fe2b956f536b75b1' \
'3fdfb28a2ffc09cb2438a61b020aaa62e8df7bb08471ccf7839a48350e485937'
)
)
end
@ -92,8 +92,8 @@ RSpec.describe Nostr::User do
kind: 1,
tags: [],
content: 'Your feedback is appreciated, now pay $8',
sig: 'f5a2cdc29723c888df52afd6f8c6e260110f74ed23fee3edbf39fff4a9f1b9f1' \
'c93284b02d4eba0481325bb5555624ddf969d5905b63f17191f9132a0ddd97b0'
sig: '48256fea31f2cabf0e92b5f67c2f654c9647be15b8bc6f381673af3748e15c76' \
'b8d505019fc4e75d79be668c18f57b69b76d95b639cca8ae9a5817d569a8d12b'
)
)
end
@ -116,8 +116,8 @@ RSpec.describe Nostr::User do
kind: 1,
tags: [%w[e 189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408]],
content: 'Your feedback is appreciated, now pay $8',
sig: '970fea8d213da86c583804522c45d04e61c18c433704b62f793f187bca82091c' \
'3884d6207c6511c0966ecf6230082179a49257b03e5a4d2d08da9124a190f1bb'
sig: '6e852020d5c70527674a21ab7d47db3c355cdbac443a80f5fe2b956f536b75b1' \
'3fdfb28a2ffc09cb2438a61b020aaa62e8df7bb08471ccf7839a48350e485937'
)
)
end

View File

@ -2,6 +2,6 @@
RSpec.describe Nostr do
it 'has a version number' do
expect(Nostr::VERSION).not_to be_nil
expect(described_class::VERSION).not_to be_nil
end
end