32 Commits

Author SHA1 Message Date
Wilson Silva
8bc6b84f6f Bump the gem version to v0.5.0 2023-11-20 21:05:36 +07:00
Wilson Silva
61a88981e6 Add full NIP-19 compatibility
note, nprofile, nevent, naddr, npub, nsec and nrelay
2023-11-20 21:03:24 +07:00
Wilson Silva
bba18d1bc0 Fix the generation of private keys
00c2660584
2023-11-20 12:39:58 +07:00
Wilson Silva
b45370e65f Remove a broken documentation link 2023-11-20 10:42:32 +07:00
Wilson Silva
53069a3d0c Update bip-schnorr, puma, rake, rubocop and rubocop-rspec 2023-11-20 10:32:27 +07:00
Wilson Silva
3520cf8219 Implement NIP-19 bech32-encoded private and public keys
https://github.com/nostr-protocol/nips/blob/master/19.md
2023-11-20 09:59:27 +07:00
Wilson Silva
3fffcd1a4e Use keyword arguments for User#create_event
This allows for code completion and fixes another RBS issue
2023-11-18 18:06:53 +07:00
Wilson Silva
2c44ae4ee8 Fix the RBS types of Nostr::Filter e, p and to_f 2023-11-18 17:22:20 +07:00
Wilson Silva
4b630c678b Ensure that Nostr::Crypto#descrypt_text always returns a string
Fixes another Steep/RBS violation
2023-11-18 16:38:03 +07:00
Wilson Silva
3077aa67a7 Fix the RBS type of Nostr::Crypto::BN_BASE 2023-11-18 16:28:31 +07:00
Wilson Silva
2584967654 Update rbs to v3.3 and steep to v1.6 2023-11-18 15:42:09 +07:00
Wilson Silva
b54147cfce Remove unnecessary blank space 2023-11-14 16:58:14 +07:00
Wilson Silva
0f9c0d3a3d Add relay message type enums 2023-11-14 16:57:33 +07:00
Wilson Silva
904fe46a9d Name the implemented NIPs 2023-11-14 16:53:38 +07:00
Wilson Silva
80c272f149 Fix the formatting of encrypted direct message 2023-11-14 16:28:16 +07:00
Wilson Silva
be4c3e0e32 Update the documentation of the subscription id
Match the changes introduced in  https://github.com/nostr-protocol/nips/pull/299
2023-11-14 16:12:33 +07:00
Wilson Silva
30eafa1203 Remove id/pubkey prefix documentation
Removed from the protocol in https://github.com/nostr-protocol/nips/pull/703
2023-11-14 16:12:32 +07:00
Wilson Silva
4f13b22e51 Update Ruby to version 3.2.2 2023-11-14 07:54:56 +07:00
Wilson Silva
6d9758a37a Fix the subscription documentation 2023-11-14 07:45:39 +07:00
Wilson Silva
4d8803d769 Update bec32 2023-11-13 21:32:11 +07:00
Wilson Silva
e9deab2fc2 Link the changelog with the homepage 2023-11-13 18:09:51 +07:00
Wilson Silva
35c7e6a76e Ignore my local copy of the nostr nips folder 2023-11-13 18:09:03 +07:00
Wilson Silva
8e1e3092c3 Update the changelog format version 2023-11-13 18:01:17 +07:00
Wilson Silva
eaa97e0018 Update the changelog 2023-11-13 17:58:04 +07:00
Wilson Silva
1fb7e454ae Update the gem's homepage 2023-11-13 17:55:36 +07:00
Wilson Silva
82aacb70e7 Use my own domain instead of Vercel's 2023-11-13 17:55:25 +07:00
Wilson Silva
d49fac49b6 Add high-level documentation
https://nostr-ruby.com
2023-11-13 17:48:17 +07:00
Wilson Silva
b206f6504e Update vitepress and install mermaid diagram plugin 2023-11-13 17:43:42 +07:00
Wilson Silva
6d81d07f8a Use bun 2023-11-13 17:43:41 +07:00
Wilson Silva
3fbc523b18 Document how to develop the documentation 2023-11-13 17:43:41 +07:00
Wilson Silva
c27de6d506 Update Puma to version 6.3
Fixes the error "Puma 5 is not compatible with Rack 3, please upgrade to Puma 6 or higher."
2023-09-16 16:13:42 +07:00
Wilson Silva
1865858230 Add the VitePress documentation scaffold 2023-09-16 15:32:53 +07:00
101 changed files with 3076 additions and 375 deletions

View File

@@ -6,6 +6,6 @@ max_line_length = 120
trim_trailing_whitespace = true trim_trailing_whitespace = true
# 2 space indentation # 2 space indentation
[*.rb] [{*.rb, *.mjs}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

3
.gitignore vendored
View File

@@ -18,3 +18,6 @@
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
# A local copy of nostr nips to help Github Copilot suggestions
/nips/

View File

@@ -9,8 +9,17 @@ AllCops:
DisplayCopNames: true DisplayCopNames: true
NewCops: enable NewCops: enable
# ----------------------- Gemspec -----------------------
Gemspec/DevelopmentDependencies:
Enabled: false
# ----------------------- Style ----------------------- # ----------------------- Style -----------------------
Style/RaiseArgs:
Exclude:
- 'lib/nostr/key.rb'
Style/StringLiterals: Style/StringLiterals:
Enabled: true Enabled: true
EnforcedStyle: single_quotes EnforcedStyle: single_quotes
@@ -38,3 +47,17 @@ Metrics/ParameterLists:
RSpec/ExampleLength: RSpec/ExampleLength:
Enabled: false Enabled: false
RSpec/FilePath:
Exclude:
- spec/nostr/errors/invalid_*
RSpec/SpecFilePathFormat:
Exclude:
- spec/nostr/errors/invalid_*
# ----------------------- Naming -----------------------
Naming/MemoizedInstanceVariableName:
Exclude:
- 'spec/nostr/key.rb'

View File

@@ -1 +1,2 @@
ruby 3.2.0 ruby 3.2.2
bun 1.0.11

View File

@@ -1,9 +1,42 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. 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.0.0/) 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). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [0.5.0] 2023-11-20
### Added
- 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 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`
### Changed
- Set the gem's homepage to [`https://nostr-ruby.com/`](https://nostr-ruby.com/)
- Updated the filter's documentation to reflect the removal of prefix matching
- Updated the subscription's id documentation to reflect the changes in the protocol definition
- Updated `Nostr::PrivateKey` and `Nostr::PublicKey` internally, instead of Strings
- Updated the gem `bip-schnorr` to version `0.6` (was `0.4`)
- Updated the gem `puma` to version `6.4` (was `6.3`)
- Updated the gem `rake` to version `13.1` (was `13.0`)
- Updated the gem `rbs` to version `3.3` (was `2.8`)
- Updated the gem `rubocop` to version `1.57` (was `1.42`)
- Updated the gem `rubocop-rspec` to version `2.25` (was `2.16`)
- Updated the gem `steep` to version `1.6` (was `1.4`)
## Fixed
- Fixed the RBS type of the constant `Nostr::Crypto::BN_BASE`
- Fixed the return type of `Nostr::Crypto#decrypt_text` when given an invalid ciphertext
- Fixed the RBS type of `Nostr::Filter#to_h`, `Nostr::Filter#e` and `Nostr::Filter#p`
- Fixed the RBS types of `EventEmitter` and `EventMachine::Channel`
- Fixed the generation of private keys
## [0.4.0] - 2023-02-25 ## [0.4.0] - 2023-02-25
### Removed ### Removed
@@ -49,6 +82,7 @@ principles of immutability and was a major source of internal complexity as I ne
- Initial release - Initial release
[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.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 [0.3.0]: https://github.com/wilsonsilva/nostr/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/wilsonsilva/nostr/compare/v0.1.0...v0.2.0 [0.2.0]: https://github.com/wilsonsilva/nostr/compare/v0.1.0...v0.2.0

333
README.md
View File

@@ -4,31 +4,30 @@
[![Maintainability](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/maintainability)](https://codeclimate.com/github/wilsonsilva/nostr/maintainability) [![Maintainability](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/maintainability)](https://codeclimate.com/github/wilsonsilva/nostr/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/test_coverage)](https://codeclimate.com/github/wilsonsilva/nostr/test_coverage) [![Test Coverage](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/test_coverage)](https://codeclimate.com/github/wilsonsilva/nostr/test_coverage)
Asynchronous Nostr client. Please note that the API is likely to change as the gem is still in development and Asynchronous Nostr client for Rubyists.
has not yet reached a stable release. Use with caution.
## Table of contents ## Table of contents
- [Installation](#installation) - [Key features](#-key-features)
- [Usage](#usage) - [Installation](#-installation)
* [Requiring the gem](#requiring-the-gem) - [Quickstart](#-quickstart)
* [Generating a keypair](#generating-a-keypair) - [Documentation](#-documentation)
* [Generating a private key and a public key](#generating-a-private-key-and-a-public-key) - [Implemented NIPs](#-implemented-nips)
* [Connecting to a Relay](#connecting-to-a-relay) - [Development](#-development)
* [WebSocket events](#websocket-events)
* [Requesting for events / creating a subscription](#requesting-for-events--creating-a-subscription)
* [Stop previous subscriptions](#stop-previous-subscriptions)
* [Publishing an event](#publishing-an-event)
* [Creating/updating your contact list](#creatingupdating-your-contact-list)
* [Sending an encrypted direct message](#sending-an-encrypted-direct-message)
- [Implemented NIPs](#implemented-nips)
- [Development](#development)
* [Type checking](#type-checking) * [Type checking](#type-checking)
- [Contributing](#contributing) - [Contributing](#-contributing)
- [License](#license) - [License](#-license)
- [Code of Conduct](#code-of-conduct) - [Code of Conduct](#-code-of-conduct)
## Installation ## 🔑 Key features
- Asynchronous
- Easy to use
- Fully documented
- Fully tested
- Fully typed
## 📦 Installation
Install the gem and add to the application's Gemfile by executing: Install the gem and add to the application's Gemfile by executing:
@@ -38,248 +37,103 @@ If bundler is not being used to manage dependencies, install the gem by executin
$ gem install nostr $ gem install nostr
## Usage ## ⚡️ Quickstart
### Requiring the gem Here is a quick example of how to use the gem. For more detailed documentation, please check the
[documentation website](https://nostr-ruby.com).
All examples below assume that the gem has been required.
```ruby ```ruby
# Require the gem
require 'nostr' require 'nostr'
```
### Generating a keypair # Instantiate a client
```ruby
keygen = Nostr::Keygen.new
keypair = keygen.generate_key_pair
keypair.private_key
keypair.public_key
```
### Generating a private key and a public key
```ruby
keygen = Nostr::Keygen.new
private_key = keygen.generate_private_key
public_key = keygen.extract_public_key(private_key)
```
### Connecting to a Relay
Clients can connect to multiple Relays. In this version, a Client can only connect to a single Relay at a time.
You may instantiate multiple Clients and multiple Relays.
```ruby
client = Nostr::Client.new client = Nostr::Client.new
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
# a) Use an existing keypair
keypair = Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.new('add-your-hex-private-key-here'),
public_key: Nostr::PublicKey.new('add-your-hex-public-key-here'),
)
# b) Or build a keypair from a private key
keygen = Nostr::Keygen.new
keypair = keygen.get_key_pair_from_private_key(
Nostr::PrivateKey.new('add-your-hex-private-key-here')
)
# c) Or create a new keypair
keygen = Nostr::Keygen.new
keypair = keygen.generate_keypair
# Create a user with the keypair
user = Nostr::User.new(keypair: keypair)
# Create a signed event
text_note_event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
# Connect asynchronously to a relay
relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
client.connect(relay) client.connect(relay)
```
### WebSocket events # Listen asynchronously for the connect event
All communication between clients and relays happen in WebSockets.
The `:connect` event is fired when a connection with a WebSocket is opened. You must call `Nostr::Client#connect` first.
```ruby
client.on :connect do client.on :connect do
# all the code goes here # Send the event to the Relay
end client.publish(text_note_event)
```
The `:close` event is fired when a connection with a WebSocket has been closed because of an error. # Create a filter to receive the first 20 text notes
# and encrypted direct messages from the relay that
# were created in the previous hour
filter = Nostr::Filter.new(
kinds: [
Nostr::EventKind::TEXT_NOTE,
Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE
],
since: Time.now.to_i - 3600, # 1 hour ago
until: Time.now.to_i,
limit: 20,
)
```ruby # Subscribe to events matching conditions of a filter
client.on :error do |error_message| subscription = client.subscribe(filter: filter)
puts error_message
# Unsubscribe from events matching the filter above
client.unsubscribe(subscription.id)
end end
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the server certificate for 'rsslay.fiatjaf.com' # Listen for incoming messages and print them
```
The `:message` event is fired when data is received through a WebSocket.
```ruby
client.on :message do |message| client.on :message do |message|
puts message puts message
end end
# [ # Listen for error messages
# "EVENT", client.on :error do |error_message|
# "d34107357089bfc9882146d3bfab0386", # Handle the error
# { end
# "content":"",
# "created_at":1676456512,
# "id":"18f63550da74454c5df7caa2a349edc5b2a6175ea4c5367fa4b4212781e5b310",
# "kind":3,
# "pubkey":"117a121fa41dc2caa0b3d6c5b9f42f90d114f1301d39f9ee96b646ebfee75e36",
# "sig":"d171420bd62cf981e8f86f2dd8f8f86737ea2bbe2d98da88db092991d125535860d982139a3c4be39886188613a9912ef380be017686a0a8b74231dc6e0b03cb",
# "tags":[
# ["p","1cc821cc2d47191b15fcfc0f73afed39a86ac6fb34fbfa7993ee3e0f0186ef7c"]
# ]
# }
# ]
```
The `:close` event is fired when a connection with a WebSocket is closed. # Listen for the close event
```ruby
client.on :close do |code, reason| client.on :close do |code, reason|
# you may attempt to reconnect # You may attempt to reconnect to the relay here
client.connect(relay)
end end
``` ```
### Requesting for events / creating a subscription ## 📚 Documentation
A client can request events and subscribe to new updates after it has established a connection with the Relay. I made a detailed documentation for this gem and it's usage. The code is also fully documented using YARD.
You may use a `Nostr::Filter` instance with as many attributes as you wish: - [Guide documentation](https://nostr-ruby.com)
- [YARD documentation](https://rubydoc.info/gems/nostr)
```ruby ## ✅ Implemented NIPs
client.on :connect do
filter = Nostr::Filter.new(
ids: ['8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8'],
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
kinds: [Nostr::EventKind::TEXT_NOTE],
e: ["f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52"],
p: ["f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8"],
since: 1230981305,
until: 1292190341,
limit: 420,
)
subscription = client.subscribe('a_random_subscription_id', filter) - [x] [NIP-01 - Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
end - [x] [NIP-02 - Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md)
``` - [x] [NIP-04 - Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)
- [x] [NIP-19 - Bech32-encoded entities](https://github.com/nostr-protocol/nips/blob/master/19.md)
With just a few: ## 🔨 Development
```ruby
client.on :connect do
filter = Nostr::Filter.new(kinds: [Nostr::EventKind::TEXT_NOTE])
subscription = client.subscribe('a_random_subscription_id', filter)
end
```
Or omit the filter:
```ruby
client.on :connect do
subscription = client.subscribe('a_random_subscription_id')
end
```
Or even omit the subscription id:
```ruby
client.on :connect do
subscription = client.subscribe('a_random_subscription_id')
end
```
### Stop previous subscriptions
You can stop receiving messages from a subscription by calling `#unsubscribe`:
```ruby
client.unsubscribe('your_subscription_id')
```
### Publishing an event
To publish an event you need a keypair.
```ruby
# Create a keypair
keypair = Nostr::KeyPair.new(
private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
)
# Add the keypair to the user facade
user = Nostr::User.new(keypair: keypair)
# Create a signed event
event = user.create_event(
created_at: 1667422587, # optional, defaults to the current time
kind: Nostr::EventKind::TEXT_NOTE,
tags: [], # optional, defaults to []
content: 'Your feedback is appreciated, now pay $8'
)
# Send it to the Relay
client.publish(event)
```
### Creating/updating your contact list
Every new contact list that gets published overwrites the past ones, so it should contain all entries.
```ruby
# Creating a contact list event with 2 contacts
update_contacts_event = user.create_event(
kind: Nostr::EventKind::CONTACT_LIST,
tags: [
[
"p", # mandatory
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", # public key of the user to add to the contacts
"wss://alicerelay.com/", # can be an empty string or can be omitted
"alice" # can be an empty string or can be omitted
],
[
"p", # mandatory
"3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", # public key of the user to add to the contacts
"wss://bobrelay.com/nostr", # can be an empty string or can be omitted
"bob" # can be an empty string or can be omitted
],
],
)
# Send it to the Relay
client.publish(update_contacts_event)
```
### Sending an encrypted direct message
```ruby
sender_private_key = '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
encrypted_direct_message = Nostr::Events::EncryptedDirectMessage.new(
sender_private_key: sender_private_key,
recipient_public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0',
plain_text: 'Your feedback is appreciated, now pay $8',
previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460' # optional
)
encrypted_direct_message.sign(sender_private_key)
# #<Nostr::Events::EncryptedDirectMessage:0x0000000104c9fa68
# @content="mjIFNo1sSP3KROE6QqhWnPSGAZRCuK7Np9X+88HSVSwwtFyiZ35msmEVoFgRpKx4?iv=YckChfS2oWCGpMt1uQ4GbQ==",
# @created_at=1676456512,
# @id="daac98826d5eb29f7c013b6160986c4baf4fe6d4b995df67c1b480fab1839a9b",
# @kind=4,
# @pubkey="8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca",
# @sig="028bb5f5bab0396e2065000c84a4bcce99e68b1a79bb1b91a84311546f49c5b67570b48d4a328a1827e7a8419d74451347d4f55011a196e71edab31aa3d6bdac",
# @tags=[["p", "6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0"], ["e", "ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460"]]>
# Send it to the Relay
client.publish(encrypted_direct_message)
````
## Implemented NIPs
- [x] [NIP-01 - Client](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [x] [NIP-02 - Client](https://github.com/nostr-protocol/nips/blob/master/02.md)
- [x] [NIP-04 - Client](https://github.com/nostr-protocol/nips/blob/master/04.md)
## Development
After checking out the repo, run `bin/setup` to install dependencies. After checking out the repo, run `bin/setup` to install dependencies.
@@ -323,17 +177,22 @@ used to provide type checking and autocompletion in your editor. Run `bundle exe
an RBS definition for the given Ruby file. And validate all definitions using [Steep](https://github.com/soutaro/steep) an RBS definition for the given Ruby file. And validate all definitions using [Steep](https://github.com/soutaro/steep)
with the command `bundle exec steep check`. with the command `bundle exec steep check`.
## Contributing ## 🐞 Issues & Bugs
If you find any issues or bugs, please report them [here](https://github.com/wilsonsilva/nostr/issues), I will be happy
to have a look at them and fix them as soon as possible.
## 🤝 Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/wilsonsilva/nostr. Bug reports and pull requests are welcome on GitHub at https://github.com/wilsonsilva/nostr.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere
to the [code of conduct](https://github.com/wilsonsilva/nostr/blob/main/CODE_OF_CONDUCT.md). to the [code of conduct](https://github.com/wilsonsilva/nostr/blob/main/CODE_OF_CONDUCT.md).
## License ## 📜 License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## Code of Conduct ## 👔 Code of Conduct
Everyone interacting in the Nostr project's codebases, issue trackers, chat rooms and mailing lists is expected Everyone interacting in the Nostr project's codebases, issue trackers, chat rooms and mailing lists is expected
to follow the [code of conduct](https://github.com/wilsonsilva/nostr/blob/main/CODE_OF_CONDUCT.md). to follow the [code of conduct](https://github.com/wilsonsilva/nostr/blob/main/CODE_OF_CONDUCT.md).

4
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/node_modules/
.vitepress/dist
.vitepress/temp
.vitepress/cache

112
docs/.vitepress/config.mjs Normal file
View File

@@ -0,0 +1,112 @@
import { defineConfig } from 'vitepress'
import { withMermaid } from "vitepress-plugin-mermaid";
// https://vitepress.dev/reference/site-config
// https://www.npmjs.com/package/vitepress-plugin-mermaid
export default defineConfig(withMermaid({
title: "Nostr",
description: "Documentation of the Nostr Ruby gem",
// https://vitepress.dev/reference/site-config#head
head: [
['link', { rel: 'icon', href: '/favicon.ico' }]
],
themeConfig: {
// https://vitepress.dev/reference/default-theme-last-updated
lastUpdated: true,
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
{ text: 'Guide', link: '/getting-started/overview' }
],
// https://vitepress.dev/reference/default-theme-search
search: {
provider: 'local'
},
// https://vitepress.dev/reference/default-theme-sidebar
sidebar: [
{
text: 'Getting started',
collapsed: false,
items: [
{ text: 'Overview', link: '/getting-started/overview' },
{ text: 'Installation', link: '/getting-started/installation' },
]
},
{
text: 'Core',
collapsed: false,
items: [
{ text: 'Client', link: '/core/client' },
{ text: 'Keys', link: '/core/keys' },
{ text: 'User', link: '/core/user' },
]
},
{
text: 'Relays',
items: [
{ text: 'Connecting to a relay', link: '/relays/connecting-to-a-relay' },
{ text: 'Publishing events', link: '/relays/publishing-events' },
{ text: 'Receiving events', link: '/relays/receiving-events' },
]
},
{
text: 'Subscriptions',
collapsed: false,
items: [
{ text: 'Creating a subscription', link: '/subscriptions/creating-a-subscription' },
{ text: 'Filtering subscription events', link: '/subscriptions/filtering-subscription-events' },
{ text: 'Updating a subscription', link: '/subscriptions/updating-a-subscription' },
{ text: 'Deleting a subscription', link: '/subscriptions/deleting-a-subscription' },
]
},
{
text: 'Events',
link: '/events',
collapsed: false,
items: [
{ text: 'Set Metadata', link: '/events/set-metadata' },
{ text: 'Text Note', link: '/events/text-note' },
{ text: 'Recommend Server', link: '/events/recommend-server' },
{ text: 'Contact List', link: '/events/contact-list' },
{ text: 'Encrypted Direct Message', link: '/events/encrypted-direct-message' },
]
},
{
text: 'Common use cases',
collapsed: false,
items: [
{ text: 'Bech32 enc/decoding (NIP-19)', link: '/common-use-cases/bech32-encoding-and-decoding-(NIP-19)' },
]
},
{
text: 'Implemented NIPs',
link: '/implemented-nips',
},
],
// https://vitepress.dev/reference/default-theme-config#sociallinks
socialLinks: [
{ icon: 'github', link: 'https://github.com/wilsonsilva/nostr' }
],
// https://vitepress.dev/reference/default-theme-edit-link
editLink: {
pattern: 'https://github.com/wilsonsilva/nostr/edit/main/docs/:path',
text: 'Edit this page on GitHub'
},
// https://vitepress.dev/reference/default-theme-footer
footer: {
message: 'Released under the <a href="https://github.com/wilsonsilva/nostr/blob/main/LICENSE.txt">MIT License</a>.',
copyright: 'Copyright © 2023-present <a href="https://github.com/wilsonsilva">Wilson Silva</a>'
}
},
// https://vitepress.dev/reference/site-config#ignoredeadlinks
ignoreDeadLinks: [
/^https?:\/\/localhost/
],
}))

44
docs/README.md Normal file
View File

@@ -0,0 +1,44 @@
# Nostr Docs
VitePress-powered documentation for the Nostr Ruby gem.
## Live Demo
https://nostr-ruby.com/
## Development
### Requirements
- [Bun](https://bun.sh/)
### Installation
```shell
bun install
```
### Tasks
The `docs:dev` script will start a local dev server with instant hot updates. Run it with the following command:
```shell
bun run docs:dev
```
Run this command to build the docs:
```shell
bun run docs:build
```
Once built, preview it locally by running:
```shell
bun run docs:preview
```
The preview command will boot up a local static web server that will serve the output directory .`vitepress/dist` at
http://localhost:4173. You can use this to make sure everything looks good before pushing to production.

49
docs/api-examples.md Normal file
View File

@@ -0,0 +1,49 @@
---
outline: deep
---
# Runtime API Examples
This page demonstrates usage of some of the runtime APIs provided by VitePress.
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
```md
<script setup>
import { useData } from 'vitepress'
const { theme, page, frontmatter } = useData()
</script>
## Results
### Theme Data
<pre>{{ theme }}</pre>
### Page Data
<pre>{{ page }}</pre>
### Page Frontmatter
<pre>{{ frontmatter }}</pre>
```
<script setup>
import { useData } from 'vitepress'
const { site, theme, page, frontmatter } = useData()
</script>
## Results
### Theme Data
<pre>{{ theme }}</pre>
### Page Data
<pre>{{ page }}</pre>
### Page Frontmatter
<pre>{{ frontmatter }}</pre>
## More
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).

BIN
docs/bun.lockb Executable file

Binary file not shown.

View File

@@ -0,0 +1,190 @@
# Encoding/decoding bech-32 strings (NIP-19)
[NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) standardizes bech32-formatted strings that can be
used to display keys, ids and other information in clients. These formats are not meant to be used anywhere in the core
protocol, they are only meant for displaying to users, copy-pasting, sharing, rendering QR codes and inputting data.
In order to guarantee the deterministic nature of the documentation, the examples below assume that there is a `keypair`
variable with the following values:
```ruby
keypair = Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
)
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
```
## Public key (npub)
### Encoding
```ruby
npub = Nostr::Bech32.npub_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
npub # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
```
### Decoding
```ruby
type, public_key = Nostr::Bech32.decode('npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg')
type # => 'npub'
public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
```
## Private key (nsec)
### Encoding
```ruby
nsec = Nostr::Bech32.nsec_encode('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
nsec # => 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
```
### Decoding
```ruby
type, private_key = Nostr::Bech32.decode('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5')
type # => 'npub'
private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
```
## Relay (nrelay)
### Encoding
```ruby
nrelay = Nostr::Bech32.nrelay_encode('wss://relay.damus.io')
nrelay # => 'nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x'
```
### Decoding
```ruby
type, data = Nostr::Bech32.decode('nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x')
type # => 'nrelay'
data.entries.first.label # => 'relay'
data.entries.first.value # => 'wss://relay.damus.io'
```
## Event (nevent)
### Encoding
```ruby{8-12}
user = Nostr::User.new(keypair: keypair)
text_note_event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
created_at: 1700467997,
content: 'Your feedback is appreciated, now pay $8'
)
nevent = Nostr::Bech32.nevent_encode(
id: text_note_event.id,
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
kind: Nostr::EventKind::TEXT_NOTE,
)
nevent # => 'nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0praxwkjagcpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqs03k8v3'
```
### Decoding
```ruby
type, event = Nostr::Bech32.decode('nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0praxwkjagcpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqs03k8v3')
type # => 'nevent'
event.entries[0].label # => 'author'
event.entries[0].value # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
event.entries[1].relay # => 'relay'
event.entries[1].value # => 'wss://relay.damus.io'
event.entries[2].label # => 'relay'
event.entries[2].value # => 'wss://nos.lol'
event.entries[3].label # => 'kind'
event.entries[3].value # => 1
```
## Address (naddr)
### Encoding
```ruby
naddr = Nostr::Bech32.naddr_encode(
pubkey: keypair.public_key,
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
kind: Nostr::EventKind::TEXT_NOTE,
identifier: 'damus',
)
naddr # => 'naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqsqptyv9kh2uc3qfs2p'
```
### Decoding
```ruby
type, addr = Nostr::Bech32.decode('naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqsqptyv9kh2uc3qfs2p')
type # => 'naddr'
addr.entries[0].label # => 'author'
addr.entries[0].value # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
addr.entries[1].label # => 'relay'
addr.entries[1].value # => 'wss://relay.damus.io'
addr.entries[2].label # => 'relay'
addr.entries[2].value # => 'wss://nos.lol'
addr.entries[3].label # => 'kind'
addr.entries[3].value # => 1
addr.entries[4].label # => 'identifier'
addr.entries[4].value # => 'damus'
```
## Profile (nprofile)
### Encoding
```ruby
relay_urls = %w[wss://relay.damus.io wss://nos.lol]
nprofile = Nostr::Bech32.nprofile_encode(pubkey: keypair.public_key, relays: relay_urls)
nprofile # => nprofile1qqs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dsxe58m5
```
### Decoding
```ruby
type, profile = Nostr::Bech32.decode('nprofile1qqs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dsxe58m5')
type # => 'nprofile'
profile.entries[0].label # => 'pubkey'
profile.entries[0].value # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
profile.entries[1].label # => 'relay'
profile.entries[1].value # => 'wss://relay.damus.io'
profile.entries[2].label # => 'relay'
profile.entries[2].value # => 'wss://nos.lol'
```
## Other simple types (note)
### Encoding
```ruby{8-9}
user = Nostr::User.new(keypair: keypair)
text_note_event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
created_at: 1700467997,
content: 'Your feedback is appreciated, now pay $8'
)
note = Nostr::Bech32.encode(hrp: 'note', data: text_note_event.id)
note # => 'note10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qnx3ujq'
```
### Decoding
```ruby
type, note = Nostr::Bech32.decode('note1pldep78zxnf5qrk6lhfx6jflzthup47793he7g0ej7z86vad963s42v0rr')
type # => 'note'
note # => '0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3'
```

108
docs/core/client.md Normal file
View File

@@ -0,0 +1,108 @@
# Client
Clients establish a WebSocket connection to [relays](../relays/connecting-to-a-relay). Through this connection, clients
communicate and subscribe to a range of [Nostr events](../events) based on specified subscription filters. These filters
define the Nostr events a client wishes to receive updates about.
::: info
Clients do not need to sign up or create an account to use Nostr. Upon connecting to a relay, a client provides
its subscription filters. The relay then streams events that match these filters to the client for the duration of the
connection.
:::
## WebSocket events
Communication between clients and relays happen via WebSockets. The client will emit WebSocket events when the
connection is __opened__, __closed__, when a __message__ is received or when there's an __error__.
::: info
WebSocket events are not [Nostr events](https://nostr.com/the-protocol/events). They are events emitted by the
WebSocket connection. The WebSocket `:message` event, however, contains a Nostr event in its payload.
:::
### connect
The `:connect` event is fired when a connection with a WebSocket is opened. You must call `Nostr::Client#connect` first.
```ruby
client = Nostr::Client.new
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
client.on :connect do
# When this block executes, you're connected to the relay
end
# Connect to a relay asynchronously
client.connect(relay)
```
Once the connection is open, you can send events to the relay, manage subscriptions, etc.
::: tip
Define the connection event handler before calling
[`Nostr::Client#connect`](https://www.rubydoc.info/gems/nostr/Nostr/Client#connect-instance_method). Otherwise,
you may miss the event.
:::
### error
The `:error` event is fired when a connection with a WebSocket has been closed because of an error.
```ruby
client.on :error do |error_message|
puts error_message
end
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the
# server certificate for 'rsslay.fiatjaf.com'
```
### message
The `:message` event is fired when data is received through a WebSocket.
```ruby
client.on :message do |message|
puts message
end
```
The message will be one of these 4 types, which must also be JSON arrays, according to the following patterns:
- `["EVENT", <subscription_id>, <event JSON>]`
- `["OK", <event_id>, <true|false>, <message>]`
- `["EOSE", <subscription_id>]`
- `["NOTICE", <message>]`
::: details Click me to see how a WebSocket message looks like
```ruby
[
"EVENT",
"d34107357089bfc9882146d3bfab0386",
{
"content": "",
"created_at": 1676456512,
"id": "18f63550da74454c5df7caa2a349edc5b2a6175ea4c5367fa4b4212781e5b310",
"kind": 3,
"pubkey": "117a121fa41dc2caa0b3d6c5b9f42f90d114f1301d39f9ee96b646ebfee75e36",
"sig": "d171420bd62cf981e8f86f2dd8f8f86737ea2bbe2d98da88db092991d125535860d982139a3c4be39886188613a9912ef380be017686a0a8b74231dc6e0b03cb",
"tags":[
["p", "1cc821cc2d47191b15fcfc0f73afed39a86ac6fb34fbfa7993ee3e0f0186ef7c"]
]
}
]
```
:::
### close
The `:close` event is fired when a connection with a WebSocket is closed.
```ruby
client.on :close do |code, reason|
puts "Error: #{code} - #{reason}"
end
```
::: tip
This handler is useful to attempt to reconnect to the relay.
:::

136
docs/core/keys.md Normal file
View File

@@ -0,0 +1,136 @@
# Keys
To [sign events](#signing-an-event), you need a **private key**. To verify signatures, you need a **public key**. The combination of a
private and a public key is called a **keypair**.
Both public and private keys are 64-character hexadecimal strings. They can be represented in bech32 format,
which is a human-readable format that starts with `nsec` for private keys and `npub` for public keys.
There are a few ways to generate a keypair.
## a) Generating a keypair
If you don't have any keys, you can generate a keypair using the
[`Nostr::Keygen`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen) class:
```ruby
keygen = Nostr::Keygen.new
keypair = keygen.generate_key_pair
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
```
## b) Generating a private key and a public key
Alternatively, if you have already generated a private key, you can extract the corresponding public key by calling
`Keygen#extract_public_key`:
```ruby
keygen = Nostr::Keygen.new
private_key = keygen.generate_private_key
public_key = keygen.extract_public_key(private_key)
private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
```
## c) Using existing hexadecimal keys
If you already have a private key and a public key in hexadecimal format, you can create a keypair using the
`Nostr::KeyPair` class:
```ruby
keypair = Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
)
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
```
### d) Use existing bech32 keys
If you already have a private key and a public key in bech32 format, you can create a keypair using the
`Nostr::KeyPair` class:
```ruby
keypair = Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.from_bech32('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'),
public_key: Nostr::PublicKey.from_bech32('npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'),
)
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
```
## e) Using an existing hexadecimal private key
If you already have a private key in hexadecimal format, you can create a keypair using the method
[`Nostr::Keygen#get_key_pair_from_private_key`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen#get_key_pair_from_private_key-instance_method):
```ruby
private_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
keygen= Nostr::Keygen.new
keypair = keygen.get_key_pair_from_private_key(private_key)
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
```
## f) Using an existing bech32 private key
If you already have a private key in bech32 format, you can create a keypair using the methods
[`Nostr::PrivateKey.from_bech32`](https://www.rubydoc.info/gems/nostr/Nostr/PrivateKey.from_bech32-class_method) and
[`Nostr::Keygen#get_key_pair_from_private_key`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen#get_key_pair_from_private_key-instance_method):
```ruby
private_key = Nostr::PrivateKey.from_bech32('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5')
keygen= Nostr::Keygen.new
keypair = keygen.get_key_pair_from_private_key(private_key)
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
```
## Signing an event
KeyPairs are used to sign [events](../events). To create a signed event, you need to instantiate a
[`Nostr::User`](https://www.rubydoc.info/gems/nostr/Nostr/User) with a keypair:
```ruby{8,11-14}
# Use an existing keypair
keypair = Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
)
# Add the keypair to a user
user = Nostr::User.new(keypair: keypair)
# Create signed events
text_note = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
```
::: details Click me to view the text_note
```ruby
# text_note.to_h
{
id: '030fbc71151379e5b58e7428ed6e7f2884e5dfc9087fd64d1dc4cc677f5097c8',
pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e', # from the keypair
created_at: 1700119819,
kind: 1, # Nostr::EventKind::TEXT_NOTE,
tags: [],
content: 'Your feedback is appreciated, now pay $8',
sig: '586877896ef6f7d54fa4dd2ade04e3fdc4dfcd6166dd0df696b3c3c768868c0b690338f5baed6ab4fc717785333cb487363384de9fb0f740ac4775522cb4acb3' # signed with the private key from the keypair
}
```
:::

43
docs/core/user.md Normal file
View File

@@ -0,0 +1,43 @@
# User
The class [`Nostr::User`](https://www.rubydoc.info/gems/nostr/Nostr/User) is an abstraction to facilitate the creation
of signed events. It is not required to use it to create events, but it is recommended.
Here's an example of how to create a signed event without the class `Nostr::User`:
```ruby
event = Nostr::Event.new(
pubkey: keypair.public_key,
kind: Nostr::EventKind::TEXT_NOTE,
tags: [],
content: 'Your feedback is appreciated, now pay $8',
)
event.sign(keypair.private_key)
```
::: details Click me to view the event
```ruby
# event.to_h
{
id: '5feb10973dbcf5f210cfc1f0aa338fee62bed6a29696a67957713599b9baf0eb',
pubkey: 'b9b9821074d1b60b8fb4a3983632af3ef9669f55b20d515bf982cda5c439ad61',
created_at: 1699847447,
kind: 1, # Nostr::EventKind::TEXT_NOTE,
tags: [],
content: 'Your feedback is appreciated, now pay $8',
sig: 'e30f2f08331f224e41a4099d16aefc780bf9f2d1191b71777e1e1789e6b51fdf7bb956f25d4ea9a152d1c66717a9d68c081ce6c89c298c3c5e794914013381ab'
}
```
:::
And here's how to create it with the class `Nostr::User`:
```ruby
user = Nostr::User.new(keypair: keypair)
event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
```

11
docs/events.md Normal file
View File

@@ -0,0 +1,11 @@
# Events
## Event Kinds
| kind | description | NIP |
| ------------- |----------------------------------------------------------------| -------------------------------------------------------------- |
| `0` | [Metadata](./events/set-metadata) | [1](https://github.com/nostr-protocol/nips/blob/master/01.md) |
| `1` | [Short Text Note](./events/text-note) | [1](https://github.com/nostr-protocol/nips/blob/master/01.md) |
| `2` | [Recommend Relay](./events/recommend-server) | |
| `3` | [Contacts](./events/contact-list) | [2](https://github.com/nostr-protocol/nips/blob/master/02.md) |
| `4` | [Encrypted Direct Messages](./events/encrypted-direct-message) | [4](https://github.com/nostr-protocol/nips/blob/master/04.md) |

View File

@@ -0,0 +1,29 @@
# Contact List
## Creating/updating your contact list
Every new contact list that gets published overwrites the past ones, so it should contain all entries.
```ruby
# Creating a contact list event with 2 contacts
update_contacts_event = user.create_event(
kind: Nostr::EventKind::CONTACT_LIST,
tags: [
[
"p", # mandatory
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", # public key of the user to add to the contacts
"wss://alicerelay.com/", # can be an empty string or can be omitted
"alice" # can be an empty string or can be omitted
],
[
"p", # mandatory
"3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", # public key of the user to add to the contacts
"wss://bobrelay.com/nostr", # can be an empty string or can be omitted
"bob" # can be an empty string or can be omitted
],
],
)
# Send it to the Relay
client.publish(update_contacts_event)
```

View File

@@ -0,0 +1,28 @@
# Encrypted Direct Message
## Sending an encrypted direct message
```ruby
sender_private_key = '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
encrypted_direct_message = Nostr::Events::EncryptedDirectMessage.new(
sender_private_key: sender_private_key,
recipient_public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0',
plain_text: 'Your feedback is appreciated, now pay $8',
previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460' # optional
)
encrypted_direct_message.sign(sender_private_key)
# #<Nostr::Events::EncryptedDirectMessage:0x0000000104c9fa68
# @content="mjIFNo1sSP3KROE6QqhWnPSGAZRCuK7Np9X+88HSVSwwtFyiZ35msmEVoFgRpKx4?iv=YckChfS2oWCGpMt1uQ4GbQ==",
# @created_at=1676456512,
# @id="daac98826d5eb29f7c013b6160986c4baf4fe6d4b995df67c1b480fab1839a9b",
# @kind=4,
# @pubkey="8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca",
# @sig="028bb5f5bab0396e2065000c84a4bcce99e68b1a79bb1b91a84311546f49c5b67570b48d4a328a1827e7a8419d74451347d4f55011a196e71edab31aa3d6bdac",
# @tags=[["p", "6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0"], ["e", "ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460"]]>
# Send it to the Relay
client.publish(encrypted_direct_message)
```

View File

@@ -0,0 +1,32 @@
# Recommend Server
The `Recommend Server` event, has a set of tags with the following structure `['e', <event-id>, <relay-url>, <marker>]`
Where:
- `<event-id>` is the id of the event being referenced.
- `<relay-url>` is the URL of a recommended relay associated with the reference. Clients SHOULD add a valid `<relay-URL>`
field, but may instead leave it as `''`.
- `<marker>` is optional and if present is one of `'reply'`, `'root'`, or `'mention'`.
Those marked with `'reply'` denote the id of the reply event being responded to. Those marked with `'root'` denote the
root id of the reply thread being responded to. For top level replies (those replying directly to the root event),
only the `'root'` marker should be used. Those marked with `'mention'` denote a quoted or reposted event id.
A direct reply to the root of a thread should have a single marked `'e'` tag of type `'root'`.
## Recommending a server
```ruby
recommend_server_event = user.create_event(
kind: Nostr::EventKind::RECOMMEND_SERVER,
tags: [
[
'e',
'461544014d87c9eaf3e76e021240007dff2c7afb356319f99c741b45749bf82f',
'wss://relay.damus.io'
],
]
)
client.publish(recommend_server_event)
```

View File

@@ -0,0 +1,20 @@
# Set Metadata
In the `Metadata` event, the `content` is set to a stringified JSON object
`{name: <username>, about: <string>, picture: <url, string>}` describing the [user](../core/user) who created the event. A relay may
delete older events once it gets a new one for the same pubkey.
## Setting the user's metadata
```ruby
metadata_event = user.create_event(
kind: Nostr::EventKind::SET_METADATA,
content: {
name: 'Wilson Silva',
about: 'Used to make hydrochloric acid bombs in high school.',
picture: 'https://thispersondoesnotexist.com/'
}
)
client.publish(metadata_event)
```

15
docs/events/text-note.md Normal file
View File

@@ -0,0 +1,15 @@
# Text Note
In the `Text Note` event, the `content` is set to the plaintext content of a note (anything the user wants to say).
Content that must be parsed, such as Markdown and HTML, should not be used.
## Sending a text note event
```ruby
text_note_event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
client.publish(text_note_event)
```

View File

@@ -0,0 +1,21 @@
# Installation
Install the gem and add to the application's Gemfile by executing:
```shell
bundle add nostr
```
If bundler is not being used to manage dependencies, install the gem by executing:
```shell
gem install nostr
```
## Requiring the gem
All examples in this guide assume that the gem has been required:
```ruby
require 'nostr'
```

View File

@@ -0,0 +1,170 @@
---
editLink: true
---
# Getting started
This gem abstracts the complexity that you would face when trying to connect to relays web sockets, send and receive
events, handle events callbacks and much more.
## Visual overview
Begin your journey with an overview of the essential functions. A visual representation below maps out the key
components we'll delve into in this section.
```mermaid
classDiagram
class Client {
connect(relay)
publish(event)
subscribe(subscription_id, filter)
unsubscribe(subscription_id)
}
class Relay {
url
name
}
class Event {
pubkey
created_at
kind
tags
content
add_event_reference(event_id)
add_pubkey_reference(pubkey)
serialize()
to_h()
sign(private_key)
}
class Subscription {
id
filter
}
class Filter {
ids
authors
kinds
since
until
limit
to_h()
}
class EventKind {
<<Enumeration>>
SET_METADATA
TEXT_NOTE
RECOMMEND_SERVER
CONTACT_LIST
ENCRYPTED_DIRECT_MESSAGE
}
class KeyPair {
private_key
public_key
}
class Keygen {
generate_key_pair()
generate_private_key()
extract_public_key(private_key)
}
class User {
keypair
create_event(event_attributes)
}
Client --> Relay : connects via <br> WebSockets to
Client --> Event : uses WebSockets to <br> publish and receive
Client --> Subscription : receives Events via
Subscription --> Filter : uses
Event --> EventKind : is of kind
User --> KeyPair : has
User --> Event : creates and signs
User --> Keygen : uses to generate keys
Keygen --> KeyPair : generates
```
## Code overview
Explore the provided code snippet to learn about initializing the Nostr [client](../core/client.md), generating
a [keypair](../core/keys), [publishing](../relays/publishing-events) an event, and
efficiently [managing event subscriptions](../subscriptions/creating-a-subscription) (including event reception,
filtering, and WebSocket event handling).
```ruby
# Require the gem
require 'nostr'
# Instantiate a client
client = Nostr::Client.new
# a) Use an existing keypair
keypair = Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.new('your-hex-private-key'),
public_key: Nostr::PublicKey.new('your-hex-public-key'),
)
# b) Or build a keypair from a private key
keygen = Nostr::Keygen.new
keypair = keygen.get_key_pair_from_private_key(
Nostr::PrivateKey.new('your-hex-private-key')
)
# c) Or create a new keypair
keygen = Nostr::Keygen.new
keypair = keygen.generate_keypair
# Create a user with the keypair
user = Nostr::User.new(keypair: keypair)
# Create a signed event
text_note_event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
# Connect asynchronously to a relay
relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
client.connect(relay)
# Listen asynchronously for the connect event
client.on :connect do
# Send the event to the Relay
client.publish(text_note_event)
# Create a filter to receive the first 20 text notes
# and encrypted direct messages from the relay that
# were created in the previous hour
filter = Nostr::Filter.new(
kinds: [
Nostr::EventKind::TEXT_NOTE,
Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE
],
since: Time.now.to_i - 3600, # 1 hour ago
until: Time.now.to_i,
limit: 20,
)
# Subscribe to events matching conditions of a filter
subscription = client.subscribe(filter: filter)
# Unsubscribe from events matching the filter above
client.unsubscribe(subscription.id)
end
# Listen for incoming messages and print them
client.on :message do |message|
puts message
end
# Listen for error messages
client.on :error do |error_message|
# Handle the error
end
# Listen for the close event
client.on :close do |code, reason|
# You may attempt to reconnect to the relay here
end
```
Beyond what's covered here, the Nostr protocol and this gem boast a wealth of additional functionalities. For an
in-depth exploration of these capabilities, proceed to the next page.

9
docs/implemented-nips.md Normal file
View File

@@ -0,0 +1,9 @@
# Implemented NIPs
NIPs stand for Nostr Implementation Possibilities. They exist to document what may be implemented by Nostr-compatible
relay and client software.
- [NIP-01: Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [NIP-02: Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md)
- [NIP-04: Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)
- [NIP-19: Bech32-encoded entities](https://github.com/nostr-protocol/nips/blob/master/19.md)

44
docs/index.md Normal file
View File

@@ -0,0 +1,44 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "Nostr"
text: "Ruby"
tagline: "The Nostr protocol implemented in a Ruby gem."
actions:
- theme: brand
text: Getting Started
link: /getting-started/overview
- theme: alt
text: View on Github
link: https://github.com/wilsonsilva/nostr
- theme: alt
text: View on RubyDoc
link: https://www.rubydoc.info/gems/nostr
- theme: alt
text: View on RubyGems
link: https://rubygems.org/gems/nostr
features:
- title: Asynchronous
details: Non-blocking I/O for maximum performance.
icon: ⚡
- title: Lightweight
details: Minimal runtime dependencies.
icon: 🪶
- title: Intuitive
details: The API mirrors the Nostr protocol specification domain language.
icon: 💡
- title: Fully documented
details: All code is documented from both the maintainer's as well as the consumer's perspective.
icon: 📚
- title: Fully tested
details: All code is tested with 100% coverage.
icon: 🧪
- 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: ✅
---

85
docs/markdown-examples.md Normal file
View File

@@ -0,0 +1,85 @@
# Markdown Extension Examples
This page demonstrates some of the built-in markdown extensions provided by VitePress.
## Syntax Highlighting
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
**Input**
````
```js{4}
export default {
data () {
return {
msg: 'Highlighted!'
}
}
}
```
````
**Output**
```js{4}
export default {
data () {
return {
msg: 'Highlighted!'
}
}
}
```
## Custom Containers
**Input**
```md
::: info
This is an info box.
:::
::: tip
This is a tip.
:::
::: warning
This is a warning.
:::
::: danger
This is a dangerous warning.
:::
::: details
This is a details block.
:::
```
**Output**
::: info
This is an info box.
:::
::: tip
This is a tip.
:::
::: warning
This is a warning.
:::
::: danger
This is a dangerous warning.
:::
::: details
This is a details block.
:::
## More
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).

12
docs/package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"devDependencies": {
"mermaid": "^10.6.1",
"vitepress": "^1.0.0-rc.25",
"vitepress-plugin-mermaid": "^2.0.15"
}
}

View File

@@ -0,0 +1,21 @@
# Connecting to a Relay
You must connect your nostr [Client](../core/client) to a relay in order to send and receive [Events](../events).
Instantiate a [`Nostr::Client`](https://www.rubydoc.info/gems/nostr/Nostr/Client) and a
[`Nostr::Relay`](https://www.rubydoc.info/gems/nostr/Nostr/Relay) giving it the `url` of your relay. The `name`
attribute is just descriptive.
Calling [`Client#connect`](https://www.rubydoc.info/gems/nostr/Nostr/Client#connect-instance_method) attempts to
establish a WebSocket connection between the Client and the Relay.
```ruby
client = Nostr::Client.new
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
# Listen for the connect event
client.on :connect do
# When this block executes, you're connected to the relay
end
# Connect to a relay asynchronously
client.connect(relay)
```

View File

@@ -0,0 +1,29 @@
# Publishing events
Create a [signed event](../core/keys) and call the method
[`Nostr::Client#publish`](https://www.rubydoc.info/gems/nostr/Nostr/Client#publish-instance_method) to send the
event to the relay.
```ruby{4-8,17}
# Create a user with the keypair
user = Nostr::User.new(keypair: keypair)
# Create a signed event
text_note_event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
# Connect asynchronously to a relay
relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
client.connect(relay)
# Listen asynchronously for the connect event
client.on :connect do
# Send the event to the relay
client.publish(text_note_event)
end
```
The relay will verify the signature of the event with the public key. If the signature is valid, the relay should
broadcast the event to all subscribers.

View File

@@ -0,0 +1,6 @@
# Receiving events
To receive events from Relays, you must create a subscription on the relay. A subscription is a filter that defines the
events you want to receive.
For more information, read the [Subscription](../subscriptions/creating-a-subscription.md) section.

View File

@@ -0,0 +1,49 @@
# Creating a subscription
A client can request events and subscribe to new updates __after__ it has established a connection with the Relay.
You may use a [`Nostr::Filter`](https://www.rubydoc.info/gems/nostr/Nostr/Filter) instance with as many attributes as
you wish:
```ruby
client.on :connect do
filter = Nostr::Filter.new(
ids: ['8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8'],
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
kinds: [Nostr::EventKind::TEXT_NOTE],
e: ["f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52"],
p: ["f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8"],
since: 1230981305,
until: 1292190341,
limit: 420,
)
subscription = client.subscribe(subscription_id: 'an-id', filter: filter)
end
```
With just a few:
```ruby
client.on :connect do
filter = Nostr::Filter.new(kinds: [Nostr::EventKind::TEXT_NOTE])
subscription = client.subscribe(subscription_id: 'an-id', filter: filter)
end
```
Or omit the filter:
```ruby
client.on :connect do
subscription = client.subscribe(subscription_id: 'an-id')
end
```
Or even omit the subscription id:
```ruby
client.on :connect do
subscription = client.subscribe(filter: filter)
subscription.id # => "13736f08dee8d7b697222ba605c6fab2" (randomly generated)
end
```

View File

@@ -0,0 +1,10 @@
# Stop previous subscriptions
You can stop receiving messages from a subscription by calling
[`Nostr::Client#unsubscribe`](https://www.rubydoc.info/gems/nostr/Nostr/Client#unsubscribe-instance_method) with the
ID of the subscription you want to stop receiving messages from:
```ruby
client.unsubscribe('your-subscription-id')
client.unsubscribe(subscription.id)
```

View File

@@ -0,0 +1,115 @@
# Filtering events
## Filtering by id
You can filter events by their ids:
```ruby
filter = Nostr::Filter.new(
ids: [
# matches events with these exact IDs
'8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8',
'461544014d87c9eaf3e76e021240007dff2c7afb356319f99c741b45749bf82f',
]
)
subscription = client.subscribe(filter: filter)
```
## Filtering by author
You can filter events by their author's pubkey:
```ruby
filter = Nostr::Filter.new(
authors: [
# matches events whose (authors) pubkey match these exact IDs
'b698043170d580f8ae5bad4ac80b1fdb508e957f0bbffe97f2a8915fa8b34070',
'51f853ff4894b062950e46ebed8c1c7015160f8173994414a96dd286f65f0f49',
]
)
subscription = client.subscribe(filter: filter)
```
## Filtering by kind
You can filter events by their kind:
```ruby
filter = Nostr::Filter.new(
kinds: [
# matches events whose kind is TEXT_NOTE
Nostr::EventKind::TEXT_NOTE,
# and matches events whose kind is CONTACT_LIST
Nostr::EventKind::CONTACT_LIST,
]
)
subscription = client.subscribe(filter: filter)
```
## Filtering by referenced event
You can filter events by the events they reference (in their `e` tag):
```ruby
filter = Nostr::Filter.new(
e: [
# matches events that reference other events whose ids match these exact IDs
'f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52',
'f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8',
]
)
subscription = client.subscribe(filter: filter)
```
## Filtering by referenced pubkey
You can filter events by the pubkeys they reference (in their `p` tag):
```ruby
filter = Nostr::Filter.new(
p: [
# matches events that reference other pubkeys that match these exact IDs
'b698043170d580f8ae5bad4ac80b1fdb508e957f0bbffe97f2a8915fa8b34070',
'51f853ff4894b062950e46ebed8c1c7015160f8173994414a96dd286f65f0f49',
]
)
subscription = client.subscribe(filter: filter)
```
## Filtering by timestamp
You can filter events by their timestamp:
```ruby
filter = Nostr::Filter.new(
since: 1230981305, # matches events that are newer than this timestamp
until: 1292190341, # matches events that are older than this timestamp
)
subscription = client.subscribe(filter: filter)
```
## Limiting the number of events
You can limit the number of events received:
```ruby
filter = Nostr::Filter.new(
limit: 420, # matches at most 420 events
)
subscription = client.subscribe(filter: filter)
```
## Combining filters
You can combine filters. For example, to match `5` text note events that are newer than `1230981305` from the author
`ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577`:
```ruby
filter = Nostr::Filter.new(
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
kinds: [Nostr::EventKind::TEXT_NOTE],
since: 1230981305,
limit: 5,
)
subscription = client.subscribe(filter: filter)
```

View File

@@ -0,0 +1,4 @@
# Updating a subscription
Updating a subscription is done by creating a new subscription with the same id as the previous one. See
[creating a subscription](./creating-a-subscription.md) for more information.

View File

@@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'nostr/errors'
require_relative 'nostr/bech32'
require_relative 'nostr/crypto' require_relative 'nostr/crypto'
require_relative 'nostr/version' require_relative 'nostr/version'
require_relative 'nostr/keygen' require_relative 'nostr/keygen'
@@ -7,12 +9,16 @@ require_relative 'nostr/client_message_type'
require_relative 'nostr/filter' require_relative 'nostr/filter'
require_relative 'nostr/subscription' require_relative 'nostr/subscription'
require_relative 'nostr/relay' require_relative 'nostr/relay'
require_relative 'nostr/relay_message_type'
require_relative 'nostr/key_pair' require_relative 'nostr/key_pair'
require_relative 'nostr/event_kind' require_relative 'nostr/event_kind'
require_relative 'nostr/event' require_relative 'nostr/event'
require_relative 'nostr/events/encrypted_direct_message' require_relative 'nostr/events/encrypted_direct_message'
require_relative 'nostr/client' require_relative 'nostr/client'
require_relative 'nostr/user' require_relative 'nostr/user'
require_relative 'nostr/key'
require_relative 'nostr/private_key'
require_relative 'nostr/public_key'
# Encapsulates all the gem's logic # Encapsulates all the gem's logic
module Nostr module Nostr

203
lib/nostr/bech32.rb Normal file
View File

@@ -0,0 +1,203 @@
# frozen_string_literal: true
require 'bech32'
require 'bech32/nostr'
require 'bech32/nostr/entity'
module Nostr
# Bech32 encoding and decoding
#
# @api public
#
module Bech32
# Decodes a bech32-encoded string
#
# @api public
#
# @example
# bech32_value = 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
# Nostr::Bech32.decode(bech32_value) # => ['npub', '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d8...']
#
# @param [String] bech32_value The bech32-encoded string to decode
#
# @return [Array<String, String>] The human readable part and the data
#
def self.decode(bech32_value)
entity = ::Bech32::Nostr::NIP19.decode(bech32_value)
case entity
in ::Bech32::Nostr::BareEntity
[entity.hrp, entity.data]
in ::Bech32::Nostr::TLVEntity
[entity.hrp, entity.entries]
end
end
# Encodes data into a bech32 string
#
# @api public
#
# @example
# Nostr::Bech32.encode(hrp: 'npub', data: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
# # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
#
# @param [String] hrp The human readable part (npub, nsec, nprofile, nrelay, nevent, naddr, etc)
# @param [String] data The data to encode
#
# @return [String] The bech32-encoded string
#
def self.encode(hrp:, data:)
::Bech32::Nostr::BareEntity.new(hrp, data).encode
end
# Encodes a hex-encoded public key into a bech32 string
#
# @api public
#
# @example
# Nostr::Bech32.npub_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
# # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
#
# @param [String] npub The public key to encode
#
# @see Nostr::Bech32#encode
# @see Nostr::PublicKey#to_bech32
# @see Nostr::PrivateKey#to_bech32
#
# @return [String] The bech32-encoded string
#
def self.npub_encode(npub)
encode(hrp: 'npub', data: npub)
end
# Encodes a hex-encoded private key into a bech32 string
#
# @api public
#
# @example
# Nostr::Bech32.nsec_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
# # => 'nsec10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
#
# @param [String] nsec The private key to encode
#
# @see Nostr::Bech32#encode
# @see Nostr::PrivateKey#to_bech32
# @see Nostr::PublicKey#to_bech32
#
# @return [String] The bech32-encoded string
#
def self.nsec_encode(nsec)
encode(hrp: 'nsec', data: nsec)
end
# Encodes an address into a bech32 string
#
# @api public
#
# @example
# naddr = Nostr::Bech32.naddr_encode(
# pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e',
# relays: ['wss://relay.damus.io', 'wss://nos.lol'],
# kind: Nostr::EventKind::TEXT_NOTE,
# identifier: 'damus'
# )
# naddr # => 'naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7ns...'
#
# @param [PublicKey] pubkey The public key to encode
# @param [Array<String>] relays The relays to encode
# @param [String] kind The kind of address to encode
# @param [String] identifier The identifier of the address to encode
#
# @return [String] The bech32-encoded string
#
def self.naddr_encode(pubkey:, relays: [], kind: nil, identifier: nil)
entry_relays = relays.map do |relay_url|
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
end
pubkey_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_AUTHOR, pubkey)
kind_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_KIND, kind)
identifier_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, identifier)
entries = [pubkey_entry, *entry_relays, kind_entry, identifier_entry].compact
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_EVENT_COORDINATE, entries)
entity.encode
end
# Encodes an event into a bech32 string
#
# @api public
#
# @example
# nevent = Nostr::Bech32.nevent_encode(
# id: '0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3',
# relays: ['wss://relay.damus.io', 'wss://nos.lol'],
# kind: Nostr::EventKind::TEXT_NOTE,
# )
# nevent # => 'nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0pra...'
#
# @param [PublicKey] id The id the event to encode
# @param [Array<String>] relays The relays to encode
# @param [String] kind The kind of event to encode
#
# @return [String] The bech32-encoded string
#
def self.nevent_encode(id:, relays: [], kind: nil)
entry_relays = relays.map do |relay_url|
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
end
id_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_AUTHOR, id)
kind_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_KIND, kind)
entries = [id_entry, *entry_relays, kind_entry].compact
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_EVENT, entries)
entity.encode
end
# Encodes a profile into a bech32 string
#
# @api public
#
# @example
# nprofile = Nostr::Bech32.nprofile_encode(
# pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e',
# relays: ['wss://relay.damus.io', 'wss://nos.lol']
# )
#
# @param [PublicKey] pubkey The public key to encode
# @param [Array<String>] relays The relays to encode
#
# @return [String] The bech32-encoded string
#
def self.nprofile_encode(pubkey:, relays: [])
entry_relays = relays.map do |relay_url|
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
end
pubkey_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, pubkey)
entries = [pubkey_entry, *entry_relays].compact
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_PROFILE, entries)
entity.encode
end
# Encodes a relay URL into a bech32 string
#
# @api public
#
# @example
# nrelay = Nostr::Bech32.nrelay_encode('wss://relay.damus.io')
# nrelay # => 'nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x'
#
# @param [String] relay_url The relay url to encode
#
# @return [String] The bech32-encoded string
#
def self.nrelay_encode(relay_url)
relay_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, relay_url)
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_RELAY, [relay_entry])
entity.encode
end
end
end

View File

@@ -74,7 +74,8 @@ module Nostr
# @example Subscribing to all events created after a certain time # @example Subscribing to all events created after a certain time
# subscription = client.subscribe(filter: Nostr::Filter.new(since: 1230981305)) # subscription = client.subscribe(filter: Nostr::Filter.new(since: 1230981305))
# #
# @param subscription_id [String] The subscription id. A random string. # @param subscription_id [String] The subscription id. An arbitrary, non-empty string of max length 64
# chars used to represent a subscription.
# @param filter [Filter] A set of attributes that represent the events that the client is interested in. # @param filter [Filter] A set of attributes that represent the events that the client is interested in.
# #
# @return [Subscription] The subscription object # @return [Subscription] The subscription object

View File

@@ -30,8 +30,8 @@ module Nostr
# encrypted = crypto.encrypt_text(sender_private_key, recipient_public_key, 'Feedback appreciated. Now pay $8') # encrypted = crypto.encrypt_text(sender_private_key, recipient_public_key, 'Feedback appreciated. Now pay $8')
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg==" # encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
# #
# @param sender_private_key [String] 32-bytes hex-encoded private key of the creator. # @param sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the creator.
# @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient. # @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient.
# @param plain_text [String] The text to be encrypted # @param plain_text [String] The text to be encrypted
# #
# @return [String] Encrypted text. # @return [String] Encrypted text.
@@ -54,14 +54,18 @@ module Nostr
# encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg==" # encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
# decrypted = crypto.decrypt_text(recipient_private_key, sender_public_key, encrypted) # decrypted = crypto.decrypt_text(recipient_private_key, sender_public_key, encrypted)
# #
# @param sender_public_key [String] 32-bytes hex-encoded public key of the message creator. # @param sender_public_key [PublicKey] 32-bytes hex-encoded public key of the message creator.
# @param recipient_private_key [String] 32-bytes hex-encoded public key of the recipient. # @param recipient_private_key [PrivateKey] 32-bytes hex-encoded public key of the recipient.
# @param encrypted_text [String] The text to be decrypted # @param encrypted_text [String] The text to be decrypted
# #
# @return [String] Decrypted text. # @return [String] Decrypted text.
# #
def decrypt_text(recipient_private_key, sender_public_key, encrypted_text) def decrypt_text(recipient_private_key, sender_public_key, encrypted_text)
base64_encoded_text, iv = encrypted_text.split('?iv=') base64_encoded_text, iv = encrypted_text.split('?iv=')
# Ensure iv and base64_encoded_text are not nil
return '' unless iv && base64_encoded_text
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt
cipher.iv = Base64.decode64(iv) cipher.iv = Base64.decode64(iv)
cipher.key = compute_shared_key(recipient_private_key, sender_public_key) cipher.key = compute_shared_key(recipient_private_key, sender_public_key)
@@ -80,7 +84,7 @@ module Nostr
# event.sig # => a signature # event.sig # => a signature
# #
# @param event [Event] The event to be signed # @param event [Event] The event to be signed
# @param private_key [String] 32-bytes hex-encoded private key. # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
# #
# @return [Event] An unsigned event. # @return [Event] An unsigned event.
# #
@@ -103,8 +107,8 @@ module Nostr
# #
# @api private # @api private
# #
# @param private_key [String] 32-bytes hex-encoded private key. # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
# @param public_key [String] 32-bytes hex-encoded public key. # @param public_key [PublicKey] 32-bytes hex-encoded public key.
# #
# @return [String] A shared key used in the event's content encryption and decryption. # @return [String] A shared key used in the event's content encryption and decryption.
# #

8
lib/nostr/errors.rb Normal file
View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
require_relative 'errors/error'
require_relative 'errors/key_validation_error'
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'

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
module Nostr
# Base error class
class Error < StandardError
end
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
module Nostr
# Raised when the human readable part of a Bech32 string is invalid
#
# @api public
#
class InvalidHRPError < KeyValidationError
# Initializes the error
#
# @example
# InvalidHRPError.new('example wrong hrp', 'nsec')
#
# @param given_hrp [String] The given human readable part of the Bech32 string
# @param allowed_hrp [String] The allowed human readable part of the Bech32 string
#
def initialize(given_hrp, allowed_hrp)
super("Invalid hrp: #{given_hrp}. The allowed hrp value for this kind of entity is '#{allowed_hrp}'.")
end
end
end

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module Nostr
# Raised when the private key is in an invalid format
#
# @api public
#
class InvalidKeyFormatError < KeyValidationError
# Initializes the error
#
# @example
# InvalidKeyFormatError.new('private'')
#
# @param [String] key_kind The kind of key that is invalid (public or private)
#
def initialize(key_kind)
super("Only lowercase hexadecimal characters are allowed in #{key_kind} keys.")
end
end
end

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module Nostr
# Raised when the private key's length is not 64 characters
#
# @api public
#
class InvalidKeyLengthError < KeyValidationError
# Initializes the error
#
# @example
# InvalidKeyLengthError.new('private'')
#
# @param [String] key_kind The kind of key that is invalid (public or private)
#
def initialize(key_kind)
super("Invalid #{key_kind} key length. It should have 64 characters.")
end
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
module Nostr
# Raised when the private key is not a string
#
# @api public
#
class InvalidKeyTypeError < KeyValidationError
# Initializes the error
#
# @example
# InvalidKeyTypeError.new('private'')
#
# @param [String] key_kind The kind of key that is invalid (public or private)
#
def initialize(key_kind) = super("Invalid #{key_kind} key type")
end
end

View File

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

View File

@@ -128,7 +128,6 @@ module Nostr
id: nil, id: nil,
sig: nil sig: nil
) )
@id = id @id = id
@sig = sig @sig = sig
@pubkey = pubkey @pubkey = pubkey
@@ -160,11 +159,11 @@ module Nostr
# pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e' # pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
# event.add_pubkey_reference(pubkey) # event.add_pubkey_reference(pubkey)
# #
# @param pubkey [String] 32-bytes hex-encoded public key. # @param pubkey [PublicKey] 32-bytes hex-encoded public key.
# #
# @return [Array<String>] The event's updated list of tags # @return [Array<String>] The event's updated list of tags
# #
def add_pubkey_reference(pubkey) = tags.push(['p', pubkey]) def add_pubkey_reference(pubkey) = tags.push(['p', pubkey.to_s])
# Signs an event with the user's private key # Signs an event with the user's private key
# #
@@ -173,7 +172,7 @@ module Nostr
# @example Signing an event # @example Signing an event
# event.sign(private_key) # event.sign(private_key)
# #
# @param private_key [String] 32-bytes hex-encoded private key. # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
# #
# @return [Event] A signed event. # @return [Event] A signed event.
# #

View File

@@ -26,8 +26,9 @@ module Nostr
# ) # )
# #
# @param plain_text [String] The +content+ of the encrypted message. # @param plain_text [String] The +content+ of the encrypted message.
# @param sender_private_key [String] 32-bytes hex-encoded private key of the message's author. # @param sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the message's author.
# @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient of the encrypted message. # @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient of the encrypted
# message.
# @param previous_direct_message [String] 32-bytes hex-encoded id identifying the previous message in a # @param previous_direct_message [String] 32-bytes hex-encoded id identifying the previous message in a
# conversation or a message we are explicitly replying to (such that contextual, more organized conversations # conversation or a message we are explicitly replying to (such that contextual, more organized conversations
# may happen # may happen
@@ -43,7 +44,7 @@ module Nostr
pubkey: sender_public_key, pubkey: sender_public_key,
kind: Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE, kind: Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE,
content: encrypted_content, content: encrypted_content,
) )
add_pubkey_reference(recipient_public_key) add_pubkey_reference(recipient_public_key)
add_event_reference(previous_direct_message) if previous_direct_message add_event_reference(previous_direct_message) if previous_direct_message

View File

@@ -3,7 +3,7 @@
module Nostr module Nostr
# A filter determines what events will be sent in a subscription. # A filter determines what events will be sent in a subscription.
class Filter class Filter
# A list of event ids or prefixes # A list of event ids
# #
# @api public # @api public
# #
@@ -14,7 +14,7 @@ module Nostr
# #
attr_reader :ids attr_reader :ids
# A list of pubkeys or prefixes, the pubkey of an event must be one of these # A list of pubkeys, the pubkey of an event must be one of these
# #
# @api public # @api public
# #
@@ -107,8 +107,8 @@ module Nostr
# ) # )
# #
# @param kwargs [Hash] # @param kwargs [Hash]
# @option kwargs [Array<String>, nil] ids A list of event ids or prefixes # @option kwargs [Array<String>, nil] ids A list of event ids
# @option kwargs [Array<String>, nil] authors A list of pubkeys or prefixes, the pubkey of an event must be one # @option kwargs [Array<String>, nil] authors A list of pubkeys, the pubkey of an event must be one
# of these # of these
# @option kwargs [Array<Integer>, nil] kinds A list of a kind numbers # @option kwargs [Array<Integer>, nil] kinds A list of a kind numbers
# @option kwargs [Array<String>, nil] e A list of event ids that are referenced in an "e" tag # @option kwargs [Array<String>, nil] e A list of event ids that are referenced in an "e" tag

100
lib/nostr/key.rb Normal file
View File

@@ -0,0 +1,100 @@
# frozen_string_literal: true
module Nostr
# Abstract class for all keys
#
# @api private
#
class Key < 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 key in hex format
#
# @return [Integer] The length of the key in hex format
#
LENGTH = 64
# Instantiates a new key. Can't be used directly because this is an abstract class. Raises a +ValidationError+
#
# @see Nostr::PrivateKey
# @see Nostr::PublicKey
#
# @param [String] hex_value Hex-encoded value of the key
#
# @raise [ValidationError]
#
def initialize(hex_value)
validate_hex_value(hex_value)
super(hex_value)
end
# Instantiates a key from a bech32 string
#
# @api public
#
# @example
# bech32_key = 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
# bech32_key.to_key # => #<Nostr::PublicKey:0x000000010601e3c8 @hex_value="...">
#
# @raise [Nostr::InvalidHRPError] if the bech32 string is invalid.
#
# @param [String] bech32_value The bech32 string representation of the key.
#
# @return [Key] the key.
#
def self.from_bech32(bech32_value)
type, data = Bech32.decode(bech32_value)
raise InvalidHRPError.new(type, hrp) unless type == hrp
new(data)
end
# Abstract method to be implemented by subclasses to provide the HRP (npub, nsec)
#
# @api private
#
# @return [String] The HRP
#
def self.hrp
raise 'Subclasses must implement this method'
end
# Converts the key to a bech32 string representation
#
# @api public
#
# @example Converting a private key to a bech32 string
# public_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
# public_key.to_bech32 # => 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
#
# @example Converting a public key to a bech32 string
# public_key = Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
# public_key.to_bech32 # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
#
# @return [String] The bech32 string representation of the key
#
def to_bech32 = Bech32.encode(hrp: self.class.hrp, data: self)
protected
# Validates the hex value during initialization
#
# @api private
#
# @param [String] _hex_value The hex value of the key
#
# @raise [KeyValidationError] When the hex value is invalid
#
# @return [void]
#
def validate_hex_value(_hex_value)
raise 'Subclasses must implement this method'
end
end
end

View File

@@ -10,7 +10,7 @@ module Nostr
# @example # @example
# keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900' # keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
# #
# @return [String] # @return [PrivateKey]
# #
attr_reader :private_key attr_reader :private_key
@@ -21,7 +21,7 @@ module Nostr
# @example # @example
# keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' # keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
# #
# @return [String] # @return [PublicKey]
# #
attr_reader :public_key attr_reader :public_key
@@ -31,16 +31,40 @@ module Nostr
# #
# @example # @example
# keypair = Nostr::KeyPair.new( # keypair = Nostr::KeyPair.new(
# private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900', # private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'),
# public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558', # public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
# ) # )
# #
# @param private_key [String] 32-bytes hex-encoded private key. # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
# @param public_key [String] 32-bytes hex-encoded public 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+
# #
def initialize(private_key:, public_key:) def initialize(private_key:, public_key:)
validate_keys(private_key, public_key)
@private_key = private_key @private_key = private_key
@public_key = public_key @public_key = public_key
end end
private
# Validates the keys
#
# @api private
#
# @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+
#
# @return [void]
#
def validate_keys(private_key, public_key)
raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey)
raise ArgumentError, 'public_key is not an instance of PublicKey' unless public_key.is_a?(Nostr::PublicKey)
end
end end
end end

View File

@@ -44,10 +44,11 @@ module Nostr
# private_key = keygen.generate_private_key # private_key = keygen.generate_private_key
# private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900' # private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
# #
# @return [String] A 32-bytes hex-encoded private key. # @return [PrivateKey] A 32-bytes hex-encoded private key.
# #
def generate_private_key def generate_private_key
(SecureRandom.random_number(group.order - 1) + 1).to_s(16) hex_value = (SecureRandom.random_number(group.order - 1) + 1).to_s(16).rjust(64, '0')
PrivateKey.new(hex_value)
end end
# Extracts a public key from a private key # Extracts a public key from a private key
@@ -59,10 +60,36 @@ module Nostr
# public_key = keygen.extract_public_key(private_key) # public_key = keygen.extract_public_key(private_key)
# public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' # public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
# #
# @return [String] A 32-bytes hex-encoded public key. # @param [PrivateKey] private_key A 32-bytes hex-encoded private key.
#
# @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
#
# @return [PublicKey] A 32-bytes hex-encoded public key.
# #
def extract_public_key(private_key) def extract_public_key(private_key)
group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0') validate_private_key(private_key)
hex_value = group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
PublicKey.new(hex_value)
end
# Builds a key pair from an existing private key
#
# @api public
#
# @example
# private_key = Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900')
# keygen.get_key_pair_from_private_key(private_key)
#
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
#
# @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
#
# @return [Nostr::KeyPair]
#
def get_key_pair_from_private_key(private_key)
validate_private_key(private_key)
public_key = extract_public_key(private_key)
KeyPair.new(private_key:, public_key:)
end end
private private
@@ -74,5 +101,17 @@ module Nostr
# @return [ECDSA::Group] # @return [ECDSA::Group]
# #
attr_reader :group attr_reader :group
# Validates that the private key is an instance of +PrivateKey+
#
# @api private
#
# @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
#
# @return [void]
#
def validate_private_key(private_key)
raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey)
end
end end
end end

36
lib/nostr/private_key.rb Normal file
View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Nostr
# 32-bytes lowercase hex-encoded private key
class PrivateKey < Key
# Human-readable part of the Bech32 encoded address
#
# @api private
#
# @return [String] The human-readable part of the Bech32 encoded address
#
def self.hrp
'nsec'
end
private
# Validates the hex value of the private key
#
# @api private
#
# @param [String] hex_value The private key in hex format
#
# @raise InvalidKeyTypeError when the private key is not a string
# @raise InvalidKeyLengthError when the private key's length is not 64 characters
# @raise InvalidKeyFormatError when the private key is in an invalid format
#
# @return [void]
#
def validate_hex_value(hex_value)
raise InvalidKeyTypeError, 'private' unless hex_value.is_a?(String)
raise InvalidKeyLengthError, 'private' unless hex_value.size == Key::LENGTH
raise InvalidKeyFormatError, 'private' unless hex_value.match(Key::FORMAT)
end
end
end

36
lib/nostr/public_key.rb Normal file
View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Nostr
# 32-bytes lowercase hex-encoded public key
class PublicKey < Key
# Human-readable part of the Bech32 encoded address
#
# @api private
#
# @return [String] The human-readable part of the Bech32 encoded address
#
def self.hrp
'npub'
end
private
# Validates the hex value of the public key
#
# @api private
#
# @param [String] hex_value The public key in hex format
#
# @raise InvalidKeyTypeError when the public key is not a string
# @raise InvalidKeyLengthError when the public key's length is not 64 characters
# @raise InvalidKeyFormatError when the public key is in an invalid format
#
# @return [void]
#
def validate_hex_value(hex_value)
raise InvalidKeyTypeError, 'public' unless hex_value.is_a?(String)
raise InvalidKeyLengthError, 'public' unless hex_value.size == Key::LENGTH
raise InvalidKeyFormatError, 'public' unless hex_value.match(Key::FORMAT)
end
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
module Nostr
# Clients can send 4 types of messages, which must be JSON arrays
module RelayMessageType
# @return [String] Used to notify clients all stored events have been sent
EOSE = 'EOSE'
# @return [String] Used to send events requested to clients
EVENT = 'EVENT'
# @return [String] Used to send human-readable messages to clients
NOTICE = 'NOTICE'
# @return [String] Used to notify clients if an EVENT was successful
OK = 'OK'
end
end

View File

@@ -5,7 +5,7 @@ require 'securerandom'
module Nostr module Nostr
# A subscription the result of a request to receive events from a relay # A subscription the result of a request to receive events from a relay
class Subscription class Subscription
# A random string that should be used to represent a subscription # An arbitrary, non-empty string of max length 64 chars used to represent a subscription
# #
# @api public # @api public
# #
@@ -41,7 +41,7 @@ module Nostr
# @example Subscribing to all events created after a certain time # @example Subscribing to all events created after a certain time
# subscription = Nostr::Subscription.new(filter: Nostr::Filter.new(since: 1230981305)) # subscription = Nostr::Subscription.new(filter: Nostr::Filter.new(since: 1230981305))
# #
# @param id [String] A random string that should be used to represent a subscription # @param id [String] An arbitrary, non-empty string of max length 64 chars used to represent a subscription
# @param filter [Filter] An object that determines what events will be sent in that subscription # @param filter [Filter] An object that determines what events will be sent in that subscription
# #
def initialize(filter:, id: SecureRandom.hex) def initialize(filter:, id: SecureRandom.hex)

View File

@@ -47,17 +47,26 @@ module Nostr
# content: 'Your feedback is appreciated, now pay $8' # content: 'Your feedback is appreciated, now pay $8'
# ) # )
# #
# @param event_attributes [Hash] # @param created_at [Integer] Date of the creation of the vent. A UNIX timestamp, in seconds.
# @option event_attributes [String] :pubkey 32-bytes hex-encoded public key of the event creator. # @param kind [Integer] The kind of the event. An integer from 0 to 3.
# @option event_attributes [Integer] :created_at Date of the creation of the vent. A UNIX timestamp, in seconds. # @param tags [Array<Array>] An array of tags. Each tag is an array of strings.
# @option event_attributes [Integer] :kind The kind of the event. An integer from 0 to 3. # @param content [String] Arbitrary string.
# @option event_attributes [Array<Array>] :tags An array of tags. Each tag is an array of strings.
# @option event_attributes [String] :content Arbitrary string.
# #
# @return [Event] # @return [Event]
# #
def create_event(event_attributes) def create_event(
event = Event.new(**event_attributes.merge(pubkey: keypair.public_key)) kind:,
content:,
created_at: Time.now.to_i,
tags: []
)
event = Event.new(
pubkey: keypair.public_key,
kind:,
content:,
created_at:,
tags:
)
event.sign(keypair.private_key) event.sign(keypair.private_key)
end end
end end

View File

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

View File

@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
spec.summary = 'Client and relay implementation of the Nostr protocol.' spec.summary = 'Client and relay implementation of the Nostr protocol.'
spec.description = 'Client and relay implementation of the Nostr protocol.' spec.description = 'Client and relay implementation of the Nostr protocol.'
spec.homepage = 'https://github.com/wilsonsilva/nostr' spec.homepage = 'https://nostr-ruby.com/'
spec.license = 'MIT' spec.license = 'MIT'
spec.required_ruby_version = '>= 3.2.0' spec.required_ruby_version = '>= 3.2.0'
spec.metadata['rubygems_mfa_required'] = 'true' spec.metadata['rubygems_mfa_required'] = 'true'
@@ -31,8 +31,8 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ['lib'] spec.require_paths = ['lib']
spec.add_dependency 'bech32', '~> 1.3' spec.add_dependency 'bech32', '~> 1.4'
spec.add_dependency 'bip-schnorr', '~> 0.4' spec.add_dependency 'bip-schnorr', '~> 0.6'
spec.add_dependency 'ecdsa', '~> 1.2' spec.add_dependency 'ecdsa', '~> 1.2'
spec.add_dependency 'event_emitter', '~> 0.2' spec.add_dependency 'event_emitter', '~> 0.2'
spec.add_dependency 'faye-websocket', '~> 0.11' spec.add_dependency 'faye-websocket', '~> 0.11'
@@ -47,17 +47,17 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'guard-rubocop', '~> 1.5' spec.add_development_dependency 'guard-rubocop', '~> 1.5'
spec.add_development_dependency 'overcommit', '~> 0.59' spec.add_development_dependency 'overcommit', '~> 0.59'
spec.add_development_dependency 'pry', '~> 0.14' spec.add_development_dependency 'pry', '~> 0.14'
spec.add_development_dependency 'puma', '~> 5.6' spec.add_development_dependency 'puma', '~> 6.4'
spec.add_development_dependency 'rack', '~> 3.0' spec.add_development_dependency 'rack', '~> 3.0'
spec.add_development_dependency 'rake', '~> 13.0' spec.add_development_dependency 'rake', '~> 13.1'
spec.add_development_dependency 'rbs', '~> 2.8' spec.add_development_dependency 'rbs', '~> 3.3'
spec.add_development_dependency 'rspec', '~> 3.12' spec.add_development_dependency 'rspec', '~> 3.12'
spec.add_development_dependency 'rubocop', '~> 1.42' spec.add_development_dependency 'rubocop', '~> 1.57'
spec.add_development_dependency 'rubocop-rake', '~> 0.6' spec.add_development_dependency 'rubocop-rake', '~> 0.6'
spec.add_development_dependency 'rubocop-rspec', '2.16' spec.add_development_dependency 'rubocop-rspec', '2.25'
spec.add_development_dependency 'simplecov', '= 0.17' spec.add_development_dependency 'simplecov', '= 0.17'
spec.add_development_dependency 'simplecov-console', '~> 0.9' spec.add_development_dependency 'simplecov-console', '~> 0.9'
spec.add_development_dependency 'steep', '~> 1.3' spec.add_development_dependency 'steep', '~> 1.6'
spec.add_development_dependency 'typeprof', '~> 0.21' spec.add_development_dependency 'typeprof', '~> 0.21'
spec.add_development_dependency 'yard', '~> 0.9' spec.add_development_dependency 'yard', '~> 0.9'
spec.add_development_dependency 'yard-junk', '~> 0.0.9' spec.add_development_dependency 'yard-junk', '~> 0.0.9'

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

@@ -0,0 +1,14 @@
module Nostr
module Bech32
# Perhaps a bug in RBS/Steep. +decode+ and +encode+ are not recognized as public class methods.
def self?.decode: (String data) -> [String, String]
def self?.encode: (hrp: String, data: String) -> String
def naddr_encode: (pubkey: PublicKey, ?relays: Array[String], ?kind: Integer, ?identifier: String) -> String
def nevent_encode: (id: PublicKey, ?relays: Array[String], ?kind: Integer) -> String
def nprofile_encode: (pubkey: PublicKey, ?relays: Array[String]) -> String
def npub_encode: (String npub) -> String
def nrelay_encode: (String nrelay) -> String
def nsec_encode: (String nsec) -> String
end
end

View File

@@ -5,16 +5,16 @@ module Nostr
def initialize: -> void def initialize: -> void
def connect: (Relay relay) -> Thread def connect: (Relay relay) -> Thread
def subscribe: (?subscription_id: String, ?filter: Filter) -> Subscription def subscribe: (?subscription_id: String, ?filter: Filter) -> Subscription
def unsubscribe: (String subscription_id) -> untyped def unsubscribe: (String subscription_id) -> void
def publish: (Event event) -> untyped def publish: (Event event) -> untyped
private private
attr_reader subscriptions: Hash[String, Subscription] attr_reader subscriptions: Hash[String, Subscription]
attr_reader parent_to_child_channel: untyped attr_reader parent_to_child_channel: EventMachine::Channel
attr_reader child_to_parent_channel: untyped attr_reader child_to_parent_channel: EventMachine::Channel
def execute_within_an_em_thread: { -> untyped } -> Thread def execute_within_an_em_thread: { -> void } -> Thread
def initialize_channels: -> untyped def initialize_channels: -> void
end end
end end

View File

@@ -1,16 +1,16 @@
module Nostr module Nostr
class Crypto class Crypto
BN_BASE: Integer BN_BASE: 0 | 2 | 10 | 16
CIPHER_CURVE: String CIPHER_CURVE: String
CIPHER_ALGORITHM: String CIPHER_ALGORITHM: String
def encrypt_text: (String, String, String) -> String def encrypt_text: (PrivateKey, PublicKey, String) -> String
def decrypt_text: (String, String, String) -> String def decrypt_text: (PrivateKey, PublicKey, String) -> String
def sign_event: (Event, String) -> Event def sign_event: (Event, PrivateKey) -> Event
private private
def compute_shared_key: (String, String) -> String def compute_shared_key: (PrivateKey, PublicKey) -> String
def hash_event:(Event) -> String def hash_event:(Event) -> String
end end
end end

View File

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

View File

@@ -0,0 +1,6 @@
module Nostr
class InvalidHRPError < KeyValidationError
def initialize: (String, String) -> void
end
end

View File

@@ -0,0 +1,5 @@
module Nostr
class InvalidKeyFormatError < KeyValidationError
def initialize: (String) -> void
end
end

View File

@@ -0,0 +1,5 @@
module Nostr
class InvalidKeyLengthError < KeyValidationError
def initialize: (String) -> void
end
end

View File

@@ -0,0 +1,5 @@
module Nostr
class InvalidKeyTypeError < KeyValidationError
def initialize: (String) -> void
end
end

View File

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

View File

@@ -1,6 +1,6 @@
module Nostr module Nostr
class Event class Event
attr_reader pubkey: String attr_reader pubkey: PublicKey
attr_reader created_at: Integer attr_reader created_at: Integer
attr_reader kind: Integer attr_reader kind: Integer
attr_reader tags: Array[Array[String]] attr_reader tags: Array[Array[String]]
@@ -9,7 +9,7 @@ module Nostr
attr_accessor sig: String?|nil attr_accessor sig: String?|nil
def initialize: ( def initialize: (
pubkey: String, pubkey: PublicKey,
kind: Integer, kind: Integer,
content: String, content: String,
?created_at: Integer, ?created_at: Integer,
@@ -31,9 +31,9 @@ module Nostr
} }
def ==: (Event other) -> bool def ==: (Event other) -> bool
def sign:(String) -> Event def sign:(PrivateKey) -> Event
def add_event_reference: (String) -> Array[Array[String]] def add_event_reference: (String) -> Array[Array[String]]
def add_pubkey_reference: (String) -> Array[Array[String]] def add_pubkey_reference: (PublicKey) -> Array[Array[String]]
end end
end end

View File

@@ -3,8 +3,8 @@ module Nostr
class EncryptedDirectMessage < Event class EncryptedDirectMessage < Event
def initialize: ( def initialize: (
plain_text: String, plain_text: String,
sender_private_key: String, sender_private_key: PrivateKey,
recipient_public_key: String, recipient_public_key: PublicKey,
?previous_direct_message: String|nil ?previous_direct_message: String|nil
) -> void ) -> void
end end

View File

@@ -3,23 +3,14 @@ module Nostr
attr_reader ids: Array[String] attr_reader ids: Array[String]
attr_reader authors: Array[String] attr_reader authors: Array[String]
attr_reader kinds: Array[Integer] attr_reader kinds: Array[Integer]
attr_reader e: String attr_reader e: Array[String]
attr_reader p: String attr_reader p: Array[String]
attr_reader since: Integer attr_reader since: Integer
attr_reader until: Integer attr_reader until: Integer
attr_reader limit: Integer attr_reader limit: Integer
def initialize: (**untyped) -> void def initialize: (**untyped) -> void
def to_h: -> { def to_h: -> Hash[::Symbol, (::Array[::String] | ::Array[::Integer] | ::Integer)]
ids: Array[String],
authors: Array[String],
kinds: Array[Integer],
e: String,
p: String,
since: Integer,
until: Integer,
limit: Integer
}
def ==: (Filter other) -> bool def ==: (Filter other) -> bool
end end
end end

16
sig/nostr/key.rbs Normal file
View File

@@ -0,0 +1,16 @@
module Nostr
class Key < String
FORMAT: Regexp
LENGTH: int
def self.from_bech32: (String) -> Key
def self.hrp: -> String
def initialize: (String) -> void
def to_bech32: -> String
private
def validate_hex_value: (String) -> nil
end
end

View File

@@ -1,9 +1,13 @@
# Classes # Classes
module Nostr module Nostr
class KeyPair class KeyPair
attr_reader private_key: String attr_reader private_key: PrivateKey
attr_reader public_key: String attr_reader public_key: PublicKey
def initialize: (private_key: String, public_key: String) -> void def initialize: (private_key: PrivateKey, public_key: PublicKey) -> void
private
def validate_keys: (PrivateKey, PublicKey) -> void
end end
end end

View File

@@ -3,11 +3,14 @@ module Nostr
class Keygen class Keygen
def initialize: -> void def initialize: -> void
def generate_key_pair: -> KeyPair def generate_key_pair: -> KeyPair
def generate_private_key: -> String def generate_private_key: -> PrivateKey
def extract_public_key: (String private_key) -> String def extract_public_key: (PrivateKey private_key) -> PublicKey
def get_key_pair_from_private_key: (PrivateKey private_key) -> KeyPair
private private
attr_reader group: untyped attr_reader group: untyped
def validate_private_key: (PrivateKey private_key) -> void
end end
end end

View File

@@ -0,0 +1,4 @@
module Nostr
class PrivateKey < Key
end
end

4
sig/nostr/public_key.rbs Normal file
View File

@@ -0,0 +1,4 @@
module Nostr
class PublicKey < Key
end
end

View File

@@ -0,0 +1,8 @@
module Nostr
module RelayMessageType
EOSE: String
EVENT: String
NOTICE: String
OK: String
end
end

View File

@@ -5,14 +5,10 @@ module Nostr
def initialize: (?keypair: KeyPair | nil, ?keygen: Keygen) -> void def initialize: (?keypair: KeyPair | nil, ?keygen: Keygen) -> void
def create_event: ( def create_event: (
{ kind: Integer,
pubkey: String, content: String,
created_at: Integer, ?created_at: Integer,
kind: Integer, ?tags: Array[Array[String]],
tags: Array[String],
content: String,
created_at: Integer,
}
) -> Event ) -> Event
private private

25
sig/vendor/bech32.rbs vendored Normal file
View File

@@ -0,0 +1,25 @@
# Added only to satisfy the Steep requirements. Not 100% reliable.
module Bech32
SEPARATOR: String
BECH32M_CONST: Integer
def encode: (untyped hrp, untyped data, untyped spec) -> untyped
def self.encode: (untyped hrp, untyped data, untyped spec) -> untyped
def decode: (untyped bech, ?Integer max_length) -> [untyped, untyped, Integer]?
def self.decode: (untyped bech, ?Integer max_length) -> [untyped, untyped, Integer]?
def create_checksum: (untyped hrp, untyped data, untyped spec) -> Array[Integer]
def self.create_checksum: (untyped hrp, untyped data, untyped spec) -> Array[Integer]
def verify_checksum: (untyped hrp, untyped data) -> Integer?
def self.verify_checksum: (untyped hrp, untyped data) -> Integer?
def expand_hrp: (untyped hrp) -> untyped
def self.expand_hrp: (untyped hrp) -> untyped
def convert_bits: (untyped data, untyped from, untyped to, ?true padding) -> Array[Integer]?
def self.convert_bits: (untyped data, untyped from, untyped to, ?true padding) -> Array[Integer]?
def polymod: (untyped values) -> Integer
def self.polymod: (untyped values) -> Integer
module Encoding
BECH32: Integer
BECH32M: Integer
end
end

41
sig/vendor/bech32/nostr/entity.rbs vendored Normal file
View File

@@ -0,0 +1,41 @@
# Added only to satisfy the Steep requirements. Not 100% reliable.
module Bech32
module Nostr
class BareEntity
attr_reader hrp: untyped
attr_reader data: untyped
def initialize: (untyped hrp, untyped data) -> void
def encode: -> untyped
end
class TLVEntry
attr_reader type: (Float | Integer | String)?
attr_reader label: String?
attr_reader value: (Float | Integer | String)?
def initialize: ((Float | Integer | String)? `type`, (Float | Integer | String)? value, ?String? label) -> void
def to_payload: -> String
def to_s: -> String
private
def hex_string?: ((Float | Integer | String)? str) -> bool
end
class TLVEntity
TYPE_SPECIAL: Integer
TYPE_RELAY: Integer
TYPE_AUTHOR: Integer
TYPE_KIND: Integer
TYPES: [Integer, Integer, Integer, Integer]
attr_reader hrp: untyped
attr_reader entries: Array[TLVEntry]
def initialize: (untyped hrp, Array[TLVEntry] entries) -> void
def self.parse: (untyped hrp, untyped data) -> TLVEntity
def encode: -> untyped
end
end
end

20
sig/vendor/bech32/nostr/nip19.rbs vendored Normal file
View File

@@ -0,0 +1,20 @@
# Added only to satisfy the Steep requirements. Not 100% reliable.
module Bech32
module Nostr
module NIP19
HRP_PUBKEY: String
HRP_PRIVATE_KEY: String
HRP_NOTE_ID: String
HRP_PROFILE: String
HRP_EVENT: String
HRP_RELAY: String
HRP_EVENT_COORDINATE: String
BARE_PREFIXES: [String, String, String]
TLV_PREFIXES: [String, String, String, String]
ALL_PREFIXES: Array[String]
def decode: (untyped string) -> untyped
def self.decode: (untyped string) -> untyped
end
end
end

21
sig/vendor/bech32/segwit_addr.rbs vendored Normal file
View File

@@ -0,0 +1,21 @@
# Added only to satisfy the Steep requirements. Not 100% reliable.
module Bech32
class SegwitAddr
HRP_MAINNET: String
HRP_TESTNET: String
HRP_REGTEST: String
attr_accessor hrp: String
attr_accessor ver: (Float | Integer | String)?
attr_accessor prog: Array[(Float | Integer | String)?]
def initialize: (?nil addr) -> void
def to_script_pubkey: -> ((Float | Integer | String)?)
def script_pubkey=: (untyped script_pubkey) -> (Array[(Float | Integer | String)?])
def addr: -> untyped
private
def parse_addr: (untyped addr) -> nil
end
end

View File

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

View File

@@ -6,7 +6,7 @@ module EventMachine
def initialize: -> void def initialize: -> void
def num_subscribers: -> Integer def num_subscribers: -> Integer
def subscribe: (*untyped a) ?{ -> untyped } -> Integer def subscribe: (*untyped a) ?{ (untyped) -> untyped } -> Integer
def unsubscribe: (untyped name) -> untyped def unsubscribe: (untyped name) -> untyped
def push: (*untyped items) -> untyped def push: (*untyped items) -> untyped
alias << push alias << push

30
sig/vendor/faye/websocket.rbs vendored Normal file
View File

@@ -0,0 +1,30 @@
# Added only to satisfy the Steep requirements. Not 100% reliable.
module Faye
class WebSocket
ADAPTERS: Hash[String, :Goliath | :Rainbows | :Thin]
@url: String
@driver_started: false
@stream: Stream
@driver: bot
def self.determine_url: (untyped env, ?[String, String] schemes) -> String
def self.ensure_reactor_running: -> nil
def self.load_adapter: (untyped backend) -> bool?
def self.secure_request?: (untyped env) -> bool
def self.websocket?: (untyped env) -> untyped
attr_reader env: untyped
def initialize: (untyped env, ?nil protocols, ?Hash[untyped, untyped] options) -> void
def start_driver: -> nil
def rack_response: -> [Integer, Hash[untyped, untyped], Array[untyped]]
class Stream
@socket_object: bot
def fail: -> untyped
def receive: (untyped data) -> untyped
end
end
end

45
sig/vendor/faye/websocket/api.rbs vendored Normal file
View File

@@ -0,0 +1,45 @@
# Added only to satisfy the Steep requirements. Not 100% reliable.
module Faye
class WebSocket
module API
CONNECTING: Integer
OPEN: Integer
CLOSING: Integer
CLOSED: Integer
CLOSE_TIMEOUT: Integer
@driver: untyped
@ping: nil
@ping_id: Integer
@stream: nil
@proxy: nil
@ping_timer: nil
@close_timer: nil
@close_params: [String, Integer]?
@onerror: nil
@onclose: nil
@onmessage: nil
@onopen: nil
attr_reader url: untyped
attr_reader ready_state: Integer
attr_reader buffered_amount: Integer
def initialize: (?Hash[untyped, untyped] options) -> void
def write: (untyped data) -> untyped
def send: (untyped message) -> false
def ping: (?String message) -> false
def close: (?nil code, ?nil reason) -> untyped
def protocol: -> String
private
def open: -> nil
def receive_message: (untyped data) -> nil
def emit_error: (untyped message) -> nil
def begin_close: (String reason, Integer code, ?Hash[untyped, untyped] options) -> nil
def finalize_close: -> nil
def parse: (untyped data) -> untyped
end
end
end

43
sig/vendor/faye/websocket/client.rbs vendored Normal file
View File

@@ -0,0 +1,43 @@
# Added only to satisfy the Steep requirements. Not 100% reliable.
module Faye
class WebSocket
class Client
DEFAULT_PORTS: Hash[String, Integer]
SECURE_PROTOCOLS: [String, String]
include EventEmitter
include API
@url: untyped
@endpoint: untyped
@origin_tls: Hash[untyped, untyped]
@socket_tls: Hash[untyped, untyped]
@driver: bot
@proxy: nil
@ssl_verifier: untyped
@stream: untyped
def initialize: (untyped url, ?Array[String] protocols, ?Hash[untyped, untyped] options) -> void
private
def configure_proxy: (Hash[untyped, untyped] proxy) -> nil
def start_tls: (untyped uri, Hash[untyped, untyped] options) -> nil
def on_connect: (untyped stream) -> untyped
def on_network_error: (nil error) -> untyped
def ssl_verify_peer: (untyped cert) -> untyped
def ssl_handshake_completed: -> untyped
module Connection
attr_accessor parent: bot
def connection_completed: -> untyped
def ssl_verify_peer: (untyped cert) -> untyped
def ssl_handshake_completed: -> untyped
def receive_data: (untyped data) -> untyped
def unbind: (?nil error) -> untyped
def write: (untyped data) -> nil
end
end
end
end

139
spec/nostr/bech32_spec.rb Normal file
View File

@@ -0,0 +1,139 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::Bech32 do
let(:keypair) { Nostr::Keygen.new.generate_key_pair }
let(:private_key) { keypair.private_key }
let(:public_key) { keypair.public_key }
describe '.encode' do
it 'encodes data into the bech32 format' do
npub = described_class.encode(hrp: 'npub', data: public_key)
expect(npub).to match(/npub1\w+/)
end
end
describe '.decode' do
it 'decodes data from the bech32 format' do
npub = described_class.encode(hrp: 'npub', data: public_key)
type, decoded = described_class.decode(npub)
aggregate_failures do
expect(type).to eq('npub')
expect(decoded).to eq(public_key)
end
end
end
describe '.nsec_encode' do
it 'encodes and decodes hexadecimal private keys' do
nsec = described_class.nsec_encode(private_key)
type, data = described_class.decode(nsec)
aggregate_failures do
expect(nsec).to match(/nsec1\w+/)
expect(type).to eq('nsec')
expect(data).to eq(private_key)
end
end
end
describe '.npub_encode' do
it 'encodes and decodes hexadecimal public keys' do
npub = described_class.npub_encode(public_key)
type, data = described_class.decode(npub)
aggregate_failures do
expect(npub).to match(/npub1\w+/)
expect(type).to eq('npub')
expect(data).to eq(public_key)
end
end
end
describe '.nprofile_encode' do
it 'encodes and decodes nprofiles with relays' do
relay_urls = %w[wss://relay.damus.io wss://nos.lol]
nprofile = described_class.nprofile_encode(pubkey: public_key, relays: relay_urls)
type, profile = described_class.decode(nprofile)
aggregate_failures do
expect(nprofile).to match(/nprofile1\w+/)
expect(type).to eq('nprofile')
expect(profile.entries[0].value).to eq(public_key)
expect(profile.entries[1].value).to eq(relay_urls[0])
expect(profile.entries[2].value).to eq(relay_urls[1])
end
end
it 'encodes and decodes nprofiles without relays' do
nprofile = described_class.nprofile_encode(pubkey: public_key)
type, profile = described_class.decode(nprofile)
aggregate_failures do
expect(nprofile).to match(/nprofile1\w+/)
expect(type).to eq('nprofile')
expect(profile.entries[0].value).to eq(public_key)
end
end
end
describe '.naddr_encode' do
it 'encodes and decodes naddr' do
relay_urls = %w[wss://relay.damus.io wss://nos.lol]
naddr = described_class.naddr_encode(
pubkey: public_key,
relays: relay_urls,
kind: 1984,
identifier: 'damus'
)
type, addr = described_class.decode(naddr)
aggregate_failures do
expect(naddr).to match(/naddr1\w+/)
expect(type).to eq('naddr')
expect(addr.entries[0].value).to eq(public_key)
expect(addr.entries[1].value).to eq(relay_urls[0])
expect(addr.entries[2].value).to eq(relay_urls[1])
expect(addr.entries[3].value).to eq(1984)
expect(addr.entries[4].value).to eq('damus')
end
end
end
describe '.nevent_encode' do
it 'encodes and decodes nevent' do
relay_urls = %w[wss://relay.damus.io wss://nos.lol]
nevent = described_class.nevent_encode(
id: '0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3',
relays: relay_urls,
kind: Nostr::EventKind::TEXT_NOTE
)
type, event = described_class.decode(nevent)
aggregate_failures do
expect(nevent).to match(/nevent1\w+/)
expect(type).to eq('nevent')
expect(event.entries[0].value).to eq('0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3')
expect(event.entries[1].value).to eq(relay_urls[0])
expect(event.entries[2].value).to eq(relay_urls[1])
expect(event.entries[3].value).to eq(Nostr::EventKind::TEXT_NOTE)
end
end
end
describe '.nrelay_encode' do
it 'encodes and decodes nrelay' do
relay_url = 'wss://relay.damus.io'
nrelay = described_class.nrelay_encode(relay_url)
type, data = described_class.decode(nrelay)
aggregate_failures do
expect(nrelay).to match(/nrelay1\w+/)
expect(type).to eq('nrelay')
expect(data.entries[0].value).to eq(relay_url)
end
end
end
end

View File

@@ -8,8 +8,8 @@ RSpec.describe Nostr::Crypto do
describe '#sign_event' do describe '#sign_event' do
let(:keypair) do let(:keypair) do
Nostr::KeyPair.new( Nostr::KeyPair.new(
public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca', public_key: Nostr::PublicKey.new('8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca'),
private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757' private_key: Nostr::PrivateKey.new('3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757')
) )
end end
@@ -34,15 +34,15 @@ RSpec.describe Nostr::Crypto do
describe '#encrypt_text' do describe '#encrypt_text' do
let(:sender_keypair) do let(:sender_keypair) do
Nostr::KeyPair.new( Nostr::KeyPair.new(
public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca', public_key: Nostr::PublicKey.new('8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca'),
private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757' private_key: Nostr::PrivateKey.new('3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757')
) )
end end
let(:recipient_keypair) do let(:recipient_keypair) do
Nostr::KeyPair.new( Nostr::KeyPair.new(
public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0', public_key: Nostr::PublicKey.new('6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0'),
private_key: '22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf' private_key: Nostr::PrivateKey.new('22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf')
) )
end end
@@ -57,23 +57,34 @@ RSpec.describe Nostr::Crypto do
describe '#descrypt_text' do describe '#descrypt_text' do
let(:sender_keypair) do let(:sender_keypair) do
Nostr::KeyPair.new( Nostr::KeyPair.new(
public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca', public_key: Nostr::PublicKey.new('8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca'),
private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757' private_key: Nostr::PrivateKey.new('3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757')
) )
end end
let(:recipient_keypair) do let(:recipient_keypair) do
Nostr::KeyPair.new( Nostr::KeyPair.new(
public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0', public_key: Nostr::PublicKey.new('6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0'),
private_key: '22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf' private_key: Nostr::PrivateKey.new('22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf')
) )
end end
it 'decrypts an encrypted text' do context 'when the encrypted text includes an iv query string' do
encrypted_text = crypto.encrypt_text(sender_keypair.private_key, recipient_keypair.public_key, 'Twitter Files') it 'decrypts an encrypted text' do
decrypted_text = crypto.decrypt_text(recipient_keypair.private_key, sender_keypair.public_key, encrypted_text) encrypted_text = crypto.encrypt_text(sender_keypair.private_key, recipient_keypair.public_key, 'Twitter Files')
decrypted_text = crypto.decrypt_text(recipient_keypair.private_key, sender_keypair.public_key, encrypted_text)
expect(decrypted_text).to eq('Twitter Files') expect(decrypted_text).to eq('Twitter Files')
end
end
context 'when the encrypted text does not include an iv query string' do
it 'returns an empty string' do
encrypted_text = 'wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?it=v38vAJ3LlJAGZxbmWU4qAg=='
decrypted_text = crypto.decrypt_text(recipient_keypair.private_key, sender_keypair.public_key, encrypted_text)
expect(decrypted_text).to eq('')
end
end end
end end
end end

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::InvalidHRPError do
describe '#initialize' do
let(:given_hrp) { 'nwrong' }
let(:allowed_hrp) { 'nsec' }
let(:error) { described_class.new(given_hrp, allowed_hrp) }
it 'builds a useful error message' do
expect(error.message).to eq("Invalid hrp: nwrong. The allowed hrp value for this kind of entity is 'nsec'.")
end
end
end

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::InvalidKeyTypeError do
describe '#initialize' do
let(:key_kind) { 'private' }
let(:error) { described_class.new(key_kind) }
it 'builds a useful error message' do
expect(error.message).to eq('Invalid private key type')
end
end
end

View File

@@ -233,8 +233,8 @@ RSpec.describe Nostr::Event do
end end
let(:keypair) do let(:keypair) do
Nostr::KeyPair.new( Nostr::KeyPair.new(
public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca', public_key: Nostr::PublicKey.new('8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca'),
private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757' private_key: Nostr::PrivateKey.new('3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757')
) )
end end

View File

@@ -6,15 +6,15 @@ RSpec.describe Nostr::Events::EncryptedDirectMessage do
describe '.new' do describe '.new' do
let(:sender_keypair) do let(:sender_keypair) do
Nostr::KeyPair.new( Nostr::KeyPair.new(
public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca', public_key: Nostr::PublicKey.new('8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca'),
private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757' private_key: Nostr::PrivateKey.new('3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757')
) )
end end
let(:recipient_keypair) do let(:recipient_keypair) do
Nostr::KeyPair.new( Nostr::KeyPair.new(
public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0', public_key: Nostr::PublicKey.new('6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0'),
private_key: '22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf' private_key: Nostr::PrivateKey.new('22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf')
) )
end end

View File

@@ -5,19 +5,43 @@ require 'spec_helper'
RSpec.describe Nostr::KeyPair do RSpec.describe Nostr::KeyPair do
let(:keypair) do let(:keypair) do
described_class.new( described_class.new(
private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900', private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'),
public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558')
) )
end end
describe '.new' do describe '.new' do
it 'creates an instance of a key pair' do context 'when private_key is not an instance of PrivateKey' do
keypair = described_class.new( it 'raises an error' do
private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900', expect do
public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' described_class.new(
) private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558')
)
end.to raise_error(ArgumentError, 'private_key is not an instance of PrivateKey')
end
end
expect(keypair).to be_an_instance_of(described_class) context 'when public_key is not an instance of PublicKey' do
it 'raises an error' do
expect do
described_class.new(
private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'),
public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
)
end.to raise_error(ArgumentError, 'public_key is not an instance of PublicKey')
end
end
context 'when private_key is an instance of PrivateKey and public_key is an instance of PublicKey' do
it 'creates an instance of a key pair' do
keypair = described_class.new(
private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'),
public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558')
)
expect(keypair).to be_an_instance_of(described_class)
end
end end
end end

73
spec/nostr/key_spec.rb Normal file
View File

@@ -0,0 +1,73 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::Key do
let(:subclass) do
Class.new(Nostr::Key) do
def self.hrp
'npub'
end
protected
def validate_hex_value(_hex_value) = nil
end
end
let(:valid_hex) { 'a' * 64 }
describe '.new' do
it 'raises an error because this is an abstract class' do
expect { described_class.new(valid_hex) }.to raise_error(/Subclasses must implement this method/)
end
end
describe '.from_bech32' do
context 'when given a valid Bech32 value' do
let(:valid_bech32) { 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg' }
it 'creates a new key' do
expect { subclass.from_bech32(valid_bech32) }.not_to raise_error
end
end
context 'when given an invalid Bech32 value' do
let(:invalid_bech32) { 'this is obviously not valid' }
it 'raises an error' do
expect { subclass.from_bech32(invalid_bech32) }.to raise_error(ArgumentError, /Invalid nip19 string\./)
end
end
end
describe '.hrp' do
context 'when called on the abstract class' do
it 'raises an error because this is an abstract method' do
expect { described_class.hrp }.to raise_error(/Subclasses must implement this method/)
end
end
context 'when called on a subclass' do
it 'returns the human readable part of a Bech32 string' do
expect(subclass.hrp).to eq('npub')
end
end
end
describe '#to_bech32' do
let(:key) { subclass.new(valid_hex) }
it 'returns a bech32 string representation of the key' do
expect(key.to_bech32).to match(/^npub[0-9a-z]+$/)
end
end
describe '#validate_hex_value' do
let(:invalid_hex) { 'g' * 64 }
it 'raises an error because this is an abstract method' do
expect { described_class.new(invalid_hex) }.to raise_error(/Subclasses must implement this method/)
end
end
end

View File

@@ -13,6 +13,31 @@ RSpec.describe Nostr::Keygen do
end end
end end
describe '.get_key_pair_from_private_key' do
context 'when given a private key' do
let(:private_key) { Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900') }
it 'generates a key pair' do
keypair = keygen.get_key_pair_from_private_key(private_key)
aggregate_failures do
expect(keypair.private_key).to be_an_instance_of(Nostr::PrivateKey)
expect(keypair.public_key).to be_an_instance_of(Nostr::PublicKey)
end
end
end
context 'when given another kind of value' do
let(:not_a_private_key) { 'something else' }
it 'raises an error' do
expect { keygen.get_key_pair_from_private_key(not_a_private_key) }.to raise_error(
ArgumentError, 'private_key is not an instance of PrivateKey'
)
end
end
end
describe '#generate_key_pair' do describe '#generate_key_pair' do
it 'generates a private/public key pair' do it 'generates a private/public key pair' do
keypair = keygen.generate_key_pair keypair = keygen.generate_key_pair
@@ -33,11 +58,24 @@ RSpec.describe Nostr::Keygen do
end end
describe '#extract_public_key' do describe '#extract_public_key' do
it 'extracts a public key from a private key' do context 'when the given value is not a private key' do
private_key = '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900' let(:not_a_private_key) { 'something else' }
public_key = keygen.extract_public_key(private_key)
expect(public_key).to eq('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558') it 'raises an error' do
expect { keygen.extract_public_key(not_a_private_key) }.to raise_error(
ArgumentError, 'private_key is not an instance of PrivateKey'
)
end
end
context 'when the given value is a private key' do
let(:private_key) { Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900') }
it 'extracts a public key from a private key' do
public_key = keygen.extract_public_key(private_key)
expect(public_key).to eq('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558')
end
end end
end end
end end

View File

@@ -0,0 +1,82 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::PrivateKey do
let(:valid_hex) { '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa' }
let(:private_key) { described_class.new(valid_hex) }
describe '.new' do
context 'when the private key is not a string' do
it 'raises an InvalidKeyTypeError' do
expect { described_class.new(1234) }.to raise_error(
Nostr::InvalidKeyTypeError,
'Invalid private key type'
)
end
end
context "when the private key's length is not 64 characters" do
it 'raises an InvalidKeyLengthError' do
expect { described_class.new('a' * 65) }.to raise_error(
Nostr::InvalidKeyLengthError,
'Invalid private key length. It should have 64 characters.'
)
end
end
context 'when the private key contains non-hexadecimal characters' do
it 'raises an InvalidKeyFormatError' do
expect { described_class.new('g' * 64) }.to raise_error(
Nostr::InvalidKeyFormatError,
'Only lowercase hexadecimal characters are allowed in private keys.'
)
end
end
context 'when the private key contains uppercase characters' do
it 'raises an InvalidKeyFormatError' do
expect { described_class.new('A' * 64) }.to raise_error(
Nostr::InvalidKeyFormatError,
'Only lowercase hexadecimal characters are allowed in private keys.'
)
end
end
context 'when the private key is valid' do
it 'does not raise any error' do
expect { described_class.new('a' * 64) }.not_to raise_error
end
end
end
describe '.from_bech32' do
context 'when given a valid Bech32 value' do
let(:valid_bech32) { 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5' }
it 'instantiates a private key from a Bech32 encoded string' do
expect(described_class.from_bech32(valid_bech32)).to eq(valid_hex)
end
end
context 'when given an invalid Bech32 value' do
let(:invalid_bech32) { 'this is obviously not valid' }
it 'raises an error' do
expect { described_class.from_bech32(invalid_bech32) }.to raise_error(ArgumentError, /Invalid nip19 string\./)
end
end
end
describe '.hrp' do
it 'returns the human readable part of a Bech32 string' do
expect(described_class.hrp).to eq('nsec')
end
end
describe '#to_bech32' do
it 'converts the hex key to bech32' do
expect(private_key.to_bech32).to eq('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5')
end
end
end

View File

@@ -0,0 +1,82 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Nostr::PublicKey do
let(:valid_hex) { '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e' }
let(:public_key) { described_class.new(valid_hex) }
describe '.new' do
context 'when the public key is not a string' do
it 'raises an InvalidKeyTypeError' do
expect { described_class.new(1234) }.to raise_error(
Nostr::InvalidKeyTypeError,
'Invalid public key type'
)
end
end
context "when the public key's length is not 64 characters" do
it 'raises an InvalidKeyLengthError' do
expect { described_class.new('a' * 65) }.to raise_error(
Nostr::InvalidKeyLengthError,
'Invalid public key length. It should have 64 characters.'
)
end
end
context 'when the public key contains non-hexadecimal characters' do
it 'raises an InvalidKeyFormatError' do
expect { described_class.new('g' * 64) }.to raise_error(
Nostr::InvalidKeyFormatError,
'Only lowercase hexadecimal characters are allowed in public keys.'
)
end
end
context 'when the public key contains uppercase characters' do
it 'raises an InvalidKeyFormatError' do
expect { described_class.new('A' * 64) }.to raise_error(
Nostr::InvalidKeyFormatError,
'Only lowercase hexadecimal characters are allowed in public keys.'
)
end
end
context 'when the public key is valid' do
it 'does not raise any error' do
expect { described_class.new('a' * 64) }.not_to raise_error
end
end
end
describe '.from_bech32' do
context 'when given a valid Bech32 value' do
let(:valid_bech32) { 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg' }
it 'instantiates a public key from a Bech32 encoded string' do
expect(described_class.from_bech32(valid_bech32)).to eq(valid_hex)
end
end
context 'when given an invalid Bech32 value' do
let(:invalid_bech32) { 'this is obviously not valid' }
it 'raises an error' do
expect { described_class.from_bech32(invalid_bech32) }.to raise_error(ArgumentError, /Invalid nip19 string\./)
end
end
end
describe '.hrp' do
it 'returns the human readable part of a Bech32 string' do
expect(described_class.hrp).to eq('npub')
end
end
describe '#to_bech32' do
it 'converts the hex key to bech32' do
expect(public_key.to_bech32).to eq('npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg')
end
end
end

View File

@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Nostr::User do RSpec.describe Nostr::User do
let(:keypair) do let(:keypair) do
Nostr::KeyPair.new( Nostr::KeyPair.new(
private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900', private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'),
public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558')
) )
end end

Some files were not shown because too many files have changed in this diff Show More