Compare commits
26 Commits
feature/ru
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
defeba1278 | ||
|
955b74054b | ||
|
90ab1a6149 | ||
|
a0cf41bfb4 | ||
|
61eb0459d4 | ||
|
0497878d54 | ||
|
7946b82aaf | ||
|
e114656166 | ||
|
04574cc836 | ||
|
1ff9611051 | ||
|
1d12363af1 | ||
|
9ae68542f1 | ||
|
6a5068a552 | ||
|
470a72d4de | ||
|
86cd6c6baa | ||
|
9d6d91e436 | ||
|
838a2db834 | ||
|
7c571d3b12 | ||
|
01010c763f | ||
|
f8893f9b0e | ||
|
3788ba4ce5 | ||
|
0f83b8071a | ||
|
c8d633dbea | ||
|
df51354d3e | ||
|
3765b60a68 | ||
|
17cd2bf0f4 |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
ruby:
|
||||
- '3.2.0'
|
||||
- '3.3.0'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
@ -5,9 +5,11 @@ require:
|
||||
- rubocop-rspec
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 3.2
|
||||
TargetRubyVersion: 3.3
|
||||
DisplayCopNames: true
|
||||
NewCops: enable
|
||||
Exclude:
|
||||
- docs/**/*
|
||||
|
||||
# ----------------------- Gemspec -----------------------
|
||||
|
||||
|
@ -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.
|
||||
|
@ -1,2 +1,2 @@
|
||||
ruby 3.2.2
|
||||
bun 1.0.11
|
||||
ruby 3.3.0
|
||||
bun 1.1.3
|
||||
|
60
CHANGELOG.md
60
CHANGELOG.md
@ -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
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Nostr
|
||||
|
||||
[](https://badge.fury.io/rb/nostr)
|
||||

|
||||
[](https://codeclimate.com/github/wilsonsilva/nostr/maintainability)
|
||||
[](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
|
||||
|
19
adr/0001-record-architecture-decisions.md
Normal file
19
adr/0001-record-architecture-decisions.md
Normal 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).
|
27
adr/0002-introduction-of-signature-class.md
Normal file
27
adr/0002-introduction-of-signature-class.md
Normal 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.
|
122
adr/0003-logging-methods-vs-logger-class.md
Normal file
122
adr/0003-logging-methods-vs-logger-class.md
Normal 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.
|
||||
|
66
adr/0004-default-logging-activation.md
Normal file
66
adr/0004-default-logging-activation.md
Normal 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
32
adr/0005-logger-types.md
Normal 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.
|
@ -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' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
BIN
docs/bun.lockb
BIN
docs/bun.lockb
Binary file not shown.
60
docs/common-use-cases/logging-and-debugging.md
Normal file
60
docs/common-use-cases/logging-and-debugging.md
Normal 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.
|
50
docs/common-use-cases/signing-and-verifying-events.md
Normal file
50
docs/common-use-cases/signing-and-verifying-events.md
Normal 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 we’re 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
|
||||
```
|
43
docs/common-use-cases/signing-and-verifying-messages.md
Normal file
43
docs/common-use-cases/signing-and-verifying-messages.md
Normal 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 we’re 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
|
||||
```
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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: ✅
|
||||
|
||||
---
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
69
lib/nostr/client/color_logger.rb
Normal file
69
lib/nostr/client/color_logger.rb
Normal 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
|
85
lib/nostr/client/logger.rb
Normal file
85
lib/nostr/client/logger.rb
Normal 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
|
69
lib/nostr/client/plain_logger.rb
Normal file
69
lib/nostr/client/plain_logger.rb
Normal 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
|
@ -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
|
||||
|
@ -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'
|
||||
|
18
lib/nostr/errors/invalid_signature_format_error.rb
Normal file
18
lib/nostr/errors/invalid_signature_format_error.rb
Normal 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
|
18
lib/nostr/errors/invalid_signature_length_error.rb
Normal file
18
lib/nostr/errors/invalid_signature_length_error.rb
Normal 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
|
16
lib/nostr/errors/invalid_signature_type_error.rb
Normal file
16
lib/nostr/errors/invalid_signature_type_error.rb
Normal 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
|
6
lib/nostr/errors/signature_validation_error.rb
Normal file
6
lib/nostr/errors/signature_validation_error.rb
Normal file
@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Nostr
|
||||
# Base class for all signature validation errors
|
||||
class SignatureValidationError < Error; end
|
||||
end
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
67
lib/nostr/signature.rb
Normal 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
|
@ -2,5 +2,5 @@
|
||||
|
||||
module Nostr
|
||||
# The version of the gem
|
||||
VERSION = '0.5.0'
|
||||
VERSION = '0.7.0'
|
||||
end
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
6
sig/nostr/client/color_logger.rbs
Normal file
6
sig/nostr/client/color_logger.rbs
Normal file
@ -0,0 +1,6 @@
|
||||
module Nostr
|
||||
class Client
|
||||
class ColorLogger < Logger
|
||||
end
|
||||
end
|
||||
end
|
12
sig/nostr/client/logger.rbs
Normal file
12
sig/nostr/client/logger.rbs
Normal 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
|
6
sig/nostr/client/plain_logger.rbs
Normal file
6
sig/nostr/client/plain_logger.rbs
Normal file
@ -0,0 +1,6 @@
|
||||
module Nostr
|
||||
class Client
|
||||
class PlainLogger < Logger
|
||||
end
|
||||
end
|
||||
end
|
@ -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
|
||||
|
||||
|
5
sig/nostr/errors/invalid_signature_format_error.rbs
Normal file
5
sig/nostr/errors/invalid_signature_format_error.rbs
Normal file
@ -0,0 +1,5 @@
|
||||
module Nostr
|
||||
class InvalidSignatureFormatError < SignatureValidationError
|
||||
def initialize: -> void
|
||||
end
|
||||
end
|
5
sig/nostr/errors/invalid_signature_length_error.rbs
Normal file
5
sig/nostr/errors/invalid_signature_length_error.rbs
Normal file
@ -0,0 +1,5 @@
|
||||
module Nostr
|
||||
class InvalidSignatureLengthError < SignatureValidationError
|
||||
def initialize: -> void
|
||||
end
|
||||
end
|
5
sig/nostr/errors/invalid_signature_type_error.rbs
Normal file
5
sig/nostr/errors/invalid_signature_type_error.rbs
Normal file
@ -0,0 +1,5 @@
|
||||
module Nostr
|
||||
class InvalidSignatureTypeError < SignatureValidationError
|
||||
def initialize: -> void
|
||||
end
|
||||
end
|
4
sig/nostr/errors/signature_validation_error.rbs
Normal file
4
sig/nostr/errors/signature_validation_error.rbs
Normal file
@ -0,0 +1,4 @@
|
||||
module Nostr
|
||||
class SignatureValidationError < Error
|
||||
end
|
||||
end
|
@ -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]]
|
||||
|
@ -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
14
sig/nostr/signature.rbs
Normal 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
|
18
sig/vendor/event_emitter.rbs
vendored
18
sig/vendor/event_emitter.rbs
vendored
@ -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
|
||||
|
4
sig/vendor/schnorr.rbs
vendored
4
sig/vendor/schnorr.rbs
vendored
@ -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
16
sig/vendor/schnorr/signature.rbs
vendored
Normal 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
|
62
spec/nostr/client/color_logger_spec.rb
Normal file
62
spec/nostr/client/color_logger_spec.rb
Normal 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
|
58
spec/nostr/client/logger_spec.rb
Normal file
58
spec/nostr/client/logger_spec.rb
Normal 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
|
63
spec/nostr/client/plain_logger_spec.rb
Normal file
63
spec/nostr/client/plain_logger_spec.rb
Normal 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
|
@ -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',
|
||||
|
@ -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(
|
||||
|
13
spec/nostr/errors/invalid_signature_format_error_spec.rb
Normal file
13
spec/nostr/errors/invalid_signature_format_error_spec.rb
Normal 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
|
13
spec/nostr/errors/invalid_signature_length_error_spec.rb
Normal file
13
spec/nostr/errors/invalid_signature_length_error_spec.rb
Normal 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
|
13
spec/nostr/errors/invalid_signature_type_error_spec.rb
Normal file
13
spec/nostr/errors/invalid_signature_type_error_spec.rb
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
56
spec/nostr/signature_spec.rb
Normal file
56
spec/nostr/signature_spec.rb
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user